Skip to content

Keyboard and Clipboard Events

Han Wei edited this page Jun 11, 2012 · 7 revisions

The following was last updated for v0.2, the current master branch of MathQuill. Much will (hopefully) change in v0.3.

The quick-and-dirty: fake keyboard events bubble in the math DOM, keypress remembers the last keydown event and does setTimeout to check textarea contents, the selection is selected in the textarea ready to be cut or copied

(This document assumes knowledge of at least the v0.2 Architecture Overview and Events sections.)

Thanks to browser inconsistencies, getting keyboard events to work for normal and special keys, for both a tap and holding down, is stupidly hard. We provide an abstraction that is cross-browser but doesn't use browser-sniffing to hide these inconsistencies from event handlers in the math tree that actually deal with math editing.

Getting clipboard actions (cut/copy/paste) to work in MathQuill seems like a simple matter of having the LaTeX of the currently selected math be selected in the textarea, but due to an obscure lag in Chrome, we have to throttle how often we set the textarea.

Clipboard Events

In some old version of Chrome there was major lag when setting the contents of a textarea. It seems to be fixed now and it's impossible to get old versions of Chrome to try to repro, so pending further confirmation that this bug can't bite us again, we might go back to the naive solution of constantly setting the contents of the textarea to be what we want.

This is a lag of milliseconds, so it's only a problem when we let it compound many, many times, and is solved by throttling how often we set the contents of the textarea.

  • When the selection in the math editable textbox changes,
    • we set a timeout, unless there is already such a timeout set,
    • for a function setTextareaSelection that will
      • set the contents of the textarea to the LaTeX of the currently selected math,
      • and select the contents of the textarea.

So, if a whole bunch of changes happen in one thread, only one timeout will be set, and when it runs, it will set the contents of the textarea to the end result.

But now there's the possibility of a race condition where the user edits and then cuts/copies/pastes before the timeout runs and changes the textarea appropriately, so in browsers that support them, we listen for the cut/copy/paste events. Like keydown/keypress, they run just before the event's action happens, but unlike keydown/keypress, you can't cancel the action. So on cut/copy, we setTextareaSelection().

On paste, we set the pasting flag (see Keyboard Events, below) and, on a timeout waiting for after the paste happens, we writeLatex() the contents of the textarea (with a hack special casing pasting into a RootTextBlock), clear the textarea and unset the pasting flag.

Keyboard Events

According to this excellent resource, the keyboard events browser inconsistencies affecting this project are as follows:

  • In most browsers,
    • it's only possible to reliably tell special keys (like arrow keys, backspace, tab etc) apart from text entry with the keydown event object
      • e.g. on keypress the keycodes for the left, up, right and down arrow keys are identical to that of %, &, ' and (, respectively
    • but it's only possible to reliably tell what character was typed on keypress
      • in fact even the jQuery-normalized event.which on keypress is unreliable, for example on the French keyboard the backslash \ (crucial to MathQuill) is typed with Ctrl+Alt+8, which for all a web app can tell is a browser or OS shortcut #11
      • we ended up waiting after keypress and seeing what text was entered into the textarea
  • Unfortunately,
    • some browsers, for special keys, only fire keydown
    • some browsers, when you hold down a key, only repeatedly fire keypress

We provide an abstraction that is cross-browser but doesn't use browser-sniffing to hide these inconsistencies from "higher-level" handlers on math DOM nodes that actually deal with text entry and backspaces and stuff.

Our cross-browser algorithm is to record keydown events and on keypress, if there's been no keydown since the last keypress of the same kind, replay the recorded one. That way special keys can be handled on keydown without worry that it doesn't fire repeatedly when you hold down special keys. Text entry is handled by checking the contents of the textarea after keypress, so there are separate higher-level math DOM event handlers for keydown and textInput.

  • The actual event handlers bound to the jQuery object (and hence, the HTML DOM element) of the root math element share a closured lastKeydn object.
    • on keydown,
      • store the event object and set the flag before triggering keydown in the math DOM
    • on keypress,
      • check the flag
        • if there's been no keydown since the last keypress,
          • It's possible for a keypress to fire without a keydown firing first if the textarea is given focus during the keydown of a different focusable element, i.e. by that element's keydown event handler, so check if this keypress is the same kind as the last
            • if not, ignore this keypress
            • if so, trigger keydown in the math DOM
      • in case we're in the race condition where the user just started editing the math and the timeout of setTextareaSelection() hasn't run yet, clear the timeout
      • now setTimeout(textInput) to wait after the keypress event has been handled by the browser, at which point
        • check the pasting flag: if set, that would mean a paste event fired but the timeout waiting til after hasn't run yet, in which case, the textarea will be nonempty but because it was pasted in, not because text has been entered in, so ignore (the timeout set in the paste event handler will paste in the text). See also 68d9d1a0
        • check if there's anything selected in the textarea: if there is, then it isn't possible that text was entered, for that would overwrite the selection, so ignore. This happens in browsers where pressing Ctrl+C triggers keypress, and the user had something selected that they intended to copy. We don't want to interpret that selection, which is the LaTeX of the currently selected math (see above), as text they entered. See also b1318e5.
          • This is kind of a lie. I actually only check if there's anything selected in non-IE <9, since, as far as I've researched, IE <9 doesn't fire keypress at unwelcome times. If anything like #40 or #71 is reported in IE <9, revert af4c280
        • see if text has been entered into the textarea, and if so,
          • trigger textInput in the math DOM,
          • set textareaSelectionTimeout to undefined: we've cleared the setTextareaSelection timeout, so it will never run, but future changes to the selection will check textareaSelectionTimeout to throttle calls and if it isn't undefined, those checks will result in the mistaken belief that there's a setTextareaSelection timeout that will run soon
        • else if not,
          • check if textareaSelectionTimeout is undefined, and if so, the timeout must've been cleared in the keypress handler, and no text was typed overwriting the selection, so now setTextareaSelection()
  • The .keydown() and .textInput() event handlers inherited by all MathElements from MathElement.prototype just tail call the parent's keydown/textInput handler (Note: In the new architecture, instead of explicitly tail calling the parent's event handler, events are bubbled by the event triggerer and to cancel bubbling, the event handler explicitly returns false)
    • The root math block overrides its keydown and keypress event handlers to call the cursor's various .write() and .backspace() etc methods
    • Since the event bubbles up starting from the cursor's parent, MathElements like LatexCommandInput can override their keydown and textInput event handlers to do stuff when the cursor is in them and the user types something
      • Such MathElement key event handlers that don't want to cancel the bubbling of the event are required to explicitly fake the bubbling by calling the key event handlers of their parent MathElement
        • They can cancel the bubbling of the event by simply not calling their parent's event handlers
      • We want the same events to bubble out of the math textboxes as would bubble out of a normal textarea, so we never return false or stop propagation on any of the textarea events, but we often want to prevent the default action, so the .keydown() event handler can return false to prevent default. Like normal event handlers, returning other falsy values does not work, it has to === false

Tested (and confirmed working) on Mac for Safari 4, 5, Opera 10.5, Chrome 5+, Camino 2, Firefox 3.6, 4, even OmniWeb 5.

Tested (and confirmed working) on Windows for IE8.