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.
by Han, Jeanine, and Mary (maintainers@mathquill.com)
Good news! We‘ve resumed active development and we’re committed to getting things running smoothly.
Find a dusty corner? Let us know!
Just load MathQuill and call our constructors on some HTML element DOM objects, for example:
<link rel="stylesheet" href="/path/to/mathquill.css"/> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> <script src="/path/to/mathquill.js"></script> <p> Solve <span id="problem">ax^2 + bx + c = 0</span>: <span id="answer">x=</span> </p> <script> var MQ = MathQuill.getInterface(2); MQ.StaticMath($('#problem')[0]); var answer = MQ.MathField($('#answer')[0], { handlers: { edit: function() { checkAnswer(answer.latex()); } } }); </script>
To load MathQuill,
mathquill.js (Google CDN-hosted copy recommended)font/ directory relative to mathquill.css (unless you'd rather change where your copy of mathquill.css includes them from), which is already the case if you just:To use the MathQuill API, first get the latest version of the interface:
var MQ = MathQuill.getInterface(2);
Now you can call MQ.StaticMath() or MQ.MathField(), which MathQuill-ify an HTML element and return an API object. If the element had already been MathQuill-ified into the same kind, return that kind of API object (if different kind or not an HTML element, null). Note that it always returns either an instance of itself, or null.
var staticMath = MQ.StaticMath(staticMathSpan); mathField instanceof MQ.StaticMath // => true mathField instanceof MQ // => true mathField instanceof MathQuill // => true var mathField = MQ.MathField(mathFieldSpan); mathField instanceof MQ.MathField // => true mathField instanceof MQ.EditableField // => true mathField instanceof MQ // => true mathField instanceof MathQuill // => true
MQ itself is a function that takes an HTML element and, if it's the root HTML element of a static math or math field, returns an API object for it (if not, null):
MQ(mathFieldSpan) instanceof MQ.MathField // => true MQ(otherSpan) // => null
API objects for the same MathQuill instance have the same .id, which will always be a unique truthy primitive value that can be used as an object key (like an ad hoc Map or Set):
MQ(mathFieldSpan).id === mathField.id // => true var setOfMathFields = {}; setOfMathFields[mathField.id] = mathField; MQ(mathFieldSpan).id in setOfMathFields // => true staticMath.id in setOfMathFields // => false
Similarly, API objects for the same MathQuill instance share a .data object (which can be used like an ad hoc WeakMap or WeakSet):
MQ(mathFieldSpan).data === mathField.data // => true mathField.data.foo = 'bar'; MQ(mathFieldSpan).data.foo // => 'bar'
Any element that has been MathQuill-ified can be reverted:
<span id="revert-me" class="mathquill-static-math"> some <code>HTML</code> </span>
MQ($('#revert-me')[0]).revert().html(); // => 'some <code>HTML</code>'
MathQuill uses computed dimensions, so if they change (because an element was mathquill-ified before it was in the visible HTML DOM, or the font size changed), then you'll need to tell MathQuill to recompute:
var mathFieldSpan = $('<span>\\sqrt{2}</span>'); var mathField = MQ.MathField(mathFieldSpan[0]); mathFieldSpan.appendTo(document.body); mathField.reflow();
MathQuill API objects further expose the following public methods:
.el() returns the root HTML element.html() returns the contents as static HTML.latex() returns the contents as LaTeX.latex('a_n x^n') will render the argument as LaTeXAdditionally, descendants of MQ.EditableField (currently only MQ.MathField) expose:
.focus(), .blur() focuses or defocuses the editable field
.write(' - 1') will write some LaTeX at the current cursor position
.cmd('\\sqrt') will enter a LaTeX command at the current cursor position or with the current selection
.select() selects the contents (just like on textareas and on inputs)
.clearSelection() clears the current selection
.moveTo{Left,Right,Dir}End() move the cursor to the left/right end of the editable field, respectively. (The first two are implemented in terms of .moveToDirEnd(dir) where dir is one of MQ.L or MQ.R, constants that obey the contract that MQ.L === -MQ.R and vice versa.)
.keystroke(keys) simulates keystrokes given a string like "Ctrl-Home Del", a whitespace-delimited list of key values with optional prefixes
.typedText(text) simulates typing text, one character at a time
ᴇxᴘᴇʀɪᴍᴇɴᴛᴀʟ .dropEmbedded(pageX, pageY, options) insert a custom embedded element at the given coordinates, where options is an object like:
{ htmlString: '<span class="custom-embed"></span>', text: function() { return 'custom_embed'; }, latex: function() { return '\customEmbed'; } }
ᴇxᴘᴇʀɪᴍᴇɴᴛᴀʟ .registerEmbed('name', function(id){return options}) allows MathQuill to parse custom embedded objects from latex, where options is an object like the one defined above in .dropEmbedded. This will parse the following latex into the embedded object you defined: \embed{name}[id]}
MathQuill overwrites the global MathQuill variable when loaded. You can undo that with .noConflict() (similar to [jQuery.noConflict()] (http://api.jquery.com/jQuery.noConflict)):
<script src="/path/to/first-mathquill.js"></script> <script src="/path/to/second-mathquill.js"></script> <script> var secondMQ = MathQuill.noConflict().getInterface(2); secondMQ.MathField(...); var firstMQ = MathQuill.getInterface(2); firstMQ.MathField(...); </script>
(Warning: This lets different copies of MathQuill each power their own math fields, but using different copies on the same DOM element won't work. Anyway, .noConflict() is primarily to help you reduce globals.)
MQ.MathField() can also take an options object:
var el = $('<span>x^2</span>').appendTo('body'); var mathField = MQ.MathField(el[0], { spaceBehavesLikeTab: true, leftRightIntoCmdGoes: 'up', restrictMismatchedBrackets: true, sumStartsWithNEquals: true, supSubsRequireOperand: true, charsThatBreakOutOfSupSub: '+-=<>', autoSubscriptNumerals: true, autoCommands: 'pi theta sqrt sum', autoOperatorNames: 'sin cos etc', substituteTextarea: function() { return document.createElement('textarea'); }, handlers: { edit: function(mathField) { ... }, upOutOf: function(mathField) { ... }, moveOutOf: function(dir, mathField) { if (dir === MQ.L) ... else ... } } });
To change mathField's options, the .config({ ... }) method takes an options object in the same format.
Global defaults for a page may be set with MQ.config({ ... }).
If spaceBehavesLikeTab is true the keystrokes {Shift-,}Spacebar will behave like {Shift-,}Tab escaping from the current block (as opposed to the default behavior of inserting a Space character).
By default, the Left and Right keys move the cursor through all possible cursor positions in a particular order: right into a fraction puts the cursor at the left end of the numerator, right out of the numerator puts the cursor at the left end of the denominator, right out of the denominator puts the cursor to the right of the fraction; symmetrically, left into a fraction puts the cursor at the right end of the denominator, etc. Note that right out of the numerator to the left end of the denominator is actually leftwards (and downwards, it‘s basically wrapped). If instead you want right to always go right, and left to always go left, you can set leftRightIntoCmdGoes to 'up' or 'down' so that left and right go up or down (respectively) into commands, e.g. 'up' means that left into a fraction goes up into the numerator, skipping the denominator; symmetrically, right out of the numerator skips the denominator and puts the cursor to the right of the fraction, which unlike the default behavior is actually rightwards (the drawback is the denominator is always skipped, you can’t get to it with just Left and Right, you have to press Down); which is the same behavior as the Desmos calculator. 'down' instead means it is the numerator that is always skipped, which is the same behavior as the Mac OS X built-in app Grapher.
If restrictMismatchedBrackets is true then you can type [a,b) and [a,b), but if you try typing [x} or \langle x|, you‘ll get [{x}] or \langle|x|\rangle instead. This lets you type (|x|+1) normally; otherwise, you’d get \left( \right| x \left| + 1 \right).
If sumStartsWithNEquals is true then when you type \sum, \prod, or \coprod, the lower limit starts out with n=, e.g. you get the LaTeX \sum_{n=}^{ }, rather than empty by default.
supSubsRequireOperand disables typing of superscripts and subscripts when there's nothing to the left of the cursor to be exponentiated or subscripted. Averts the especially confusing typo x^^2, which looks much like x^2.
charsThatBreakOutOfSupSub sets the chars that when typed, “break out” of superscripts and subscripts: for example, typing x^2n+y normally results in the LaTeX x^{2n+y}, you have to hit Down or Tab (or Space if spaceBehavesLikeTab is true) to move the cursor out of the exponent and get the LaTeX x^{2n}+y; this option makes + “break out” of the exponent and type what you expect. Problem is, now you can‘t just type x^n+m to get the LaTeX x^{n+m}, you have to type x^(n+m and delete the paren or something. (Doesn’t apply to the first character in a superscript or subscript, so typing x^-6 still results in x^{-6}.)
autoCommands, a space-delimited list of LaTeX control words (no backslash, letters only, min length 2), defines the (default empty) set of “auto-commands”, commands automatically rendered by just typing the letters without typing a backslash first.
autoOperatorNames, a list of the same form (space-delimited letters-only each length>=2), and overrides the set of operator names that automatically become non-italicized when typing the letters without typing a backslash first, like sin, log, etc. Defaults to the LaTeX built-in operator names (Section 3.17 of the Short Math Guide) plus some missing trig operators like sech, arcsec, arsinh, etc.
substituteTextarea, a function that creates a focusable DOM element, called when setting up a math field. It defaults to <textarea autocorrect=off .../>, but for example, Desmos substitutes <span tabindex=0></span> on iOS to suppress the built-in virtual keyboard in favor of a custom math keypad that calls the MathQuill API. Unfortunately there‘s no universal check for a virtual keyboard, you can’t even detect a touchscreen (notably Modernizr gave up) and even if you could, Windows 8 and ChromeOS devices have both physical keyboards and touchscreens and you can connect physical keyboards to iOS and Android devices with Bluetooth, so touchscreen != virtual keyboard. Desmos currently sniffs the user agent for iOS, so Bluetooth keyboards just don't work in Desmos on iOS, the tradeoffs are up to you.
Supported handlers:
moveOutOf, deleteOutOf, and selectOutOf are called with dir and the math field API object as argumentsupOutOf, downOutOf, enter, and edit are called with just the API object as the argumentThe *OutOf handlers are called when Left/Right/Up/Down/Backspace/Del/ Shift-Left/Shift-Right is pressed but the cursor is at the left/right/top/bottom edge and so nothing happens within the math field. For example, when the cursor is at the left edge, pressing the Left key causes the moveOutOf handler (if provided) to be called with MQ.L and the math field API object as arguments, and Backspace causes deleteOutOf (if provided) to be called with MQ.L and the API object as arguments, etc.
The enter handler is called whenever Enter is pressed.
The edit handler is called when the contents of the field might have been changed by stuff being typed, or deleted, or written with the API, etc. (Deprecated aliases: edited, reflow.)
Handlers are always called directly on the handlers object passed in, preserving the this value, so you can do stuff like:
var MathList = P(function(_) { _.init = function() { this.maths = []; this.el = ... }; _.add = function() { var math = MQ.MathField($('<span/>')[0], { handlers: this }); $(math.el()).appendTo(this.el); math.data.i = this.maths.length; this.maths.push(math); }; _.moveOutOf = function(dir, math) { var adjacentI = (dir === MQ.L ? math.data.i - 1 : math.data.i + 1); var adjacentMath = this.maths[adjacentI]; if (adjacentMath) adjacentMath.focus().moveToDirEnd(-dir); }; ... });
Of course you can always ignore the last argument, like when the handlers close over the math field:
var latex = ''; var mathField = MQ.MathField($('#mathfield')[0], { handlers: { edit: function() { latex = mathField.latex(); }, enter: function() { submitLatex(latex); } } });
A Note On Changing Colors:
To change the foreground color, don't just set the color, also set the border-color, because the cursor, fraction bar, and square root overline are all borders, not text. (Example below.)
Due to technical limitations of IE8, if you support it, and want to give a MathQuill editable a background color other than white, and support square roots, parentheses, square brackets, or curly braces, you will need to, in addition to of course setting the background color on the editable itself, set it on elements with class mq-matrixed, and then set a Chroma filter on elements with class mq-matrixed-container.
For example, to style as white-on-black instead of black-on-white:
#my-math-input {
color: white;
border-color: white;
background: black;
}
#my-math-input .mq-matrixed {
background: black;
}
#my-math-input .mq-matrixed-container {
filter: progid:DXImageTransform.Microsoft.Chroma(color='black');
}
(This is because almost all math rendered by MathQuill has a transparent background, so for them it‘s sufficient to set the background color on the editable itself. The exception is, IE8 doesn’t support CSS transforms, so MathQuill uses a matrix filter to stretch parens etc, which anti-aliases wrongly without an opaque background, so MathQuill defaults to white.)
To hack on MathQuill, you‘re gonna want to build and test the source files you edit. In addition to make, MathQuill uses some build tools written on Node, so you will need to install that before running make. (Once it’s installed, make automatically does npm install, installing the necessary build tools.)
make builds build/mathquill.{css,js,min.js}make dev won't try to minify MathQuill (which can be annoyingly slow)make test builds mathquill.test.js (used by test/unit.html) and also doesn't minifymake basic builds mathquill-basic.{js,min.js,css} and font/Symbola-basic.{eot,ttf}; serve and load them instead for a stripped- down version of MathQuill for basic mathematics, without advanced LaTeX commands. Specifically, it doesn‘t let you type LaTeX backslash commands with \ or text blocks with $, and also won’t render any LaTeX commands that can‘t by typed without \. The resulting JS is only somewhat smaller, but the font is like 100x smaller. (TODO: reduce full MathQuill’s font size.)All the CSS is in src/css. Most of it‘s pretty straightforward, the choice of font isn’t settled, and fractions are somewhat arcane, see the Wiki pages “Fonts” and “Fractions”.
All the JavaScript that you actually want to read is in src/, build/ is created by make to contain the same JS cat'ed and minified.
There‘s a lot of JavaScript but the big picture isn’t too complicated, there's 2 thin layers sandwiching 2 broad but modularized layers:
More specifically:
(In comments and internal documentation, :: means .prototype..)
tree.js defines base classes of objects relating to the tree.cursor.js defines objects representing the cursor and a selection of math or text, with associated HTML elements.Fraction, SquareRoot, or VanillaSymbol.\frac or \ . Unlike loose usage in the LaTeX community, where \ne and \neq (which print the same symbol, ≠) might or might not be considered the same command, in the context of MathQuill they are considered different “control sequences” for the same “command”..moveTowards on the node just left of the cursor, dispatching on what kind of command the node is (Fraction::moveTowards and SquareRoot::moveTowards can insert the cursor in different places).controller.js defines the base class for the controller, which each math field or static math instance has one of, and to which each service adds methods.publicapi.js defines the global MathQuill.getInterface() function, the MQ.MathField() etc. constructors, and the API objects returned by them. The constructors, and the API methods on the objects they return, call appropriate controller methods to initialize and manipulate math field and static math instances.Misc.:
intro.js defines some simple sugar for the idiomatic JS classes used throughout MathQuill, plus some globals and opening boilerplate.
Classes are defined using Pjs, and the variable _ is used by convention as the prototype.
services/*.util.js files are unimportant to the overall architecture, you can ignore them until you have to deal with code that is using them.
The Source Code Form of MathQuill is subject to the terms of the Mozilla Public License, v. 2.0: http://mozilla.org/MPL/2.0/
The quick-and-dirty is you can do whatever if modifications to MathQuill are in public GitHub forks. (Other ways to publicize modifications are also fine, as are private use modifications. See also: MPL 2.0 FAQ)