/** * MathQuill: http://mathquill.com * by Jay and Han (laughinghan@gmail.com) * * This Source Code Form is subject to the terms of the * Mozilla Public License, v. 2.0. If a copy of the MPL * was not distributed with this file, You can obtain * one at http://mozilla.org/MPL/2.0/. */ (function() { var jQuery = window.jQuery, undefined, mqCmdId = 'mathquill-command-id', mqBlockId = 'mathquill-block-id', min = Math.min, max = Math.max; function noop() {} /** * A utility higher-order function that makes defining variadic * functions more convenient by letting you essentially define functions * with the last argument as a splat, i.e. the last argument "gathers up" * remaining arguments to the function: * var doStuff = variadic(function(first, rest) { return rest; }); * doStuff(1, 2, 3); // => [2, 3] */ var __slice = [].slice; function variadic(fn) { var numFixedArgs = fn.length - 1; return function() { var args = __slice.call(arguments, 0, numFixedArgs); var varArg = __slice.call(arguments, numFixedArgs); return fn.apply(this, args.concat([ varArg ])); }; } /** * A utility higher-order function that makes combining object-oriented * programming and functional programming techniques more convenient: * given a method name and any number of arguments to be bound, returns * a function that calls it's first argument's method of that name (if * it exists) with the bound arguments and any additional arguments that * are passed: * var sendMethod = send('method', 1, 2); * var obj = { method: function() { return Array.apply(this, arguments); } }; * sendMethod(obj, 3, 4); // => [1, 2, 3, 4] * // or more specifically, * var obj2 = { method: function(one, two, three) { return one*two + three; } }; * sendMethod(obj2, 3); // => 5 * sendMethod(obj2, 4); // => 6 */ var send = variadic(function(method, args) { return variadic(function(obj, moreArgs) { if (method in obj) return obj[method].apply(obj, args.concat(moreArgs)); }); }); /** * A utility higher-order function that creates "implicit iterators" * from "generators": given a function that takes in a sole argument, * a "yield_" function, that calls "yield_" repeatedly with an object as * a sole argument (presumably objects being iterated over), returns * a function that calls it's first argument on each of those objects * (if the first argument is a function, it is called repeatedly with * each object as the first argument, otherwise it is stringified and * the method of that name is called on each object (if such a method * exists)), passing along all additional arguments: * var a = [ * { method: function(list) { list.push(1); } }, * { method: function(list) { list.push(2); } }, * { method: function(list) { list.push(3); } } * ]; * a.each = iterator(function(yield_) { * for (var i in this) yield_(this[i]); * }); * var list = []; * a.each('method', list); * list; // => [1, 2, 3] * // Note that the for-in loop will yield 'each', but 'each' maps to * // the function object created by iterator() which does not have a * // .method() method, so that just fails silently. */ function iterator(generator) { return variadic(function(fn, args) { if (typeof fn !== 'function') fn = send(fn); var yield_ = function(obj) { return fn.apply(obj, [ obj ].concat(args)); }; return generator.call(this, yield_); }); } /** * sugar to make defining lots of commands easier. * TODO: rethink this. */ function bind(cons /*, args... */) { var args = __slice.call(arguments, 1); return function() { return cons.apply(this, args); }; } /** * a development-only debug method. This definition and all * calls to `pray` will be stripped from the minified * build of mathquill. * * This function must be called by name to be removed * at compile time. Do not define another function * with the same name, and only call this function by * name. */ function pray(message, cond) { if (!cond) throw new Error('prayer failed: '+message); } var P = (function(prototype, ownProperty, undefined) { // helper functions that also help minification function isObject(o) { return typeof o === 'object'; } function isFunction(f) { return typeof f === 'function'; } // used to extend the prototypes of superclasses (which might not // have `.Bare`s) function SuperclassBare() {} return function P(_superclass /* = Object */, definition) { // handle the case where no superclass is given if (definition === undefined) { definition = _superclass; _superclass = Object; } // C is the class to be returned. // // It delegates to instantiating an instance of `Bare`, so that it // will always return a new instance regardless of the calling // context. // // TODO: the Chrome inspector shows all created objects as `C` // rather than `Object`. Setting the .name property seems to // have no effect. Is there a way to override this behavior? function C() { var self = new Bare; if (isFunction(self.init)) self.init.apply(self, arguments); return self; } // C.Bare is a class with a noop constructor. Its prototype is the // same as C, so that instances of C.Bare are also instances of C. // New objects can be allocated without initialization by calling // `new MyClass.Bare`. function Bare() {} C.Bare = Bare; // Set up the prototype of the new class. var _super = SuperclassBare[prototype] = _superclass[prototype]; var proto = Bare[prototype] = C[prototype] = C.p = new SuperclassBare; // other variables, as a minifier optimization var extensions; // set the constructor property on the prototype, for convenience proto.constructor = C; C.mixin = function(def) { Bare[prototype] = C[prototype] = P(C, def)[prototype]; return C; } return (C.open = function(def) { extensions = {}; if (isFunction(def)) { // call the defining function with all the arguments you need // extensions captures the return value. extensions = def.call(C, proto, _super, C, _superclass); } else if (isObject(def)) { // if you passed an object instead, we'll take it extensions = def; } // ...and extend it if (isObject(extensions)) { for (var ext in extensions) { if (ownProperty.call(extensions, ext)) { proto[ext] = extensions[ext]; } } } // if there's no init, we assume we're inheriting a non-pjs class, so // we default to applying the superclass's constructor. if (!isFunction(proto.init)) { proto.init = _superclass; } return C; })(definition); } // as a minifier optimization, we've closured in a few helper functions // and the string 'prototype' (C[p] is much shorter than C.prototype) })('prototype', ({}).hasOwnProperty); /************************************************* * Base classes of edit tree-related objects * * Only doing tree node manipulation via these * adopt/ disown methods guarantees well-formedness * of the tree. ************************************************/ // L = 'left' // R = 'right' // // the contract is that they can be used as object properties // and (-L) === R, and (-R) === L. var L = MathQuill.L = -1; var R = MathQuill.R = 1; function prayDirection(dir) { pray('a direction was passed', dir === L || dir === R); } /** * Tiny extension of jQuery adding directionalized DOM manipulation methods. * * Funny how Pjs v3 almost just works with `jQuery.fn.init`. * * jQuery features that don't work on $: * - jQuery.*, like jQuery.ajax, obviously (Pjs doesn't and shouldn't * copy constructor properties) * * - jQuery(function), the shortcut for `jQuery(document).ready(function)`, * because `jQuery.fn.init` is idiosyncratic and Pjs doing, essentially, * `jQuery.fn.init.apply(this, arguments)` isn't quite right, you need: * * _.init = function(s, c) { jQuery.fn.init.call(this, s, c, $(document)); }; * * if you actually give a shit (really, don't bother), * see https://github.com/jquery/jquery/blob/1.7.2/src/core.js#L889 * * - jQuery(selector), because jQuery translates that to * `jQuery(document).find(selector)`, but Pjs doesn't (should it?) let * you override the result of a constructor call * + note that because of the jQuery(document) shortcut-ness, there's also * the 3rd-argument-needs-to-be-`$(document)` thing above, but the fix * for that (as can be seen above) is really easy. This problem requires * a way more intrusive fix * * And that's it! Everything else just magically works because jQuery internally * uses `this.constructor()` everywhere (hence calling `$`), but never ever does * `this.constructor.find` or anything like that, always doing `jQuery.find`. */ var $ = P(jQuery, function(_) { _.insDirOf = function(dir, el) { return dir === L ? this.insertBefore(el.first()) : this.insertAfter(el.last()); }; _.insAtDirEnd = function(dir, el) { return dir === L ? this.prependTo(el) : this.appendTo(el); }; }); var Point = P(function(_) { _.parent = 0; _[L] = 0; _[R] = 0; _.init = function(parent, leftward, rightward) { this.parent = parent; this[L] = leftward; this[R] = rightward; }; this.copy = function(pt) { return Point(pt.parent, pt[L], pt[R]); }; }); /** * MathQuill virtual-DOM tree-node abstract base class */ var Node = P(function(_) { _[L] = 0; _[R] = 0 _.parent = 0; var id = 0; function uniqueNodeId() { return id += 1; } this.byId = {}; _.init = function() { this.id = uniqueNodeId(); Node.byId[this.id] = this; this.ends = {}; this.ends[L] = 0; this.ends[R] = 0; }; _.dispose = function() { delete Node.byId[this.id]; }; _.toString = function() { return '{{ MathQuill Node #'+this.id+' }}'; }; _.jQ = $(); _.jQadd = function(jQ) { return this.jQ = this.jQ.add(jQ); }; _.jQize = function(jQ) { // jQuery-ifies this.html() and links up the .jQ of all corresponding Nodes var jQ = $(jQ || this.html()); function jQadd(el) { if (el.getAttribute) { var cmdId = el.getAttribute('mathquill-command-id'); var blockId = el.getAttribute('mathquill-block-id'); if (cmdId) Node.byId[cmdId].jQadd(el); if (blockId) Node.byId[blockId].jQadd(el); } for (el = el.firstChild; el; el = el.nextSibling) { jQadd(el); } } for (var i = 0; i < jQ.length; i += 1) jQadd(jQ[i]); return jQ; }; _.createDir = function(dir, cursor) { prayDirection(dir); var node = this; node.jQize(); node.jQ.insDirOf(dir, cursor.jQ); cursor[dir] = node.adopt(cursor.parent, cursor[L], cursor[R]); return node; }; _.createLeftOf = function(el) { return this.createDir(L, el); }; _.selectChildren = function(leftEnd, rightEnd) { return Selection(leftEnd, rightEnd); }; _.bubble = iterator(function(yield_) { for (var ancestor = this; ancestor; ancestor = ancestor.parent) { var result = yield_(ancestor); if (result === false) break; } return this; }); _.postOrder = iterator(function(yield_) { (function recurse(descendant) { descendant.eachChild(recurse); yield_(descendant); })(this); return this; }); _.isEmpty = function() { return this.ends[L] === 0 && this.ends[R] === 0; }; _.children = function() { return Fragment(this.ends[L], this.ends[R]); }; _.eachChild = function() { var children = this.children(); children.each.apply(children, arguments); return this; }; _.foldChildren = function(fold, fn) { return this.children().fold(fold, fn); }; _.withDirAdopt = function(dir, parent, withDir, oppDir) { Fragment(this, this).withDirAdopt(dir, parent, withDir, oppDir); return this; }; _.adopt = function(parent, leftward, rightward) { Fragment(this, this).adopt(parent, leftward, rightward); return this; }; _.disown = function() { Fragment(this, this).disown(); return this; }; _.remove = function() { this.jQ.remove(); this.postOrder('dispose'); return this.disown(); }; }); function prayWellFormed(parent, leftward, rightward) { pray('a parent is always present', parent); pray('leftward is properly set up', (function() { // either it's empty and `rightward` is the left end child (possibly empty) if (!leftward) return parent.ends[L] === rightward; // or it's there and its [R] and .parent are properly set up return leftward[R] === rightward && leftward.parent === parent; })()); pray('rightward is properly set up', (function() { // either it's empty and `leftward` is the right end child (possibly empty) if (!rightward) return parent.ends[R] === leftward; // or it's there and its [L] and .parent are properly set up return rightward[L] === leftward && rightward.parent === parent; })()); } /** * An entity outside the virtual tree with one-way pointers (so it's only a * "view" of part of the tree, not an actual node/entity in the tree) that * delimits a doubly-linked list of sibling nodes. * It's like a fanfic love-child between HTML DOM DocumentFragment and the Range * classes: like DocumentFragment, its contents must be sibling nodes * (unlike Range, whose contents are arbitrary contiguous pieces of subtrees), * but like Range, it has only one-way pointers to its contents, its contents * have no reference to it and in fact may still be in the visible tree (unlike * DocumentFragment, whose contents must be detached from the visible tree * and have their 'parent' pointers set to the DocumentFragment). */ var Fragment = P(function(_) { _.init = function(withDir, oppDir, dir) { if (dir === undefined) dir = L; prayDirection(dir); pray('no half-empty fragments', !withDir === !oppDir); this.ends = {}; if (!withDir) return; pray('withDir is passed to Fragment', withDir instanceof Node); pray('oppDir is passed to Fragment', oppDir instanceof Node); pray('withDir and oppDir have the same parent', withDir.parent === oppDir.parent); this.ends[dir] = withDir; this.ends[-dir] = oppDir; this.jQ = this.fold(this.jQ, function(jQ, el) { return jQ.add(el.jQ); }); }; _.jQ = $(); // like Cursor::withDirInsertAt(dir, parent, withDir, oppDir) _.withDirAdopt = function(dir, parent, withDir, oppDir) { return (dir === L ? this.adopt(parent, withDir, oppDir) : this.adopt(parent, oppDir, withDir)); }; _.adopt = function(parent, leftward, rightward) { prayWellFormed(parent, leftward, rightward); var self = this; self.disowned = false; var leftEnd = self.ends[L]; if (!leftEnd) return this; var rightEnd = self.ends[R]; if (leftward) { // NB: this is handled in the ::each() block // leftward[R] = leftEnd } else { parent.ends[L] = leftEnd; } if (rightward) { rightward[L] = rightEnd; } else { parent.ends[R] = rightEnd; } self.ends[R][R] = rightward; self.each(function(el) { el[L] = leftward; el.parent = parent; if (leftward) leftward[R] = el; leftward = el; }); return self; }; _.disown = function() { var self = this; var leftEnd = self.ends[L]; // guard for empty and already-disowned fragments if (!leftEnd || self.disowned) return self; self.disowned = true; var rightEnd = self.ends[R] var parent = leftEnd.parent; prayWellFormed(parent, leftEnd[L], leftEnd); prayWellFormed(parent, rightEnd, rightEnd[R]); if (leftEnd[L]) { leftEnd[L][R] = rightEnd[R]; } else { parent.ends[L] = rightEnd[R]; } if (rightEnd[R]) { rightEnd[R][L] = leftEnd[L]; } else { parent.ends[R] = leftEnd[L]; } return self; }; _.remove = function() { this.jQ.remove(); this.each('postOrder', 'dispose'); return this.disown(); }; _.each = iterator(function(yield_) { var self = this; var el = self.ends[L]; if (!el) return self; for (; el !== self.ends[R][R]; el = el[R]) { var result = yield_(el); if (result === false) break; } return self; }); _.fold = function(fold, fn) { this.each(function(el) { fold = fn.call(this, fold, el); }); return fold; }; }); /** * Registry of LaTeX commands and commands created when typing * a single character. * * (Commands are all subclasses of Node.) */ var LatexCmds = {}, CharCmds = {}; /******************************************** * Cursor and Selection "singleton" classes *******************************************/ /* The main thing that manipulates the Math DOM. Makes sure to manipulate the HTML DOM to match. */ /* Sort of singletons, since there should only be one per editable math textbox, but any one HTML document can contain many such textboxes, so any one JS environment could actually contain many instances. */ //A fake cursor in the fake textbox that the math is rendered in. var Cursor = P(Point, function(_) { _.init = function(initParent, options) { this.parent = initParent; this.options = options; var jQ = this.jQ = this._jQ = $(''); //closured for setInterval this.blink = function(){ jQ.toggleClass('mq-blink'); }; this.upDownCache = {}; }; _.show = function() { this.jQ = this._jQ.removeClass('mq-blink'); if ('intervalId' in this) //already was shown, just restart interval clearInterval(this.intervalId); else { //was hidden and detached, insert this.jQ back into HTML DOM if (this[R]) { if (this.selection && this.selection.ends[L][L] === this[L]) this.jQ.insertBefore(this.selection.jQ); else this.jQ.insertBefore(this[R].jQ.first()); } else this.jQ.appendTo(this.parent.jQ); this.parent.focus(); } this.intervalId = setInterval(this.blink, 500); return this; }; _.hide = function() { if ('intervalId' in this) clearInterval(this.intervalId); delete this.intervalId; this.jQ.detach(); this.jQ = $(); return this; }; _.withDirInsertAt = function(dir, parent, withDir, oppDir) { if (parent !== this.parent && this.parent.blur) this.parent.blur(); this.parent = parent; this[dir] = withDir; this[-dir] = oppDir; }; _.insDirOf = function(dir, el) { prayDirection(dir); this.withDirInsertAt(dir, el.parent, el[dir], el); this.parent.jQ.addClass('mq-hasCursor'); this.jQ.insDirOf(dir, el.jQ); return this; }; _.insLeftOf = function(el) { return this.insDirOf(L, el); }; _.insRightOf = function(el) { return this.insDirOf(R, el); }; _.insAtDirEnd = function(dir, el) { prayDirection(dir); this.withDirInsertAt(dir, el, 0, el.ends[dir]); this.jQ.insAtDirEnd(dir, el.jQ); el.focus(); return this; }; _.insAtLeftEnd = function(el) { return this.insAtDirEnd(L, el); }; _.insAtRightEnd = function(el) { return this.insAtDirEnd(R, el); }; /** * jump up or down from one block Node to another: * - cache the current Point in the node we're jumping from * - check if there's a Point in it cached for the node we're jumping to * + if so put the cursor there, * + if not seek a position in the node that is horizontally closest to * the cursor's current position */ _.jumpUpDown = function(from, to) { var self = this; self.upDownCache[from.id] = Point.copy(self); var cached = self.upDownCache[to.id]; if (cached) { cached[R] ? self.insLeftOf(cached[R]) : self.insAtRightEnd(cached.parent); } else { var pageX = self.offset().left; to.seek(pageX, self); } }; _.offset = function() { //in Opera 11.62, .getBoundingClientRect() and hence jQuery::offset() //returns all 0's on inline elements with negative margin-right (like //the cursor) at the end of their parent, so temporarily remove the //negative margin-right when calling jQuery::offset() //Opera bug DSK-360043 //http://bugs.jquery.com/ticket/11523 //https://github.com/jquery/jquery/pull/717 var self = this, offset = self.jQ.removeClass('mq-cursor').offset(); self.jQ.addClass('mq-cursor'); return offset; } _.unwrapGramp = function() { var gramp = this.parent.parent; var greatgramp = gramp.parent; var rightward = gramp[R]; var cursor = this; var leftward = gramp[L]; gramp.disown().eachChild(function(uncle) { if (uncle.isEmpty()) return; uncle.children() .adopt(greatgramp, leftward, rightward) .each(function(cousin) { cousin.jQ.insertBefore(gramp.jQ.first()); }) ; leftward = uncle.ends[R]; }); if (!this[R]) { //then find something to be rightward to insLeftOf if (this[L]) this[R] = this[L][R]; else { while (!this[R]) { this.parent = this.parent[R]; if (this.parent) this[R] = this.parent.ends[L]; else { this[R] = gramp[R]; this.parent = greatgramp; break; } } } } if (this[R]) this.insLeftOf(this[R]); else this.insAtRightEnd(greatgramp); gramp.jQ.remove(); if (gramp[L].siblingDeleted) gramp[L].siblingDeleted(cursor.options, R); if (gramp[R].siblingDeleted) gramp[R].siblingDeleted(cursor.options, L); }; _.startSelection = function() { var anticursor = this.anticursor = Point.copy(this); var ancestors = anticursor.ancestors = {}; // a map from each ancestor of // the anticursor, to its child that is also an ancestor; in other words, // the anticursor's ancestor chain in reverse order for (var ancestor = anticursor; ancestor.parent; ancestor = ancestor.parent) { ancestors[ancestor.parent.id] = ancestor; } }; _.endSelection = function() { delete this.anticursor; }; _.select = function() { var anticursor = this.anticursor; if (this[L] === anticursor[L] && this.parent === anticursor.parent) return false; // Find the lowest common ancestor (`lca`), and the ancestor of the cursor // whose parent is the LCA (which'll be an end of the selection fragment). for (var ancestor = this; ancestor.parent; ancestor = ancestor.parent) { if (ancestor.parent.id in anticursor.ancestors) { var lca = ancestor.parent; break; } } pray('cursor and anticursor in the same tree', lca); // The cursor and the anticursor should be in the same tree, because the // mousemove handler attached to the document, unlike the one attached to // the root HTML DOM element, doesn't try to get the math tree node of the // mousemove target, and Cursor::seek() based solely on coordinates stays // within the tree of `this` cursor's root. // The other end of the selection fragment, the ancestor of the anticursor // whose parent is the LCA. var antiAncestor = anticursor.ancestors[lca.id]; // Now we have two either Nodes or Points, guaranteed to have a common // parent and guaranteed that if both are Points, they are not the same, // and we have to figure out which is the left end and which the right end // of the selection. var leftEnd, rightEnd, dir = R; // This is an extremely subtle algorithm. // As a special case, `ancestor` could be a Point and `antiAncestor` a Node // immediately to `ancestor`'s left. // In all other cases, // - both Nodes // - `ancestor` a Point and `antiAncestor` a Node // - `ancestor` a Node and `antiAncestor` a Point // `antiAncestor[R] === rightward[R]` for some `rightward` that is // `ancestor` or to its right, if and only if `antiAncestor` is to // the right of `ancestor`. if (ancestor[L] !== antiAncestor) { for (var rightward = ancestor; rightward; rightward = rightward[R]) { if (rightward[R] === antiAncestor[R]) { dir = L; leftEnd = ancestor; rightEnd = antiAncestor; break; } } } if (dir === R) { leftEnd = antiAncestor; rightEnd = ancestor; } // only want to select Nodes up to Points, can't select Points themselves if (leftEnd instanceof Point) leftEnd = leftEnd[R]; if (rightEnd instanceof Point) rightEnd = rightEnd[L]; this.hide().selection = lca.selectChildren(leftEnd, rightEnd); this.insDirOf(dir, this.selection.ends[dir]); this.selectionChanged(); return true; }; _.clearSelection = function() { if (this.selection) { this.selection.clear(); delete this.selection; this.selectionChanged(); } return this; }; _.deleteSelection = function() { if (!this.selection) return; this[L] = this.selection.ends[L][L]; this[R] = this.selection.ends[R][R]; this.selection.remove(); this.selectionChanged(); delete this.selection; }; _.replaceSelection = function() { var seln = this.selection; if (seln) { this[L] = seln.ends[L][L]; this[R] = seln.ends[R][R]; delete this.selection; } return seln; }; }); var Selection = P(Fragment, function(_, super_) { _.init = function() { super_.init.apply(this, arguments); this.jQ = this.jQ.wrapAll('').parent(); //can't do wrapAll(this.jQ = $(...)) because wrapAll will clone it }; _.adopt = function() { this.jQ.replaceWith(this.jQ = this.jQ.children()); return super_.adopt.apply(this, arguments); }; _.clear = function() { // using the browser's native .childNodes property so that we // don't discard text nodes. this.jQ.replaceWith(this.jQ[0].childNodes); return this; }; _.join = function(methodName) { return this.fold('', function(fold, child) { return fold + child[methodName](); }); }; }); /********************************************* * Controller for a MathQuill instance, * on which services are registered with * * Controller.open(function(_) { ... }); * ********************************************/ var Controller = P(function(_) { _.init = function(API, root, container) { this.API = API; this.root = root; this.container = container; API.__controller = root.controller = this; this.cursor = root.cursor = Cursor(root, API.__options); // TODO: stop depending on root.cursor, and rm it }; _.handle = function(name, dir) { var handlers = this.API.__options.handlers; if (handlers && handlers[name]) { if (dir === L || dir === R) handlers[name](dir, this.API); else handlers[name](this.API); } }; var notifyees = []; this.onNotify = function(f) { notifyees.push(f); }; _.notify = function() { for (var i = 0; i < notifyees.length; i += 1) { notifyees[i].apply(this.cursor, arguments); } return this; }; }); /********************************************************* * The publicly exposed MathQuill API. ********************************************************/ /** * Global function that takes an HTML element and, if it's the root HTML element * of a static math or math or text field, returns its API object (if not, null). * Identity of API object guaranteed if called multiple times, i.e.: * * var mathfield = MathQuill.MathField(mathFieldSpan); * assert(MathQuill(mathFieldSpan) === mathfield); * assert(MathQuill(mathFieldSpan) === MathQuill(mathFieldSpan)); * */ function MathQuill(el) { if (!el || !el.nodeType) return null; // check that `el` is a HTML element, using the // same technique as jQuery: https://github.com/jquery/jquery/blob/679536ee4b7a92ae64a5f58d90e9cc38c001e807/src/core/init.js#L92 var blockId = $(el).children('.mq-root-block').attr(mqBlockId); return blockId ? Node.byId[blockId].controller.API : null; }; MathQuill.noConflict = function() { window.MathQuill = origMathQuill; return MathQuill; }; var origMathQuill = window.MathQuill; window.MathQuill = MathQuill; /** * Returns function (to be publicly exported) that MathQuill-ifies an HTML * element and returns an API object. If the element had already been MathQuill- * ified into the same kind, return the original API object (if different kind * or not an HTML element, null). */ function APIFnFor(APIClass) { function APIFn(el, opts) { var mq = MathQuill(el); if (mq instanceof APIClass || !el || !el.nodeType) return mq; return APIClass($(el), opts); } APIFn.prototype = APIClass.prototype; return APIFn; } var Options = P(), optionProcessors = {}; MathQuill.__options = Options.p; var AbstractMathQuill = P(function(_) { _.init = function() { throw "wtf don't call me, I'm 'abstract'"; }; _.initRoot = function(root, el, opts) { this.__options = Options(); this.config(opts); var ctrlr = Controller(this, root, el); ctrlr.createTextarea(); var contents = el.contents().detach(); root.jQ = $('').attr(mqBlockId, root.id).appendTo(el); this.latex(contents.text()); this.revert = function() { return el.empty().unbind('.mathquill') .removeClass('mq-editable-field mq-math-mode mq-text-mode') .append(contents); }; }; _.config = MathQuill.config = function(opts) { for (var opt in opts) if (opts.hasOwnProperty(opt)) { var optVal = opts[opt], processor = optionProcessors[opt]; this.__options[opt] = (processor ? processor(optVal) : optVal); } return this; }; _.el = function() { return this.__controller.container[0]; }; _.text = function() { return this.__controller.exportText(this.__options); }; _.latex = function(latex) { if (arguments.length > 0) { this.__controller.renderLatexMath(latex); if (this.__controller.blurred) this.__controller.cursor.hide().parent.blur(); return this; } return this.__controller.exportLatex(); }; _.html = function() { return this.__controller.root.jQ.html() .replace(/ mathquill-(?:command|block)-id="?\d+"?/g, '') .replace(/.?<\/span>/i, '') .replace(/ mq-hasCursor|mq-hasCursor ?/, '') .replace(/ class=(""|(?= |>))/g, ''); }; _.reflow = function() { this.__controller.root.postOrder('reflow'); return this; }; }); MathQuill.prototype = AbstractMathQuill.prototype; MathQuill.StaticMath = APIFnFor(P(AbstractMathQuill, function(_, super_) { _.init = function(el) { this.initRoot(MathBlock(), el.addClass('mq-math-mode')); this.__controller.delegateMouseEvents(); this.__controller.staticMathTextareaEvents(); }; _.latex = function() { var returned = super_.latex.apply(this, arguments); if (arguments.length > 0) { this.__controller.root.postOrder('registerInnerField', this.innerFields = []); } return returned; }; })); var EditableField = MathQuill.EditableField = P(AbstractMathQuill, function(_) { _.initRootAndEvents = function(root, el, opts) { this.initRoot(root, el, opts); this.__controller.editable = true; this.__controller.delegateMouseEvents(); this.__controller.editablesTextareaEvents(); }; _.focus = function() { this.__controller.textarea.focus(); return this; }; _.blur = function() { this.__controller.textarea.blur(); return this; }; _.write = function(latex) { this.__controller.writeLatex(latex); if (this.__controller.blurred) this.__controller.cursor.hide().parent.blur(); return this; }; _.cmd = function(cmd) { var ctrlr = this.__controller.notify(), cursor = ctrlr.cursor.show(); if (/^\\[a-z]+$/i.test(cmd)) { cmd = cmd.slice(1); var klass = LatexCmds[cmd]; if (klass) { cmd = klass(cmd); if (cursor.selection) cmd.replaces(cursor.replaceSelection()); cmd.createLeftOf(cursor); } else /* TODO: API needs better error reporting */; } else cursor.parent.write(cursor, cmd, cursor.replaceSelection()); if (ctrlr.blurred) cursor.hide().parent.blur(); return this; }; _.select = function() { var ctrlr = this.__controller; ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); while (ctrlr.cursor[L]) ctrlr.selectLeft(); return this; }; _.clearSelection = function() { this.__controller.cursor.clearSelection(); return this; }; _.moveToDirEnd = function(dir) { this.__controller.notify('move').cursor.insAtDirEnd(dir, this.__controller.root); return this; }; _.moveToLeftEnd = function() { return this.moveToDirEnd(L); }; _.moveToRightEnd = function() { return this.moveToDirEnd(R); }; _.keystroke = function(keys) { var keys = keys.replace(/^\s+|\s+$/g, '').split(/\s+/); for (var i = 0; i < keys.length; i += 1) { this.__controller.keystroke(keys[i], { preventDefault: noop }); } return this; }; _.typedText = function(text) { for (var i = 0; i < text.length; i += 1) this.__controller.typedText(text.charAt(i)); return this; }; }); function RootBlockMixin(_) { var names = 'workingGroupChange moveOutOf deleteOutOf selectOutOf upOutOf downOutOf reflow'.split(' '); for (var i = 0; i < names.length; i += 1) (function(name) { _[name] = function(dir) { this.controller.handle(name, dir); }; }(names[i])); } var Parser = P(function(_, super_, Parser) { // The Parser object is a wrapper for a parser function. // Externally, you use one to parse a string by calling // var result = SomeParser.parse('Me Me Me! Parse Me!'); // You should never call the constructor, rather you should // construct your Parser from the base parsers and the // parser combinator methods. function parseError(stream, message) { if (stream) { stream = "'"+stream+"'"; } else { stream = 'EOF'; } throw 'Parse Error: '+message+' at '+stream; } _.init = function(body) { this._ = body; }; _.parse = function(stream) { return this.skip(eof)._(stream, success, parseError); function success(stream, result) { return result; } }; // -*- primitive combinators -*- // _.or = function(alternative) { pray('or is passed a parser', alternative instanceof Parser); var self = this; return Parser(function(stream, onSuccess, onFailure) { return self._(stream, onSuccess, failure); function failure(newStream) { return alternative._(stream, onSuccess, onFailure); } }); }; _.then = function(next) { var self = this; return Parser(function(stream, onSuccess, onFailure) { return self._(stream, success, onFailure); function success(newStream, result) { var nextParser = (next instanceof Parser ? next : next(result)); pray('a parser is returned', nextParser instanceof Parser); return nextParser._(newStream, onSuccess, onFailure); } }); }; // -*- optimized iterative combinators -*- // _.many = function() { var self = this; return Parser(function(stream, onSuccess, onFailure) { var xs = []; while (self._(stream, success, failure)); return onSuccess(stream, xs); function success(newStream, x) { stream = newStream; xs.push(x); return true; } function failure() { return false; } }); }; _.times = function(min, max) { if (arguments.length < 2) max = min; var self = this; return Parser(function(stream, onSuccess, onFailure) { var xs = []; var result = true; var failure; for (var i = 0; i < min; i += 1) { result = self._(stream, success, firstFailure); if (!result) return onFailure(stream, failure); } for (; i < max && result; i += 1) { result = self._(stream, success, secondFailure); } return onSuccess(stream, xs); function success(newStream, x) { xs.push(x); stream = newStream; return true; } function firstFailure(newStream, msg) { failure = msg; stream = newStream; return false; } function secondFailure(newStream, msg) { return false; } }); }; // -*- higher-level combinators -*- // _.result = function(res) { return this.then(succeed(res)); }; _.atMost = function(n) { return this.times(0, n); }; _.atLeast = function(n) { var self = this; return self.times(n).then(function(start) { return self.many().map(function(end) { return start.concat(end); }); }); }; _.map = function(fn) { return this.then(function(result) { return succeed(fn(result)); }); }; _.skip = function(two) { return this.then(function(result) { return two.result(result); }); }; // -*- primitive parsers -*- // var string = this.string = function(str) { var len = str.length; var expected = "expected '"+str+"'"; return Parser(function(stream, onSuccess, onFailure) { var head = stream.slice(0, len); if (head === str) { return onSuccess(stream.slice(len), head); } else { return onFailure(stream, expected); } }); }; var regex = this.regex = function(re) { pray('regexp parser is anchored', re.toString().charAt(1) === '^'); var expected = 'expected '+re; return Parser(function(stream, onSuccess, onFailure) { var match = re.exec(stream); if (match) { var result = match[0]; return onSuccess(stream.slice(result.length), result); } else { return onFailure(stream, expected); } }); }; var succeed = Parser.succeed = function(result) { return Parser(function(stream, onSuccess) { return onSuccess(stream, result); }); }; var fail = Parser.fail = function(msg) { return Parser(function(stream, _, onFailure) { return onFailure(stream, msg); }); }; var letter = Parser.letter = regex(/^[a-z]/i); var letters = Parser.letters = regex(/^[a-z]*/i); var digit = Parser.digit = regex(/^[0-9]/); var digits = Parser.digits = regex(/^[0-9]*/); var whitespace = Parser.whitespace = regex(/^\s+/); var optWhitespace = Parser.optWhitespace = regex(/^\s*/); var any = Parser.any = Parser(function(stream, onSuccess, onFailure) { if (!stream) return onFailure(stream, 'expected any character'); return onSuccess(stream.slice(1), stream.charAt(0)); }); var all = Parser.all = Parser(function(stream, onSuccess, onFailure) { return onSuccess('', stream); }); var eof = Parser.eof = Parser(function(stream, onSuccess, onFailure) { if (stream) return onFailure(stream, 'expected EOF'); return onSuccess(stream, stream); }); }); /************************************************* * 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() { checkTextarea(); }); // -*- 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(); 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; handleKey(); if (shouldBeSelected) checkTextareaFor(function() { 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 }); } 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(); // 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 }; }; }()); /*********************************************** * Export math in a human-readable text format * As you can see, only half-baked so far. **********************************************/ Controller.open(function(_, super_) { _.exportText = function(opts) { return this.root.foldChildren('', function(text, child) { return text + child.text(opts); }) .replace(/\\operatorname\{(.*?)\}/g,"$1") .replace(/\\/g, "") .replace(/\* *\*/g,'*') .replace(/ *_/g,'_') .replace(/\* *$/,''); //Random cases of hanging multiplications...just remove these. //TODO: '**' shouldnt happen, so it should really be dealt with by fixing whatever is causing them }; }); Controller.open(function(_) { _.focusBlurEvents = function() { var ctrlr = this, root = ctrlr.root, cursor = ctrlr.cursor; var blurTimeout; ctrlr.textarea.focus(function() { ctrlr.blurred = false; clearTimeout(blurTimeout); ctrlr.container.addClass('mq-focused'); if (!cursor.parent) cursor.insAtRightEnd(root); if (cursor.selection) { cursor.selection.jQ.removeClass('mq-blur'); ctrlr.selectionChanged(); //re-select textarea contents after tabbing away and back } else cursor.show(); }).blur(function() { ctrlr.blurred = true; blurTimeout = setTimeout(function() { // wait for blur on window; if root.postOrder('intentionalBlur'); // none, intentional blur: #264 cursor.clearSelection(); blur(); }); $(window).on('blur', windowBlur); }); function windowBlur() { // blur event also fired on window, just switching clearTimeout(blurTimeout); // tabs/windows, not intentional blur if (cursor.selection) cursor.selection.jQ.addClass('mq-blur'); blur(); } function blur() { // not directly in the textarea blur handler so as to be cursor.hide().parent.blur(); // synchronous with/in the same frame as ctrlr.container.removeClass('mq-focused'); // clearing/blurring selection $(window).off('blur', windowBlur); } ctrlr.blurred = true; cursor.hide().parent.blur(); }; }); /** * TODO: I wanted to move MathBlock::focus and blur here, it would clean * up lots of stuff like, TextBlock::focus is set to MathBlock::focus * and TextBlock::blur calls MathBlock::blur, when instead they could * use inheritance and super_. * * Problem is, there's lots of calls to .focus()/.blur() on nodes * outside Controller::focusBlurEvents(), such as .postOrder('blur') on * insertion, which if MathBlock::blur becomes Node::blur, would add the * 'blur' CSS class to all Symbol's (because .isEmpty() is true for all * of them). * * I'm not even sure there aren't other troublesome calls to .focus() or * .blur(), so this is TODO for now. */ /***************************************** * Deals with the browser DOM events from * interaction with the typist. ****************************************/ Controller.open(function(_) { _.keystroke = function(key, evt) { this.cursor.parent.keystroke(key, evt, this); }; }); Node.open(function(_) { _.keystroke = function(key, e, ctrlr) { var cursor = ctrlr.cursor; switch (key) { case 'Ctrl-Shift-Backspace': case 'Ctrl-Backspace': while (cursor[L] || cursor.selection) { ctrlr.backspace(); } break; case 'Shift-Backspace': case 'Backspace': ctrlr.backspace(); break; // Tab or Esc -> go one block right if it exists, else escape right. case 'Esc': case 'Tab': ctrlr.escapeDir(R, key, e); return; // Shift-Tab -> go one block left if it exists, else escape left. case 'Shift-Tab': case 'Shift-Esc': ctrlr.escapeDir(L, key, e); return; // End -> move to the end of the current block. case 'End': ctrlr.notify('move').cursor.insAtRightEnd(cursor.parent); break; // Ctrl-End -> move all the way to the end of the root block. case 'Ctrl-End': ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); break; // Shift-End -> select to the end of the current block. case 'Shift-End': while (cursor[R]) { ctrlr.selectRight(); } break; // Ctrl-Shift-End -> select to the end of the root block. case 'Ctrl-Shift-End': while (cursor[R] || cursor.parent !== ctrlr.root) { ctrlr.selectRight(); } break; // Home -> move to the start of the root block or the current block. case 'Home': ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent); break; // Ctrl-Home -> move to the start of the current block. case 'Ctrl-Home': ctrlr.notify('move').cursor.insAtLeftEnd(ctrlr.root); break; // Shift-Home -> select to the start of the current block. case 'Shift-Home': while (cursor[L]) { ctrlr.selectLeft(); } break; // Ctrl-Shift-Home -> move to the start of the root block. case 'Ctrl-Shift-Home': while (cursor[L] || cursor.parent !== ctrlr.root) { ctrlr.selectLeft(); } break; case 'Left': ctrlr.moveLeft(); break; case 'Shift-Left': ctrlr.selectLeft(); break; case 'Ctrl-Left': break; case 'Right': ctrlr.moveRight(); break; case 'Shift-Right': ctrlr.selectRight(); break; case 'Ctrl-Right': break; case 'Up': ctrlr.moveUp(); break; case 'Down': ctrlr.moveDown(); break; case 'Shift-Up': if (cursor[L]) { while (cursor[L]) ctrlr.selectLeft(); } else { ctrlr.selectLeft(); } case 'Shift-Down': if (cursor[R]) { while (cursor[R]) ctrlr.selectRight(); } else { ctrlr.selectRight(); } case 'Ctrl-Up': break; case 'Ctrl-Down': break; case 'Ctrl-Shift-Del': case 'Ctrl-Del': while (cursor[R] || cursor.selection) { ctrlr.deleteForward(); } break; case 'Shift-Del': case 'Del': ctrlr.deleteForward(); break; case 'Meta-A': case 'Ctrl-A': ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); while (cursor[L]) ctrlr.selectLeft(); break; default: return; } e.preventDefault(); ctrlr.scrollHoriz(); }; _.moveOutOf = // called by Controller::escapeDir, moveDir _.moveTowards = // called by Controller::moveDir _.deleteOutOf = // called by Controller::deleteDir _.deleteTowards = // called by Controller::deleteDir _.unselectInto = // called by Controller::selectDir _.selectOutOf = // called by Controller::selectDir _.selectTowards = // called by Controller::selectDir function() { pray('overridden or never called on this node'); }; }); Controller.open(function(_) { this.onNotify(function(e) { if (e === 'move' || e === 'upDown') this.show().clearSelection(); }); _.escapeDir = function(dir, key, e) { prayDirection(dir); var cursor = this.cursor; // only prevent default of Tab if not in the root editable if (cursor.parent !== this.root) e.preventDefault(); // want to be a noop if in the root editable (in fact, Tab has an unrelated // default browser action if so) if (cursor.parent === this.root) return; cursor.parent.moveOutOf(dir, cursor); cursor.parent.bubble('workingGroupChange'); return this.notify('move'); }; optionProcessors.leftRightIntoCmdGoes = function(updown) { if (updown && updown !== 'up' && updown !== 'down') { throw '"up" or "down" required for leftRightIntoCmdGoes option, ' + 'got "'+updown+'"'; } return updown; }; _.moveDir = function(dir) { prayDirection(dir); var cursor = this.cursor, updown = cursor.options.leftRightIntoCmdGoes; if (cursor.selection) { cursor.insDirOf(dir, cursor.selection.ends[dir]); } else if (cursor[dir]) cursor[dir].moveTowards(dir, cursor, updown); else cursor.parent.moveOutOf(dir, cursor, updown); cursor.parent.bubble('workingGroupChange'); return this.notify('move'); }; _.moveLeft = function() { return this.moveDir(L); }; _.moveRight = function() { return this.moveDir(R); }; /** * moveUp and moveDown have almost identical algorithms: * - first check left and right, if so insAtLeft/RightEnd of them * - else check the parent's 'upOutOf'/'downOutOf' property: * + if it's a function, call it with the cursor as the sole argument and * use the return value as if it were the value of the property * + if it's a Node, jump up or down into it: * - if there is a cached Point in the block, insert there * - else, seekHoriz within the block to the current x-coordinate (to be * as close to directly above/below the current position as possible) * + unless it's exactly `true`, stop bubbling */ _.moveUp = function() { return moveUpDown(this, 'up'); }; _.moveDown = function() { return moveUpDown(this, 'down'); }; function moveUpDown(self, dir) { var cursor = self.notify('upDown').cursor; var dirInto = dir+'Into', dirOutOf = dir+'OutOf'; if (cursor[R][dirInto]) cursor.insAtLeftEnd(cursor[R][dirInto]); else if (cursor[L][dirInto]) cursor.insAtRightEnd(cursor[L][dirInto]); else { cursor.parent.bubble(function(ancestor) { var prop = ancestor[dirOutOf]; if (prop) { if (typeof prop === 'function') prop = ancestor[dirOutOf](cursor); if (prop instanceof Node) cursor.jumpUpDown(ancestor, prop); if (prop !== true) return false; } }); } cursor.parent.bubble('workingGroupChange'); return self; } this.onNotify(function(e) { if (e !== 'upDown') this.upDownCache = {}; }); this.onNotify(function(e) { if (e === 'edit') this.show().deleteSelection(); }); _.deleteDir = function(dir) { prayDirection(dir); var cursor = this.cursor; var hadSelection = cursor.selection; this.notify('edit'); // deletes selection if present if (!hadSelection) { if (cursor[dir]) cursor[dir].deleteTowards(dir, cursor); else cursor.parent.deleteOutOf(dir, cursor); } if (cursor[L].siblingDeleted) cursor[L].siblingDeleted(cursor.options, R); if (cursor[R].siblingDeleted) cursor[R].siblingDeleted(cursor.options, L); cursor.parent.bubble('reflow'); cursor.parent.bubble('workingGroupChange'); return this; }; _.backspace = function() { return this.deleteDir(L); }; _.deleteForward = function() { return this.deleteDir(R); }; this.onNotify(function(e) { if (e !== 'select') this.endSelection(); }); _.selectDir = function(dir) { var cursor = this.notify('select').cursor, seln = cursor.selection; prayDirection(dir); if (!cursor.anticursor) cursor.startSelection(); var node = cursor[dir]; if (node) { // "if node we're selecting towards is inside selection (hence retracting) // and is on the *far side* of the selection (hence is only node selected) // and the anticursor is *inside* that node, not just on the other side" if (seln && seln.ends[dir] === node && cursor.anticursor[-dir] !== node) { node.unselectInto(dir, cursor); } else node.selectTowards(dir, cursor); } else cursor.parent.selectOutOf(dir, cursor); cursor.clearSelection(); cursor.select() || cursor.show(); }; _.selectLeft = function() { return this.selectDir(L); }; _.selectRight = function() { return this.selectDir(R); }; }); // Parser MathCommand var latexMathParser = (function() { function commandToBlock(cmd) { var block = MathBlock(); cmd.adopt(block, 0, 0); return block; } function joinBlocks(blocks) { var firstBlock = blocks[0] || MathBlock(); for (var i = 1; i < blocks.length; i += 1) { blocks[i].children().adopt(firstBlock, firstBlock.ends[R], 0); } return firstBlock; } var string = Parser.string; var regex = Parser.regex; var letter = Parser.letter; var any = Parser.any; var optWhitespace = Parser.optWhitespace; var succeed = Parser.succeed; var fail = Parser.fail; // Parsers yielding MathCommands var variable = letter.map(function(c) { return Letter(c); }); var symbol = regex(/^[^${}\\_^]/).map(function(c) { return VanillaSymbol(c); }); var controlSequence = regex(/^[^\\a-eg-zA-Z]/) // hotfix #164; match MathBlock::write .or(string('\\').then( regex(/^[a-z]+/i) .or(regex(/^\s+/).result(' ')) .or(any) )).then(function(ctrlSeq) { var cmdKlass = LatexCmds[ctrlSeq]; if (cmdKlass) { return cmdKlass(ctrlSeq).parser(); } else { return fail('unknown command: \\'+ctrlSeq); } }) ; var command = controlSequence .or(variable) .or(symbol) ; // Parsers yielding MathBlocks var mathGroup = string('{').then(function() { return mathSequence; }).skip(string('}')); var mathBlock = optWhitespace.then(mathGroup.or(command.map(commandToBlock))); var mathSequence = mathBlock.many().map(joinBlocks).skip(optWhitespace); var optMathBlock = string('[').then( mathBlock.then(function(block) { return block.join('latex') !== ']' ? succeed(block) : fail(); }) .many().map(joinBlocks).skip(optWhitespace) ).skip(string(']')) ; var latexMath = mathSequence; latexMath.block = mathBlock; latexMath.optBlock = optMathBlock; return latexMath; })(); Controller.open(function(_, super_) { _.exportLatex = function() { return this.root.latex().replace(/(\\[a-z]+) (?![a-z])/ig,'$1'); }; _.writeLatex = function(latex) { var cursor = this.notify('edit').cursor; var all = Parser.all; var eof = Parser.eof; var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); if (block && !block.isEmpty()) { block.children().adopt(cursor.parent, cursor[L], cursor[R]); var jQ = block.jQize(); jQ.insertBefore(cursor.jQ); cursor[L] = block.ends[R]; block.finalizeInsert(cursor.options, cursor); if (block.ends[R][R].siblingCreated) block.ends[R][R].siblingCreated(cursor.options, L); if (block.ends[L][L].siblingCreated) block.ends[L][L].siblingCreated(cursor.options, R); cursor.parent.bubble('reflow'); } return this; }; _.renderLatexMath = function(latex) { var root = this.root, cursor = this.cursor; var all = Parser.all; var eof = Parser.eof; var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); root.eachChild('postOrder', 'dispose'); root.ends[L] = root.ends[R] = 0; if (block) { block.children().adopt(root, 0, 0); } var jQ = root.jQ; if (block) { var html = block.join('html'); jQ.html(html); root.jQize(jQ.children()); root.finalizeInsert(cursor.options); } else { jQ.empty(); } delete cursor.selection; cursor.insAtRightEnd(root); }; _.renderLatexText = function(latex) { var root = this.root, cursor = this.cursor; root.jQ.children().slice(1).remove(); root.eachChild('postOrder', 'dispose'); root.ends[L] = root.ends[R] = 0; delete cursor.selection; cursor.show().insAtRightEnd(root); var regex = Parser.regex; var string = Parser.string; var eof = Parser.eof; var all = Parser.all; // Parser RootMathCommand var mathMode = string('$').then(latexMathParser) // because TeX is insane, math mode doesn't necessarily // have to end. So we allow for the case that math mode // continues to the end of the stream. .skip(string('$').or(eof)) .map(function(block) { // HACK FIXME: this shouldn't have to have access to cursor var rootMathCommand = RootMathCommand(cursor); rootMathCommand.createBlocks(); var rootMathBlock = rootMathCommand.ends[L]; block.children().adopt(rootMathBlock, 0, 0); return rootMathCommand; }) ; var escapedDollar = string('\\$').result('$'); var textChar = escapedDollar.or(regex(/^[^$]/)).map(VanillaSymbol); var latexText = mathMode.or(textChar).many(); var commands = latexText.skip(eof).or(all.result(false)).parse(latex); if (commands) { for (var i = 0; i < commands.length; i += 1) { commands[i].adopt(root, root.ends[R], 0); } root.jQize().appendTo(root.jQ); root.finalizeInsert(cursor.options); } }; }); /******************************************************** * Deals with mouse events for clicking, drag-to-select *******************************************************/ Controller.open(function(_) { _.delegateMouseEvents = function() { var ultimateRootjQ = this.root.jQ; //drag-to-select event handling this.container.bind('mousedown.mathquill', function(e) { var rootjQ = $(e.target).closest('.mq-root-block'); var root = Node.byId[rootjQ.attr(mqBlockId) || ultimateRootjQ.attr(mqBlockId)]; var ctrlr = root.controller, cursor = ctrlr.cursor, blink = cursor.blink; var textareaSpan = ctrlr.textareaSpan, textarea = ctrlr.textarea; var target; function mousemove(e) { target = $(e.target); } function docmousemove(e) { if (!cursor.anticursor) cursor.startSelection(); ctrlr.seek(target, e.pageX, e.pageY).cursor.select(); target = undefined; } // outside rootjQ, the MathQuill node corresponding to the target (if any) // won't be inside this root, so don't mislead Controller::seek with it function mouseup(e) { cursor.blink = blink; if (!cursor.selection) { if (ctrlr.editable) { cursor.show(); } else { textareaSpan.detach(); } } // delete the mouse handlers now that we're not dragging anymore rootjQ.unbind('mousemove', mousemove); $(e.target.ownerDocument).unbind('mousemove', docmousemove).unbind('mouseup', mouseup); } if (ctrlr.blurred) { if (!ctrlr.editable) rootjQ.prepend(textareaSpan); textarea.focus(); } e.preventDefault(); // doesn't work in IE\u22648, but it's a one-line fix: e.target.unselectable = true; // http://jsbin.com/yagekiji/1 cursor.blink = noop; ctrlr.seek($(e.target), e.pageX, e.pageY).cursor.startSelection(); rootjQ.mousemove(mousemove); $(e.target.ownerDocument).mousemove(docmousemove).mouseup(mouseup); // listen on document not just body to not only hear about mousemove and // mouseup on page outside field, but even outside page, except iframes: https://github.com/mathquill/mathquill/commit/8c50028afcffcace655d8ae2049f6e02482346c5#commitcomment-6175800 }); } }); Controller.open(function(_) { _.seek = function(target, pageX, pageY) { var cursor = this.notify('select').cursor; if (target) { var nodeId = target.attr(mqBlockId) || target.attr(mqCmdId); if (!nodeId) { var targetParent = target.parent(); nodeId = targetParent.attr(mqBlockId) || targetParent.attr(mqCmdId); } } var node = nodeId ? Node.byId[nodeId] : this.root; pray('nodeId is the id of some Node that exists', node); // don't clear selection until after getting node from target, in case // target was selection span, otherwise target will have no parent and will // seek from root, which is less accurate (e.g. fraction) cursor.clearSelection().show(); node.seek(pageX, cursor); this.scrollHoriz(); // before .selectFrom when mouse-selecting, so // always hits no-selection case in scrollHoriz and scrolls slower cursor.parent.bubble('workingGroupChange'); return this; }; }); /*********************************************** * Horizontal panning for editable fields that * overflow their width **********************************************/ Controller.open(function(_) { _.scrollHoriz = function() { var cursor = this.cursor, seln = cursor.selection; var rootRect = this.root.jQ[0].getBoundingClientRect(); if (!seln) { var x = cursor.jQ[0].getBoundingClientRect().left; if (x > rootRect.right - 20) var scrollBy = x - (rootRect.right - 20); else if (x < rootRect.left + 20) var scrollBy = x - (rootRect.left + 20); else return; } else { var rect = seln.jQ[0].getBoundingClientRect(); var overLeft = rect.left - (rootRect.left + 20); var overRight = rect.right - (rootRect.right - 20); if (seln.ends[L] === cursor[R]) { if (overLeft < 0) var scrollBy = overLeft; else if (overRight > 0) { if (rect.left - overRight < rootRect.left + 20) var scrollBy = overLeft; else var scrollBy = overRight; } else return; } else { if (overRight > 0) var scrollBy = overRight; else if (overLeft < 0) { if (rect.right - overLeft > rootRect.right - 20) var scrollBy = overRight; else var scrollBy = overLeft; } else return; } } this.root.jQ.stop().animate({ scrollLeft: '+=' + scrollBy}, 100); }; }); /********************************************* * Manage the MathQuill instance's textarea * (as owned by the Controller) ********************************************/ Controller.open(function(_) { Options.p.substituteTextarea = function() { return $('