Skip to content

Improved sendkeys

The $.fn.sendkeys Plugin

This is an updated version of my original $.fn.sendkeys post. Last updated 2014-03-13 for version 3. Note that this version makes sendkeys a bililiteRange plugin, with the jQuery plugin just a little wrapper for that.

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>

{ 16 } Comments

  1. Brad Friedman | August 5, 2011 at 12:00 pm | Permalink

    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 | August 5, 2011 at 12:10 pm | Permalink

    @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 | October 17, 2011 at 10:17 pm | Permalink

    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 | October 18, 2011 at 7:56 am | Permalink

    @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 | October 3, 2012 at 2:31 pm | Permalink

    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 | October 3, 2012 at 2:56 pm | Permalink

    @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 | October 3, 2012 at 3:00 pm | Permalink

    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 | October 3, 2012 at 3:04 pm | Permalink

    @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 | October 3, 2012 at 3:29 pm | Permalink

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

  10. Danny | October 5, 2012 at 9:28 am | Permalink

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

  11. mottusuchi | November 6, 2012 at 8:45 pm | Permalink

    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 | November 13, 2012 at 4:14 am | Permalink

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

  13. John McLear | November 13, 2012 at 4:23 am | Permalink

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

  14. Danny | November 13, 2012 at 6:22 am | Permalink

    @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 | November 18, 2013 at 9:46 am | Permalink

    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 | November 18, 2013 at 12:15 pm | Permalink

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

{ 3 } Trackbacks

  1. […] http://bililite.com/blog/2011/01/23/improved-sendkeys/ […]

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

  3. […] 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 […]

Post a Comment

Your email is never published nor shared. Required fields are marked *

Notify me of followup comments via e-mail. You can also subscribe without commenting.