blob: cdb968681b18601ca32f48a69333c1ae19c42247 [file] [log] [blame]
/*************************************************
* Sane Keyboard Events Shim
*
* An abstraction layer wrapping the textarea in
* an object with methods to manipulate and listen
* to events on, that hides all the nasty cross-
* browser incompatibilities behind a uniform API.
*
* Design goal: This is a *HARD* internal
* abstraction barrier. Cross-browser
* inconsistencies are not allowed to leak through
* and be dealt with by event handlers. All future
* cross-browser issues that arise must be dealt
* with here, and if necessary, the API updated.
*
* Organization:
* - key values map and stringify()
* - saneKeyboardEvents()
* + defer() and flush()
* + event handler logic
* + attach event handlers and export methods
************************************************/
var saneKeyboardEvents = (function() {
// The following [key values][1] map was compiled from the
// [DOM3 Events appendix section on key codes][2] and
// [a widely cited report on cross-browser tests of key codes][3],
// except for 10: 'Enter', which I've empirically observed in Safari on iOS
// and doesn't appear to conflict with any other known key codes.
//
// [1]: http://www.w3.org/TR/2012/WD-DOM-Level-3-Events-20120614/#keys-keyvalues
// [2]: http://www.w3.org/TR/2012/WD-DOM-Level-3-Events-20120614/#fixed-virtual-key-codes
// [3]: http://unixpapa.com/js/key.html
var KEY_VALUES = {
8: 'Backspace',
9: 'Tab',
10: 'Enter', // for Safari on iOS
13: 'Enter',
16: 'Shift',
17: 'Control',
18: 'Alt',
20: 'CapsLock',
27: 'Esc',
32: 'Spacebar',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'Left',
38: 'Up',
39: 'Right',
40: 'Down',
45: 'Insert',
46: 'Del',
144: 'NumLock'
};
// To the extent possible, create a normalized string representation
// of the key combo (i.e., key code and modifier keys).
function stringify(evt) {
var which = evt.which || evt.keyCode;
var keyVal = KEY_VALUES[which];
var key;
var modifiers = [];
if (evt.ctrlKey) modifiers.push('Ctrl');
if (evt.originalEvent && evt.originalEvent.metaKey) modifiers.push('Meta');
if (evt.altKey) modifiers.push('Alt');
if (evt.shiftKey) modifiers.push('Shift');
key = keyVal || String.fromCharCode(which);
if (!modifiers.length && !keyVal) return key;
modifiers.push(key);
return modifiers.join('-');
}
// create a keyboard events shim that calls callbacks at useful times
// and exports useful public methods
return function saneKeyboardEvents(el, handlers) {
var keydown = null;
var keypress = null;
var textarea = jQuery(el);
var target = jQuery(handlers.container || textarea);
// checkTextareaFor() is called after keypress or paste events to
// say "Hey, I think something was just typed" or "pasted" (resp.),
// so that at all subsequent opportune times (next event or timeout),
// will check for expected typed or pasted text.
// Need to check repeatedly because #135: in Safari 5.1 (at least),
// after selecting something and then typing, the textarea is
// incorrectly reported as selected during the input event (but not
// subsequently).
var checkTextarea = noop, timeoutId;
function checkTextareaFor(checker) {
checkTextarea = checker;
clearTimeout(timeoutId);
timeoutId = setTimeout(checker);
}
target.bind('keydown keypress input keyup focusout paste', function(e) { checkTextarea(e); });
// -*- public methods -*- //
function select(text) {
// check textarea at least once/one last time before munging (so
// no race condition if selection happens after keypress/paste but
// before checkTextarea), then never again ('cos it's been munged)
checkTextarea();
checkTextarea = noop;
clearTimeout(timeoutId);
textarea.val(text);
if (text && textarea[0].select) textarea[0].select();
shouldBeSelected = !!text;
}
var shouldBeSelected = false;
// -*- helper subroutines -*- //
// Determine whether there's a selection in the textarea.
// This will always return false in IE < 9, which don't support
// HTMLTextareaElement::selection{Start,End}.
function hasSelection() {
var dom = textarea[0];
if (!('selectionStart' in dom)) return false;
return dom.selectionStart !== dom.selectionEnd;
}
function handleKey() {
handlers.keystroke(stringify(keydown), keydown);
}
// -*- event handlers -*- //
function onKeydown(e) {
keydown = e;
keypress = null;
if (shouldBeSelected) checkTextareaFor(function(e) {
if (!(e && e.type === 'focusout') && textarea[0].select) {
textarea[0].select(); // re-select textarea in case it's an unrecognized
}
checkTextarea = noop; // key that clears the selection, then never
clearTimeout(timeoutId); // again, 'cos next thing might be blur
});
handleKey();
}
function onKeypress(e) {
// call the key handler for repeated keypresses.
// This excludes keypresses that happen directly
// after keydown. In that case, there will be
// no previous keypress, so we skip it here
if (keydown && keypress) handleKey();
keypress = e;
checkTextareaFor(typedText);
}
function typedText() {
// If there is a selection, the contents of the textarea couldn't
// possibly have just been typed in.
// This happens in browsers like Firefox and Opera that fire
// keypress for keystrokes that are not text entry and leave the
// selection in the textarea alone, such as Ctrl-C.
// Note: we assume that browsers that don't support hasSelection()
// also never fire keypress on keystrokes that are not text entry.
// This seems reasonably safe because:
// - all modern browsers including IE 9+ support hasSelection(),
// making it extremely unlikely any browser besides IE < 9 won't
// - as far as we know IE < 9 never fires keypress on keystrokes
// that aren't text entry, which is only as reliable as our
// tests are comprehensive, but the IE < 9 way to do
// hasSelection() is poorly documented and is also only as
// reliable as our tests are comprehensive
// If anything like #40 or #71 is reported in IE < 9, see
// b1318e5349160b665003e36d4eedd64101ceacd8
if (hasSelection()) return;
var text = textarea.val();
if (text.length === 1) {
textarea.val('');
handlers.typedText(text);
} // in Firefox, keys that don't type text, just clear seln, fire keypress
// https://github.com/mathquill/mathquill/issues/293#issuecomment-40997668
else if (text && textarea[0].select) textarea[0].select(); // re-select if that's why we're here
}
function onBlur() { keydown = keypress = null; }
function onPaste(e) {
// browsers are dumb.
//
// In Linux, middle-click pasting causes onPaste to be called,
// when the textarea is not necessarily focused. We focus it
// here to ensure that the pasted text actually ends up in the
// textarea.
//
// It's pretty nifty that by changing focus in this handler,
// we can change the target of the default action. (This works
// on keydown too, FWIW).
//
// And by nifty, we mean dumb (but useful sometimes).
textarea.focus();
checkTextareaFor(pastedText);
}
function pastedText() {
var text = textarea.val();
textarea.val('');
if (text) handlers.paste(text);
}
// -*- attach event handlers -*- //
target.bind({
keydown: onKeydown,
keypress: onKeypress,
focusout: onBlur,
paste: onPaste
});
// -*- export public methods -*- //
return {
select: select
};
};
}());