blob: 2ccdeb2b267c2f994d8f788b92aa5b7f84444182 [file] [log] [blame]
/*********************************************************
* The publicly exposed MathQuill API.
********************************************************/
var API = {}, Options = P(), optionProcessors = {}, Progenote = P(), EMBEDS = {};
/**
* Interface Versioning (#459, #495) to allow us to virtually guarantee
* backcompat. v0.10.x introduces it, so for now, don't completely break the
* API for people who don't know about it, just complain with console.warn().
*
* The methods are shimmed in outro.js so that MQ.MathField.prototype etc can
* be accessed.
*/
function insistOnInterVer() {
if (window.console) console.warn(
'You are using the MathQuill API without specifying an interface version, ' +
'which will fail in v1.0.0. Easiest fix is to do the following before ' +
'doing anything else:\n' +
'\n' +
' MathQuill = MathQuill.getInterface(1);\n' +
' // now MathQuill.MathField() works like it used to\n' +
'\n' +
'See also the "`dev` branch (2014–2015) → v0.10.0 Migration Guide" at\n' +
' https://github.com/mathquill/mathquill/wiki/%60dev%60-branch-(2014%E2%80%932015)-%E2%86%92-v0.10.0-Migration-Guide'
);
}
// globally exported API object
function MathQuill(el) {
insistOnInterVer();
return MQ1(el);
};
MathQuill.prototype = Progenote.p;
MathQuill.interfaceVersion = function(v) {
// shim for #459-era interface versioning (ended with #495)
if (v !== 1) throw 'Only interface version 1 supported. You specified: ' + v;
insistOnInterVer = function() {
if (window.console) console.warn(
'You called MathQuill.interfaceVersion(1); to specify the interface ' +
'version, which will fail in v1.0.0. You can fix this easily by doing ' +
'this before doing anything else:\n' +
'\n' +
' MathQuill = MathQuill.getInterface(1);\n' +
' // now MathQuill.MathField() works like it used to\n' +
'\n' +
'See also the "`dev` branch (2014–2015) → v0.10.0 Migration Guide" at\n' +
' https://github.com/mathquill/mathquill/wiki/%60dev%60-branch-(2014%E2%80%932015)-%E2%86%92-v0.10.0-Migration-Guide'
);
};
insistOnInterVer();
return MathQuill;
};
MathQuill.getInterface = getInterface;
var MIN = getInterface.MIN = 1, MAX = getInterface.MAX = 2;
function getInterface(v) {
if (!(MIN <= v && v <= MAX)) throw 'Only interface versions between ' +
MIN + ' and ' + MAX + ' supported. You specified: ' + v;
/**
* Function that takes an HTML element and, if it's the root HTML element of a
* static math or math or text field, returns an API object for it (else, null).
*
* var mathfield = MQ.MathField(mathFieldSpan);
* assert(MQ(mathFieldSpan).id === mathfield.id);
* assert(MQ(mathFieldSpan).id === MQ(mathFieldSpan).id);
*
*/
function MQ(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);
var ctrlr = blockId && Node.byId[blockId].controller;
return ctrlr ? APIClasses[ctrlr.KIND_OF_MQ](ctrlr) : null;
};
var APIClasses = {};
MQ.L = L;
MQ.R = R;
function config(currentOptions, newOptions) {
if (newOptions && newOptions.handlers) {
newOptions.handlers = { fns: newOptions.handlers, APIClasses: APIClasses };
}
for (var name in newOptions) if (newOptions.hasOwnProperty(name)) {
var value = newOptions[name], processor = optionProcessors[name];
currentOptions[name] = (processor ? processor(value) : value);
}
}
MQ.config = function(opts) { config(Options.p, opts); return this; };
MQ.registerEmbed = function(name, options) {
if (!/^[a-z][a-z0-9]*$/i.test(name)) {
throw 'Embed name must start with letter and be only letters and digits';
}
EMBEDS[name] = options;
};
var AbstractMathQuill = APIClasses.AbstractMathQuill = P(Progenote, function(_) {
_.init = function(ctrlr) {
this.__controller = ctrlr;
this.__options = ctrlr.options;
this.id = ctrlr.id;
this.data = ctrlr.data;
};
_.__mathquillify = function(classNames) {
var ctrlr = this.__controller, root = ctrlr.root, el = ctrlr.container;
ctrlr.createTextarea();
var contents = el.addClass(classNames).contents().detach();
root.jQ =
$('<span class="mq-root-block"/>').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 = function(opts) { config(this.__options, opts); return this; };
_.el = function() { return this.__controller.container[0]; };
_.text = function() { return this.__controller.exportText(); };
_.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 class="?mq-cursor( mq-blink)?"?>.?<\/span>/i, '')
.replace(/ mq-hasCursor|mq-hasCursor ?/, '')
.replace(/ class=(""|(?= |>))/g, '');
};
_.reflow = function() {
this.__controller.root.postOrder('reflow');
return this;
};
});
MQ.prototype = AbstractMathQuill.prototype;
APIClasses.EditableField = P(AbstractMathQuill, function(_, super_) {
_.__mathquillify = function() {
super_.__mathquillify.apply(this, arguments);
this.__controller.editable = true;
this.__controller.delegateMouseEvents();
this.__controller.editablesTextareaEvents();
return this;
};
_.focus = function() { this.__controller.textarea.focus(); return this; };
_.blur = function() { this.__controller.textarea.blur(); return this; };
_.write = function(latex) {
this.__controller.writeLatex(latex);
this.__controller.scrollHoriz();
if (this.__controller.blurred) this.__controller.cursor.hide().parent.blur();
return this;
};
_.cmd = function(cmd) {
var ctrlr = this.__controller.notify(), cursor = ctrlr.cursor;
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.show());
this.__controller.scrollHoriz();
}
else /* TODO: API needs better error reporting */;
}
else cursor.parent.write(cursor, cmd);
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;
};
_.dropEmbedded = function(pageX, pageY, options) {
var clientX = pageX - $(window).scrollLeft();
var clientY = pageY - $(window).scrollTop();
var el = document.elementFromPoint(clientX, clientY);
this.__controller.seek($(el), pageX, pageY);
var cmd = Embed().setOptions(options);
cmd.createLeftOf(this.__controller.cursor);
};
_.clickAt = function(clientX, clientY, target) {
target = target || document.elementFromPoint(clientX, clientY);
var ctrlr = this.__controller, root = ctrlr.root;
if (!jQuery.contains(root.jQ[0], target)) target = root.jQ[0];
ctrlr.seek($(target), clientX + pageXOffset, clientY + pageYOffset);
if (ctrlr.blurred) this.focus();
return this;
};
_.ignoreNextMousedown = function(fn) {
this.__controller.cursor.options.ignoreNextMousedown = fn;
return this;
};
});
MQ.EditableField = function() { throw "wtf don't call me, I'm 'abstract'"; };
MQ.EditableField.prototype = APIClasses.EditableField.prototype;
/**
* Export the API functions that MathQuill-ify an HTML element into API objects
* of each class. If the element had already been MathQuill-ified but into a
* different kind (or it's not an HTML element), return null.
*/
for (var kind in API) (function(kind, defAPIClass) {
var APIClass = APIClasses[kind] = defAPIClass(APIClasses);
MQ[kind] = function(el, opts) {
var mq = MQ(el);
if (mq instanceof APIClass || !el || !el.nodeType) return mq;
var ctrlr = Controller(APIClass.RootBlock(), $(el), Options());
ctrlr.KIND_OF_MQ = kind;
return APIClass(ctrlr).__mathquillify(opts, v);
};
MQ[kind].prototype = APIClass.prototype;
}(kind, API[kind]));
return MQ;
}
MathQuill.noConflict = function() {
window.MathQuill = origMathQuill;
return MathQuill;
};
var origMathQuill = window.MathQuill;
window.MathQuill = MathQuill;
function RootBlockMixin(_) {
var names = 'moveOutOf deleteOutOf selectOutOf upOutOf downOutOf'.split(' ');
for (var i = 0; i < names.length; i += 1) (function(name) {
_[name] = function(dir) { this.controller.handle(name, dir); };
}(names[i]));
_.reflow = function() {
this.controller.handle('reflow');
this.controller.handle('edited');
this.controller.handle('edit');
};
}