The $.fn.sendkeys Plugin

This is now obsolete. sendkeys is at version 4, and is documented at "Rethinking $.fn.sendkeys".

I wanted to make a general-purpose onscreen keypad, and wasted a huge amount of time trying to find a way to simulate a keypress. $(element).trigger("keypress",...) won't work. Neither will keyup or keydown. For security reasons, I guess, you can't tell an element to pretend a key was pressed. The browser is too worried that you will access menus or something.

So I wrote my own plugin and named it after Microsoft's sendkeys which does similar things. For any editable element elem, $(elem).sendkeys(string) inserts string at the insertion point or selection. It's the insertion point sensitivity that makes it more sophisticated than elem.value += string.

It depends on my bililiteRange routines to manage the selections in a cross-browser way.

Downloads

See $.fn.sendkeys on github.

See bililiteRange on github.

See the demo.

Usage

It's very simple, $(element).sendkeys(string) inserts string at the insertion point in an input, textarea or other element with contenteditable=true. If the insertion point is not currently in the element, it remembers where the insertion point was when sendkeys was last called (if the insertion point was never in the element, it appends to the end).

Special keys

"Special keys" are indicated by curly braces. The following are defined:

{backspace}
Delete backwards
{del}
Delete forwards
{rightarrow}
Move the insertion point to the right
{leftarrow}
Move the insertion point to the left
{selectall}
Select the entire field
{enter}
Insert a newline. Warning: In contenteditable elements, {enter} is flaky and inconsistent across browsers. This is due to the flakiness of contenteditable itself; I can't figure out what to do about this.
{tab}
Insert a '\t' character. $().sendkeys('\t') would work just as well, but there are circumstances when I wanted to avoid having to escape backslashes.
{newline}
Insert a '\n' character, without the mangling that {enter} does.
{{}
Inserts a { by itself
{selection}
Inserts the text of the original selection (useful for creating "wrapping" functions, like "<em>{selection}</em>").
{mark}
Remembers the current insertion point and restores it after the sendkeys call. Thus "<p>{mark}</p>" inserts <p></p> and leaves the insertion point between the tags.
So $(elem).sendkeys('1234') inserts 1234, $(elem).sendkeys('123{backspace}4') inserts 124, and $(elem).sendkeys('1234{leftarrow}{leftarrow}{leftarrow}{del}') inserts 134, and $(elem).sendkeys('<a href="{mark}">{selection}</a>') turns the current selection into the text of a hyperlink and leaves the insertion point in position to type the link itself.

I used Microsoft's key-escaping notation rather than backslashes because putting backslashes in strings means escaping them, and I always get lost in the forest of slashes. Unlike the Microsoft function, this does not use metacharacters (+^%~).

Special keys

There are no real options to the plugin call itself, but you can set your own special keys in the bililiteRange.sendkeys object. The defaults are:


bililiteRange.sendkeys = {
	'{backspace}': function (rng){...},
	'{rightarrow}': function (rng){...},
	'{leftarrow}': function (rng){...},
	etc.
};

rng is the bililiteRange object being manipulated. The default action (for anything not in braces) is rng.text(string, 'end'). You can create synonyms easily, like bililiteRange.sendkeys['{BS}'] = bililiteRange.sendkeys['{backspace}']. And adding new functions is like bililiteRange.sendkeys['{selectall}'] = function(rng) {rng.bounds('all')}. Using an undefined "specialkey" just inserts the name, so $(el).sendkeys('{foo}') inserts 'foo'. The original text of the range is stored in rng.data().sendkeysOriginalText, so a way to show the selected text would be bililiteRange.sendkeys['{console}'] = function (rng) {console.log(rng.data().sendkeysOriginalText).

The jQuery plugin (but not the bililiteRange function) turns '\n' into '{enter}' in the input string. It can be escaped to just insert the newline by quoting it in braces: '{\n}'. This mangling turned out to be less useful than I thought, but I kept it in for backwards compatibility.

bililiteRange function

The jQuery plugin is just a wrapper for the bililiteRange(element).sendkeys(string) function. The plugin is simply:

$.fn.sendkeys = function (x){
	x = x.replace(/([^{])\n/g, '$1{enter}'); // turn line feeds into explicit break insertions, but not if escaped
	return this.each( function(){
		bililiteRange(this).bounds('selection').sendkeys(x).select();
		this.focus();
	});
}; // sendkeys

So the plugin manipulates the element (acting on the selection, then selecting the result), while the bililiteRange function affects the text but does not change the selection.

Events

To help simulate an actual keypress, the plugin does a trigger('keypress') with the event keyCode, charCode and which all set to the ascii value of the letter sent. This is only triggered with simplechar; special characters do not trigger that event.

In addition, trigger('sendkeys') (a custom event) is executed, with event.which set to the original string that was sent.


$('.selectoutput').click(function(){
	$('.output').removeClass('selected');
	var index = $(this).parents('th').index();
	$('.output').eq(index).addClass('selected').focus();
});
$('div.test input:button').click(function(){
	$('.output.selected').sendkeys($('div.test input:text').val());
});
$('div.wrap input:button').click(function(){
	var tag = $('div.wrap select').val();
	$('.output.selected').sendkeys('<'+tag+'>{selection}{mark}</'+tag+'>');
});
$('.phonepad input').click(function(){
	$('.output.selected').sendkeys(this.name || this.value);
});
$('.output').each(function(){
	bililiteRange(this); // initialize the selection tracking
}).on('keypress', function(evt){
	$('#keypress').text($('#keypress').text()+' '+evt.which);
}).on('sendkeys', function(evt){
	$('#sendkeys').text($('#sendkeys').text()+' '+evt.which);
}).on('focus', function(){
	var index = $(this).parents('td').index();
	$('.output').removeClass('selected');
	$('.output').eq(index).addClass('selected')
	$('.selectoutput').eq(index).attr('checked',true);;
});

<div>
	<table style="width: 100%" border="2" id="demo" >
		<thead>
			<tr>
				<th><label>
					<input type="radio" class="selectoutput" name="selectoutput" checked="checked" />
					<code>&lt;input&gt;</code>
				</label></th>
				<th><label>
					<input type="radio" class="selectoutput" name="selectoutput" />
					<code>&lt;textarea&gt;</code>
				</label></th>
				<th><label>
					<input type="radio" class="selectoutput" name="selectoutput" />
					<code>&lt;div&gt;</code>
				</label></th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<td><input type="text" class="output selected" /></td>
				<td><textarea class="output"></textarea></td>
				<td><div class="output" contentEditable="true"></div></td>
			</tr>
		</tbody>
	</table>
<div class="phonepad">
<input type="button" name="{leftarrow}" value="&larr;"/><input type="button" name="{rightarrow}" value="&rarr;"/><input type="button" name="{backspace}" value="BS"/><input type="button" name="{selectall}" value="All"/><br/>
<input type="button" value="7" /><input type="button" value="8" /><input type="button" value="9" /><br/>
<input type="button" value="4" /><input type="button" value="5" /><input type="button" value="6" /><br/>
<input type="button" value="1" /><input type="button" value="2" /><input type="button" value="3" /><br/>
<input type="button" value="*" /><input type="button" value="0" /><input type="button" value="#" /><input type="button" name="{enter}" value="&crarr;"/></div>
<div class="test"><input type="text" /><input type="button" value="test"/></div>
<div class="wrap"><select><option>em</option><option>strong</option><option>del</option></select><input type="button" value="Wrap Selection"/></div>
<p>The test button will send the entered string directly. The wrap button will wrap the current selection with the given tag and leave the insertion point after the selection (uses <code>&lt;tag&gt;{selection}{mark}&lt;/tag&gt;</code>).</p>
<div id="keypress">keypress event.which:</div>
<div id="sendkeys">sendkeys event.which:</div>
</div>

22 Comments

  1. Brad Friedman says:

    I think the flakiness you’re experiencing with {enter} might be due to the difference in ASCII between a newline (ASCII code 13) and a carriage return (ASCII code 10). If you replace {enter} with ‘\r\n’ instead of just ‘\n,’ I think you’ll experience more consistent behavior across browsers.

    Great work here!

  2. Danny says:

    @Brad
    That may be part of it (when I get a change I’ll try it), but I think the issue is the inconsistency on how browsers handle returns in contenteditable, whether to split the <div< or to insert a <br>, and then how they handle editing across separate HTML elements (there’s no newline character inserted).
    –Danny

  3. Mike says:

    Hi, many thanks for doing this, very useful. A problem I’ve noticed is that if you use your keypad to enter some value and then change the caret position using your real keyboard’s left or right keys, it no longer works properly (in IE). Is this a known issue and/or any suggestion of how to workaround it ?

  4. Danny says:

    @Mike:
    I’m noticing that as I play with it now. I’m not sure what it’s doing. I’ll try to get a chance to look at it.
    Danny

  5. John McLear says:

    SelectAll doesn’t seem to emulate the behavior of Control A on Windows (Chrome). I’m testing this on Etherpad Lite.

    Any idea?

  6. Danny says:

    @John McLear
    It works for me in Chrome. The commands are case-sensitive; are you using {selectall}? Do you have a sample page with the error?
    –Danny

  7. John McLear says:

    Danny, if you are familiar w/ git doing a
    git clone git://github.com/Pita/etherpad-lite.git
    then
    checkout feature/frontend-tests
    then bin/run.sh
    Will bring our testing framework live on http://127.0.0.1:9001/tests/frontend/

    Simply uncomment the keystroke_urls_become_clickable.js in index.html and you can see the test working in real time. Visiting a pad on http://beta.etherpad.org and pressing control A shows the correct behavior for Select All.

  8. Danny says:

    @John McClear:
    I’m not going to have the time to analyze this. It looks like you’re using a content-editable <div> for your editor. You may have to look at my source code (which hopefully is reasonably understandable) to figure out what is going on.
    –Danny

  9. John McLear says:

    Alrighty, ta anyways, will keep you posted on any other bugs I discover.

  10. Danny says:

    @John:
    Based on your colleagues’ work, I’ve updated the code to work with iframes.
    –Danny

  11. mottusuchi says:

    sir,
    when i try to use nicedit (http://nicedit.com/) with this sendkeys it is not working? how can i sendkeys to nicedit…. plz help….
    mottusuchi

  12. John McLear says:

    Hey man any chance of support for ‘tab’, ‘uparrow’ and ‘downarrow’ please? :)

  13. John McLear says:

    Also how about moving this to github and support for ‘pageup’ & ‘pagedown’ ? :)

  14. Danny says:

    @John McLear:
    I’ve never quite gotten my head around git/github, though I realize I really need to. The code is MIT licensed, so feel free to fork your own version and put it on GitHub. As for other keys, the plugin only manipulates a single element, so sendkeys('\t') will insert a tab character. If you want to move the focus to the next element you’ll have to do that directly with element.focus().
    I’ve though about uparrow/downarrow etc., but that would involve somehow knowing about the layout of the text and I can’t see how to do that. How many characters are in a line? If you have any ideas, please let me know.
    –Danny

  15. Kevin says:

    It’s probably worth mentioning for anyone that finds this that Danny now has his code on github:

    https://github.com/dwachss/bililiteRange

  16. Danny says:

    @Kevin:
    Thanks for mentioning this; I guess my last comment a year ago is out of date.
    –Danny

  17. Hacking at 0300 : Cross-Browser Text Ranges and Selections says:

    […] Fixin’s for chili Improved sendkeys […]

  18. Hacking at 0300 : Preserving the Insertion Point on Blur says:

    […] watch it in action, see the sendkeys demo. Enter text in each box, then click or tab between them or click the radio buttons above them to […]

  19. Alex says:

    Thank you so much for this great plugin. Helped me to deal with some strange problems

  20. Tim Macfarlane says:

    Hi Danny, jquery.sendkeys has been extremely useful for testing browser apps, so thank you! I’ve created an NPM module for it, please get in touch if you want to own it.

    https://www.npmjs.com/package/jquery-sendkeys
    https://github.com/featurist/jquery-sendkeys

  21. Danny says:

    @Tim:
    Thanks. I don’t know much about node, but it looks like a good thing. Can you just fork the bililiteRange project (https://github.com/dwachss/bililiteRange) and submit a pull request?

    If not, it’s fine to leave it at this and I will have to look at npm myself.

    Of note, I am currently working on updating sendkeys and can hopefully make it “invisible”–have it intercept keydown events and just work, as:
    $(element).trigger({type: 'keydown', key: 'Enter'});

    I will update this post when I’m done.

    –Danny

Leave a Reply


Warning: Undefined variable $user_ID in /home/public/blog/wp-content/themes/evanescence/comments.php on line 75