| /************************************************* | |
| * Abstract classes of text blocks | |
| ************************************************/ | |
| /** | |
| * Blocks of plain text, with one or two TextPiece's as children. | |
| * Represents flat strings of typically serif-font Roman characters, as | |
| * opposed to hierchical, nested, tree-structured math. | |
| * Wraps a single HTMLSpanElement. | |
| */ | |
| var TextBlock = P(Node, function(_, super_) { | |
| _.ctrlSeq = '\\text'; | |
| _.replaces = function(replacedText) { | |
| if (replacedText instanceof Fragment) | |
| this.replacedText = replacedText.remove().jQ.text(); | |
| else if (typeof replacedText === 'string') | |
| this.replacedText = replacedText; | |
| }; | |
| _.jQadd = function(jQ) { | |
| super_.jQadd.call(this, jQ); | |
| if (this.ends[L]) this.ends[L].jQadd(this.jQ[0].firstChild); | |
| }; | |
| _.createLeftOf = function(cursor) { | |
| var textBlock = this; | |
| super_.createLeftOf.call(this, cursor); | |
| if (textBlock[R].siblingCreated) textBlock[R].siblingCreated(cursor.options, L); | |
| if (textBlock[L].siblingCreated) textBlock[L].siblingCreated(cursor.options, R); | |
| textBlock.bubble('reflow'); | |
| cursor.insAtRightEnd(textBlock); | |
| if (textBlock.replacedText) | |
| for (var i = 0; i < textBlock.replacedText.length; i += 1) | |
| textBlock.write(cursor, textBlock.replacedText.charAt(i)); | |
| }; | |
| _.parser = function() { | |
| var textBlock = this; | |
| // TODO: correctly parse text mode | |
| var string = Parser.string; | |
| var regex = Parser.regex; | |
| var optWhitespace = Parser.optWhitespace; | |
| return optWhitespace | |
| .then(string('{')).then(regex(/^[^}]*/)).skip(string('}')) | |
| .map(function(text) { | |
| if (text.length === 0) return Fragment(); | |
| TextPiece(text).adopt(textBlock, 0, 0); | |
| return textBlock; | |
| }) | |
| ; | |
| }; | |
| _.textContents = function() { | |
| return this.foldChildren('', function(text, child) { | |
| return text + child.text; | |
| }); | |
| }; | |
| _.text = function() { return '"' + this.textContents() + '"'; }; | |
| _.latex = function() { | |
| var contents = this.textContents(); | |
| if (contents.length === 0) return ''; | |
| return '\\text{' + contents + '}'; | |
| }; | |
| _.html = function() { | |
| return ( | |
| '<span class="mq-text-mode" mathquill-command-id='+this.id+'>' | |
| + this.textContents() | |
| + '</span>' | |
| ); | |
| }; | |
| // editability methods: called by the cursor for editing, cursor movements, | |
| // and selection of the MathQuill tree, these all take in a direction and | |
| // the cursor | |
| _.moveTowards = function(dir, cursor) { cursor.insAtDirEnd(-dir, this); }; | |
| _.moveOutOf = function(dir, cursor) { cursor.insDirOf(dir, this); }; | |
| _.unselectInto = _.moveTowards; | |
| // TODO: make these methods part of a shared mixin or something. | |
| _.selectTowards = MathCommand.prototype.selectTowards; | |
| _.deleteTowards = MathCommand.prototype.deleteTowards; | |
| _.selectOutOf = function(dir, cursor) { | |
| cursor.insDirOf(dir, this); | |
| }; | |
| _.deleteOutOf = function(dir, cursor) { | |
| // backspace and delete at ends of block don't unwrap | |
| if (this.isEmpty()) cursor.insRightOf(this); | |
| }; | |
| _.write = function(cursor, ch) { | |
| cursor.show().deleteSelection(); | |
| if (ch !== '$') { | |
| if (!cursor[L]) TextPiece(ch).createLeftOf(cursor); | |
| else cursor[L].appendText(ch); | |
| } | |
| else if (this.isEmpty()) { | |
| cursor.insRightOf(this); | |
| VanillaSymbol('\\$','$').createLeftOf(cursor); | |
| } | |
| else if (!cursor[R]) cursor.insRightOf(this); | |
| else if (!cursor[L]) cursor.insLeftOf(this); | |
| else { // split apart | |
| var leftBlock = TextBlock(); | |
| var leftPc = this.ends[L]; | |
| leftPc.disown().jQ.detach(); | |
| leftPc.adopt(leftBlock, 0, 0); | |
| cursor.insLeftOf(this); | |
| super_.createLeftOf.call(leftBlock, cursor); | |
| } | |
| }; | |
| _.seek = function(pageX, cursor) { | |
| cursor.hide(); | |
| var textPc = fuseChildren(this); | |
| // insert cursor at approx position in DOMTextNode | |
| var avgChWidth = this.jQ.width()/this.text.length; | |
| var approxPosition = Math.round((pageX - this.jQ.offset().left)/avgChWidth); | |
| if (approxPosition <= 0) cursor.insAtLeftEnd(this); | |
| else if (approxPosition >= textPc.text.length) cursor.insAtRightEnd(this); | |
| else cursor.insLeftOf(textPc.splitRight(approxPosition)); | |
| // move towards mousedown (pageX) | |
| var displ = pageX - cursor.show().offset().left; // displacement | |
| var dir = displ && displ < 0 ? L : R; | |
| var prevDispl = dir; | |
| // displ * prevDispl > 0 iff displacement direction === previous direction | |
| while (cursor[dir] && displ * prevDispl > 0) { | |
| cursor[dir].moveTowards(dir, cursor); | |
| prevDispl = displ; | |
| displ = pageX - cursor.offset().left; | |
| } | |
| if (dir*displ < -dir*prevDispl) cursor[-dir].moveTowards(-dir, cursor); | |
| if (!cursor.anticursor) { | |
| // about to start mouse-selecting, the anticursor is gonna get put here | |
| this.anticursorPosition = cursor[L] && cursor[L].text.length; | |
| // ^ get it? 'cos if there's no cursor[L], it's 0... I'm a terrible person. | |
| } | |
| else if (cursor.anticursor.parent === this) { | |
| // mouse-selecting within this TextBlock, re-insert the anticursor | |
| var cursorPosition = cursor[L] && cursor[L].text.length;; | |
| if (this.anticursorPosition === cursorPosition) { | |
| cursor.anticursor = Point.copy(cursor); | |
| } | |
| else { | |
| if (this.anticursorPosition < cursorPosition) { | |
| var newTextPc = cursor[L].splitRight(this.anticursorPosition); | |
| cursor[L] = newTextPc; | |
| } | |
| else { | |
| var newTextPc = cursor[R].splitRight(this.anticursorPosition - cursorPosition); | |
| } | |
| cursor.anticursor = Point(this, newTextPc[L], newTextPc); | |
| } | |
| } | |
| }; | |
| _.blur = function(cursor) { | |
| MathBlock.prototype.blur.call(this); | |
| if (!cursor) return; | |
| if (this.textContents() === '') { | |
| this.remove(); | |
| if (cursor[L] === this) cursor[L] = this[L]; | |
| else if (cursor[R] === this) cursor[R] = this[R]; | |
| } | |
| else fuseChildren(this); | |
| }; | |
| function fuseChildren(self) { | |
| self.jQ[0].normalize(); | |
| var textPcDom = self.jQ[0].firstChild; | |
| if (!textPcDom) return; | |
| pray('only node in TextBlock span is Text node', textPcDom.nodeType === 3); | |
| // nodeType === 3 has meant a Text node since ancient times: | |
| // http://reference.sitepoint.com/javascript/Node/nodeType | |
| var textPc = TextPiece(textPcDom.data); | |
| textPc.jQadd(textPcDom); | |
| self.children().disown(); | |
| return textPc.adopt(self, 0, 0); | |
| } | |
| _.focus = MathBlock.prototype.focus; | |
| }); | |
| /** | |
| * Piece of plain text, with a TextBlock as a parent and no children. | |
| * Wraps a single DOMTextNode. | |
| * For convenience, has a .text property that's just a JavaScript string | |
| * mirroring the text contents of the DOMTextNode. | |
| * Text contents must always be nonempty. | |
| */ | |
| var TextPiece = P(Node, function(_, super_) { | |
| _.init = function(text) { | |
| super_.init.call(this); | |
| this.text = text; | |
| }; | |
| _.jQadd = function(dom) { this.dom = dom; this.jQ = $(dom); }; | |
| _.jQize = function() { | |
| return this.jQadd(document.createTextNode(this.text)); | |
| }; | |
| _.appendText = function(text) { | |
| this.text += text; | |
| this.dom.appendData(text); | |
| }; | |
| _.prependText = function(text) { | |
| this.text = text + this.text; | |
| this.dom.insertData(0, text); | |
| }; | |
| _.insTextAtDirEnd = function(text, dir) { | |
| prayDirection(dir); | |
| if (dir === R) this.appendText(text); | |
| else this.prependText(text); | |
| }; | |
| _.splitRight = function(i) { | |
| var newPc = TextPiece(this.text.slice(i)).adopt(this.parent, this, this[R]); | |
| newPc.jQadd(this.dom.splitText(i)); | |
| this.text = this.text.slice(0, i); | |
| return newPc; | |
| }; | |
| function endChar(dir, text) { | |
| return text.charAt(dir === L ? 0 : -1 + text.length); | |
| } | |
| _.moveTowards = function(dir, cursor) { | |
| prayDirection(dir); | |
| var ch = endChar(-dir, this.text) | |
| var from = this[-dir]; | |
| if (from) from.insTextAtDirEnd(ch, dir); | |
| else TextPiece(ch).createDir(-dir, cursor); | |
| return this.deleteTowards(dir, cursor); | |
| }; | |
| _.latex = function() { return this.text; }; | |
| _.deleteTowards = function(dir, cursor) { | |
| if (this.text.length > 1) { | |
| if (dir === R) { | |
| this.dom.deleteData(0, 1); | |
| this.text = this.text.slice(1); | |
| } | |
| else { | |
| // note that the order of these 2 lines is annoyingly important | |
| // (the second line mutates this.text.length) | |
| this.dom.deleteData(-1 + this.text.length, 1); | |
| this.text = this.text.slice(0, -1); | |
| } | |
| } | |
| else { | |
| this.remove(); | |
| this.jQ.remove(); | |
| cursor[dir] = this[dir]; | |
| } | |
| }; | |
| _.selectTowards = function(dir, cursor) { | |
| prayDirection(dir); | |
| var anticursor = cursor.anticursor; | |
| var ch = endChar(-dir, this.text) | |
| if (anticursor[dir] === this) { | |
| var newPc = TextPiece(ch).createDir(dir, cursor); | |
| anticursor[dir] = newPc; | |
| cursor.insDirOf(dir, newPc); | |
| } | |
| else { | |
| var from = this[-dir]; | |
| if (from) from.insTextAtDirEnd(ch, dir); | |
| else { | |
| var newPc = TextPiece(ch).createDir(-dir, cursor); | |
| newPc.jQ.insDirOf(-dir, cursor.selection.jQ); | |
| } | |
| if (this.text.length === 1 && anticursor[-dir] === this) { | |
| anticursor[-dir] = this[-dir]; // `this` will be removed in deleteTowards | |
| } | |
| } | |
| return this.deleteTowards(dir, cursor); | |
| }; | |
| }); | |
| LatexCmds.text = | |
| LatexCmds.textnormal = | |
| LatexCmds.textrm = | |
| LatexCmds.textup = | |
| LatexCmds.textmd = TextBlock; | |
| function makeTextBlock(latex, tagName, attrs) { | |
| return P(TextBlock, { | |
| ctrlSeq: latex, | |
| htmlTemplate: '<'+tagName+' '+attrs+'>&0</'+tagName+'>' | |
| }); | |
| } | |
| LatexCmds.em = LatexCmds.italic = LatexCmds.italics = | |
| LatexCmds.emph = LatexCmds.textit = LatexCmds.textsl = | |
| makeTextBlock('\\textit', 'i', 'class="mq-text-mode"'); | |
| LatexCmds.strong = LatexCmds.bold = LatexCmds.textbf = | |
| makeTextBlock('\\textbf', 'b', 'class="mq-text-mode"'); | |
| LatexCmds.sf = LatexCmds.textsf = | |
| makeTextBlock('\\textsf', 'span', 'class="mq-sans-serif mq-text-mode"'); | |
| LatexCmds.tt = LatexCmds.texttt = | |
| makeTextBlock('\\texttt', 'span', 'class="mq-monospace mq-text-mode"'); | |
| LatexCmds.textsc = | |
| makeTextBlock('\\textsc', 'span', 'style="font-variant:small-caps" class="mq-text-mode"'); | |
| LatexCmds.uppercase = | |
| makeTextBlock('\\uppercase', 'span', 'style="text-transform:uppercase" class="mq-text-mode"'); | |
| LatexCmds.lowercase = | |
| makeTextBlock('\\lowercase', 'span', 'style="text-transform:lowercase" class="mq-text-mode"'); | |
| var RootMathCommand = P(MathCommand, function(_, super_) { | |
| _.init = function(cursor) { | |
| super_.init.call(this, '$'); | |
| this.cursor = cursor; | |
| }; | |
| _.htmlTemplate = '<span class="mq-math-mode">&0</span>'; | |
| _.createBlocks = function() { | |
| super_.createBlocks.call(this); | |
| this.ends[L].cursor = this.cursor; | |
| this.ends[L].write = function(cursor, ch) { | |
| if (ch !== '$') | |
| MathBlock.prototype.write.call(this, cursor, ch); | |
| else if (this.isEmpty()) { | |
| cursor.insRightOf(this.parent); | |
| this.parent.deleteTowards(dir, cursor); | |
| VanillaSymbol('\\$','$').createLeftOf(cursor.show()); | |
| } | |
| else if (!cursor[R]) | |
| cursor.insRightOf(this.parent); | |
| else if (!cursor[L]) | |
| cursor.insLeftOf(this.parent); | |
| else | |
| MathBlock.prototype.write.call(this, cursor, ch); | |
| }; | |
| }; | |
| _.latex = function() { | |
| return '$' + this.ends[L].latex() + '$'; | |
| }; | |
| }); | |
| var RootTextBlock = P(RootMathBlock, function(_, super_) { | |
| _.keystroke = function(key) { | |
| if (key === 'Spacebar' || key === 'Shift-Spacebar') return; | |
| return super_.keystroke.apply(this, arguments); | |
| }; | |
| _.write = function(cursor, ch) { | |
| cursor.show().deleteSelection(); | |
| if (ch === '$') | |
| RootMathCommand(cursor).createLeftOf(cursor); | |
| else { | |
| var html; | |
| if (ch === '<') html = '<'; | |
| else if (ch === '>') html = '>'; | |
| VanillaSymbol(ch, html).createLeftOf(cursor); | |
| } | |
| }; | |
| }); | |
| API.TextField = function(APIClasses) { | |
| return P(APIClasses.EditableField, function(_, super_) { | |
| this.RootBlock = RootTextBlock; | |
| _.__mathquillify = function() { | |
| return super_.__mathquillify.call(this, 'mq-editable-field mq-text-mode'); | |
| }; | |
| _.latex = function(latex) { | |
| if (arguments.length > 0) { | |
| this.__controller.renderLatexText(latex); | |
| if (this.__controller.blurred) this.__controller.cursor.hide().parent.blur(); | |
| return this; | |
| } | |
| return this.__controller.exportLatex(); | |
| }; | |
| }); | |
| }; |