| /************************************************* | |
| * 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 | |
| }; | |
| }; | |
| }()); |