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