Merge pull request #622 from mathquill/feature.fastClick-rebase API to enable "fast clicks" on touch devices Argghhhhh. Want touch swipes to scroll the page, even if they start in MathQuill (so that it's possible to scroll an expression list filled to the brim with nothing but MathQuills), but touch taps to place the cursor. Now, with mouse events, if all you care about is a logical "click" on the current platform, you just listen for the click event and the browser will take care of whether a particular sequence of mousedown-mousemoves-mouseup is a click or not. With touch events, there's no equivalent logical "tap" event, and while legacy mouse events do fire if you don't .preventDefault() on touchstart, Safari on iOS waits a glacial ~360ms [1] after a tap in case you're actually doing the double-tap gesture to zoom in. [1]: Videos: http://developer.telerik.com/featured/300-ms-click-delay-ios-8/ It gets better. Not only are the legacy mouse events unhelpful, they're misleading, because there is no way to directly tell them apart from real mouse events and no way to get them not to fire except calling .preventDefault(), which breaks touch-swipe-to-scroll. As a result, across the Web there are dozens of blog posts about, and shitty JS libraries for, "fast clicks", that all disagree on what counts as a "tap", and also when to ignore a mouse event because it's probably a legacy one 300ms after a touch event that's already been dealt with. By far the 2 most-cited "fast click" solutions are Google Developer's "Fast Buttons" article [2] and FT Labs' FastClick library [3]; only the latter ignores taps that are to stop scrolling after a fling, taps where touchend is >700ms after touchstart, or taps with more than one finger, but only the former appears to put a time and distance limit on mouse events to ignore (FastClick just ignores the next one). [2]: https://web.archive.org/web/20150406091222/https://developers.google.com/mobile/articles/fast_buttons [3]: https://github.com/ftlabs/fastclick (Google seems to have lost the original widely-cited "Fast Buttons" article: https://developers.google.com/mobile/articles/fast_buttons ) MathQuill's sponsor, Desmos, has a custom touch events library whose definition of "tap", and whose strategy for ignoring delayed mouse events, is different from either of the above. Clearly, this stuff needs to be decided by the parent application using MathQuill. So, with heart-wrenching despair, here are 2 new API calls: - .clickAt(clientX, clientY, target) takes in the touch event info and places the cursor like there was a click there (also useful for e.g. obviating .dropEmbedded()) + `target` is optional but optimizes-away one DOM call for free - .ignoreNextMousedown(fn) takes a function that is called on subsequent mousedown events, which MathQuill will ignore if true is returned. MathQuill stops calling the function once it returns false. You should call this whenever there might be a legacy mousedown, even if it doesn't "count" as a tap to you, else MathQuill will get clicked fast sometimes and slow others, that'd be weird. -- Why clientX/Y even though .seek() takes in pageX/Y? Well, document.elementFromPoint() *has* to take in clientX/Y, whereas .seek() merely *happens* to take in pageX/Y because it's slightly easier to work with jQuery::offset(), and that's likely to change because jQuery::offset() has poor performance so MathQuill may want to start working with .getBoundingClientRects() directly, which would make it actually easier to work with clientX/Y than pageX/Y in the first place. For now, though, I'm not gonna port everything over the whole .seek() system to clientX/Y yet, instead .clickAt() just converts clientX/Y to pageX/Y by way of window.pageXOffset and window.pageYOffset, which according to QuirksMode [4] is the difference between them and has very good support. (It's also used by jQuery to convert from clientX/Y to what jQuery::offset() returns [5].) [4]: http://www.quirksmode.org/mobile/tableViewport.html#t10 [5]: https://github.com/jquery/jquery/blob/1.12.3/src/offset.js#L113-L114 -- The test case has a trivial notion of logical "tap", which is if no touchmoves happen between the touchstart and touchend. Note that it calls .ignoreNextMousedown() after every touchend, or else sometimes if you wiggle your finger a little but not much, the logical "tap" won't happen but Safari on iOS will still fire a legacy mousedown event. Test case also gained -webkit-tap-highlight-color to to hide the gray tap highlight box.