diff --git a/conf/defaults.config b/conf/defaults.config index 7773cb771a..cba6c704c5 100644 --- a/conf/defaults.config +++ b/conf/defaults.config @@ -934,6 +934,8 @@ $options{PGCodeMirror} = 1; $options{PGMathView} = 0; # This sets if WirisEditor is available on the PG editor for use as a minimal latex equation editor $options{PGWirisEditor}= 0; +# This sets if MathQuill is available on the PG editor for use as a minimal latex equation editor +$options{PGMathQuill}= 0; ########################################################################################### #### Default settings for the PG translator @@ -959,6 +961,9 @@ $pg{options}{mathViewLocale} = "mv_locale_us.js"; # Default for showing the WirisEditor preview system. To completely disable WirisEditor you need to change the PG special environment variable. $pg{options}{useWirisEditor} = 1; +# Default for showing the MathQuill preview system. To completely disable MathQuill you need to change the PG special environment variable. +$pg{options}{useMathQuill} = 1; + # Show correct answers (when allowed) by default? $pg{options}{showCorrectAnswers} = 0; # this is a backup value use when nothing else has been set. I can think of no case where it should anything but zero. @@ -1379,6 +1384,7 @@ $webservices = { $pg{specialPGEnvironmentVars}{DragMath} = 0; $pg{specialPGEnvironmentVars}{MathView} = 0; $pg{specialPGEnvironmentVars}{WirisEditor} = 0; +$pg{specialPGEnvironmentVars}{MathQuill} = 0; ############################################################################### # default Homework Config settings @@ -1667,6 +1673,11 @@ $ConfigValues = [ doc2 => 'WIRIS EDITOR is a commercial software. Using it in WebWork requires a valid license. Please contact sales@wiris.com for further information or visit www.wiris.com/solutions/webwork. Visual math editor (WYSIWYG) that allows inserting mathematical and chemistry equations. Based on JavaScript technology. It runs on any browser, including the ones in tablet PCs. You can use a large collection of icons nicely organized in thematic tabs in order to create formulas. Handwriting input also available for touchable devices. Fully featured equation editor including accessibility features.', type => 'boolean' + }, + { var => 'pg{specialPGEnvironmentVars}{MathQuill}', + doc => 'Use MathQuill to live-render responses (proof of concept)', + doc2 => 'MathQuill effectively renders student responses into display-math without the need for Preview Answer. Open source.', + type => 'boolean' }, { var => 'pg{options}{showEvaluatedAnswers}', doc => 'Display the evaluated student answer', diff --git a/conf/localOverrides.conf.dist b/conf/localOverrides.conf.dist index f1cbdbdddc..6bd37f88f0 100644 --- a/conf/localOverrides.conf.dist +++ b/conf/localOverrides.conf.dist @@ -223,6 +223,7 @@ $options{PGCodeMirror} = 1; # This sets if mathview is available on the PG editor for use as a minimal latex equation editor $options{PGMathView} = 0; $options{PGWirisEditor} = 0; +$options{PGMathQuill} = 0; ################################################################################ # PG subsystem options diff --git a/htdocs/js/apps/MathQuill/font/Symbola-basic.eot b/htdocs/js/apps/MathQuill/font/Symbola-basic.eot new file mode 100644 index 0000000000..2e39ec7bbf Binary files /dev/null and b/htdocs/js/apps/MathQuill/font/Symbola-basic.eot differ diff --git a/htdocs/js/apps/MathQuill/font/Symbola-basic.ttf b/htdocs/js/apps/MathQuill/font/Symbola-basic.ttf new file mode 100644 index 0000000000..cf968b9ad7 Binary files /dev/null and b/htdocs/js/apps/MathQuill/font/Symbola-basic.ttf differ diff --git a/htdocs/js/apps/MathQuill/font/Symbola-basic.woff b/htdocs/js/apps/MathQuill/font/Symbola-basic.woff new file mode 100644 index 0000000000..104a15090e Binary files /dev/null and b/htdocs/js/apps/MathQuill/font/Symbola-basic.woff differ diff --git a/htdocs/js/apps/MathQuill/font/Symbola-basic.woff2 b/htdocs/js/apps/MathQuill/font/Symbola-basic.woff2 new file mode 100755 index 0000000000..7c0d6c8cf6 Binary files /dev/null and b/htdocs/js/apps/MathQuill/font/Symbola-basic.woff2 differ diff --git a/htdocs/js/apps/MathQuill/font/Symbola.eot b/htdocs/js/apps/MathQuill/font/Symbola.eot new file mode 100755 index 0000000000..0d10a95b88 Binary files /dev/null and b/htdocs/js/apps/MathQuill/font/Symbola.eot differ diff --git a/htdocs/js/apps/MathQuill/font/Symbola.otf b/htdocs/js/apps/MathQuill/font/Symbola.otf new file mode 100755 index 0000000000..d549563484 Binary files /dev/null and b/htdocs/js/apps/MathQuill/font/Symbola.otf differ diff --git a/htdocs/js/apps/MathQuill/font/Symbola.svg b/htdocs/js/apps/MathQuill/font/Symbola.svg new file mode 100755 index 0000000000..eff3111f8b --- /dev/null +++ b/htdocs/js/apps/MathQuill/font/Symbola.svg @@ -0,0 +1,5102 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Designer : George Douros +Foundry : Free +Foundry URL : http://users.teilar.gr/_g1951d/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/htdocs/js/apps/MathQuill/font/Symbola.ttf b/htdocs/js/apps/MathQuill/font/Symbola.ttf new file mode 100755 index 0000000000..52337df9be Binary files /dev/null and b/htdocs/js/apps/MathQuill/font/Symbola.ttf differ diff --git a/htdocs/js/apps/MathQuill/font/Symbola.woff b/htdocs/js/apps/MathQuill/font/Symbola.woff new file mode 100755 index 0000000000..b9bba23986 Binary files /dev/null and b/htdocs/js/apps/MathQuill/font/Symbola.woff differ diff --git a/htdocs/js/apps/MathQuill/font/Symbola.woff2 b/htdocs/js/apps/MathQuill/font/Symbola.woff2 new file mode 100755 index 0000000000..9d3e8209cf Binary files /dev/null and b/htdocs/js/apps/MathQuill/font/Symbola.woff2 differ diff --git a/htdocs/js/apps/MathQuill/mathquill-basic.css b/htdocs/js/apps/MathQuill/mathquill-basic.css new file mode 100644 index 0000000000..df9bd10bb4 --- /dev/null +++ b/htdocs/js/apps/MathQuill/mathquill-basic.css @@ -0,0 +1,425 @@ +/* + * MathQuill v0.10.1 http://mathquill.com + * by Han, Jeanine, and Mary maintainers@mathquill.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/. + */ +@font-face { + font-family: Symbola; + src: url(font/Symbola-basic.eot); + src: local("Symbola Regular"), local("Symbola"), url(font/Symbola-basic.woff2) format("woff2"), url(font/Symbola-basic.woff) format("woff"), url(font/Symbola-basic.ttf) format("truetype"); +} +.mq-editable-field { + display: -moz-inline-box; + display: inline-block; +} +.mq-editable-field .mq-cursor { + border-left: 1px solid black; + margin-left: -1px; + position: relative; + z-index: 1; + padding: 0; + display: -moz-inline-box; + display: inline-block; +} +.mq-editable-field .mq-cursor.mq-blink { + visibility: hidden; +} +.mq-editable-field, +.mq-math-mode .mq-editable-field { + border: 1px solid gray; +} +.mq-editable-field.mq-focused, +.mq-math-mode .mq-editable-field.mq-focused { + -webkit-box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0; + -moz-box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0; + box-shadow: #8bd 0 0 1px 2px, inset #6ae 0 0 2px 0; + border-color: #709AC0; + border-radius: 1px; +} +.mq-math-mode .mq-editable-field { + margin: 1px; +} +.mq-editable-field .mq-latex-command-input { + color: inherit; + font-family: "Courier New", monospace; + border: 1px solid gray; + padding-right: 1px; + margin-right: 1px; + margin-left: 2px; +} +.mq-editable-field .mq-latex-command-input.mq-empty { + background: transparent; +} +.mq-editable-field .mq-latex-command-input.mq-hasCursor { + border-color: ActiveBorder; +} +.mq-editable-field.mq-empty:after, +.mq-editable-field.mq-text-mode:after, +.mq-math-mode .mq-empty:after { + visibility: hidden; + content: 'c'; +} +.mq-editable-field .mq-cursor:only-child:after, +.mq-editable-field .mq-textarea + .mq-cursor:last-child:after { + visibility: hidden; + content: 'c'; +} +.mq-editable-field .mq-text-mode .mq-cursor:only-child:after { + content: ''; +} +.mq-editable-field.mq-text-mode { + overflow-x: auto; + overflow-y: hidden; +} +.mq-root-block, +.mq-math-mode .mq-root-block { + display: -moz-inline-box; + display: inline-block; + width: 100%; + padding: 2px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + white-space: nowrap; + overflow: hidden; + vertical-align: middle; +} +.mq-math-mode { + font-variant: normal; + font-weight: normal; + font-style: normal; + font-size: 115%; + line-height: 1; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-non-leaf, +.mq-math-mode .mq-scaled { + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode var, +.mq-math-mode .mq-text-mode, +.mq-math-mode .mq-nonSymbola { + font-family: "Times New Roman", Symbola, serif; + line-height: .9; +} +.mq-math-mode * { + font-size: inherit; + line-height: inherit; + margin: 0; + padding: 0; + border-color: black; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + box-sizing: border-box; +} +.mq-math-mode .mq-empty { + background: #ccc; +} +.mq-math-mode .mq-empty.mq-root-block { + background: transparent; +} +.mq-math-mode.mq-empty { + background: transparent; +} +.mq-math-mode .mq-text-mode { + font-size: 87%; +} +.mq-math-mode .mq-font { + font: 1em "Times New Roman", Symbola, serif; +} +.mq-math-mode .mq-font * { + font-family: inherit; + font-style: inherit; +} +.mq-math-mode b, +.mq-math-mode b.mq-font { + font-weight: bolder; +} +.mq-math-mode var, +.mq-math-mode i, +.mq-math-mode i.mq-font { + font-style: italic; +} +.mq-math-mode var.mq-f { + margin-right: 0.2em; + margin-left: 0.1em; +} +.mq-math-mode .mq-roman var.mq-f { + margin: 0; +} +.mq-math-mode big { + font-size: 125%; +} +.mq-math-mode .mq-roman { + font-style: normal; +} +.mq-math-mode .mq-sans-serif { + font-family: sans-serif, Symbola, serif; +} +.mq-math-mode .mq-monospace { + font-family: monospace, Symbola, serif; +} +.mq-math-mode .mq-overline { + border-top: 1px solid black; + margin-top: 1px; +} +.mq-math-mode .mq-underline { + border-bottom: 1px solid black; + margin-bottom: 1px; +} +.mq-math-mode .mq-binary-operator { + padding: 0 0.2em; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-supsub { + font-size: 90%; + vertical-align: -0.5em; +} +.mq-math-mode .mq-supsub.mq-limit { + font-size: 80%; + vertical-align: -0.4em; +} +.mq-math-mode .mq-supsub.mq-sup-only { + vertical-align: .5em; +} +.mq-math-mode .mq-supsub.mq-sup-only .mq-sup { + display: inline-block; + vertical-align: text-bottom; +} +.mq-math-mode .mq-supsub .mq-sup { + display: block; +} +.mq-math-mode .mq-supsub .mq-sub { + display: block; + float: left; +} +.mq-math-mode .mq-supsub.mq-limit .mq-sub { + margin-left: -0.25em; +} +.mq-math-mode .mq-supsub .mq-binary-operator { + padding: 0 .1em; +} +.mq-math-mode .mq-supsub .mq-fraction { + font-size: 70%; +} +.mq-math-mode sup.mq-nthroot { + font-size: 80%; + vertical-align: 0.8em; + margin-right: -0.6em; + margin-left: .2em; + min-width: .5em; +} +.mq-math-mode .mq-paren { + padding: 0 .1em; + vertical-align: top; + -webkit-transform-origin: center .06em; + -moz-transform-origin: center .06em; + -ms-transform-origin: center .06em; + -o-transform-origin: center .06em; + transform-origin: center .06em; +} +.mq-math-mode .mq-paren.mq-ghost { + color: silver; +} +.mq-math-mode .mq-paren + span { + margin-top: .1em; + margin-bottom: .1em; +} +.mq-math-mode .mq-array { + vertical-align: middle; + text-align: center; +} +.mq-math-mode .mq-array > span { + display: block; +} +.mq-math-mode .mq-operator-name { + font-family: Symbola, "Times New Roman", serif; + line-height: .9; + font-style: normal; +} +.mq-math-mode var.mq-operator-name.mq-first { + padding-left: .2em; +} +.mq-math-mode var.mq-operator-name.mq-last { + padding-right: .2em; +} +.mq-math-mode .mq-fraction { + font-size: 90%; + text-align: center; + vertical-align: -0.4em; + padding: 0 .2em; +} +.mq-math-mode .mq-fraction, +.mq-math-mode .mq-large-operator, +.mq-math-mode x:-moz-any-link { + display: -moz-groupbox; +} +.mq-math-mode .mq-fraction, +.mq-math-mode .mq-large-operator, +.mq-math-mode x:-moz-any-link, +.mq-math-mode x:default { + display: inline-block; +} +.mq-math-mode .mq-numerator, +.mq-math-mode .mq-denominator { + display: block; +} +.mq-math-mode .mq-numerator { + padding: 0 0.1em; +} +.mq-math-mode .mq-denominator { + border-top: 1px solid; + float: right; + width: 100%; + padding: 0.1em; +} +.mq-math-mode .mq-sqrt-prefix { + padding-top: 0; + position: relative; + top: 0.1em; + vertical-align: top; + -webkit-transform-origin: top; + -moz-transform-origin: top; + -ms-transform-origin: top; + -o-transform-origin: top; + transform-origin: top; +} +.mq-math-mode .mq-sqrt-stem { + border-top: 1px solid; + margin-top: 1px; + padding-left: .15em; + padding-right: .2em; + margin-right: .1em; + padding-top: 1px; +} +.mq-math-mode .mq-vector-prefix { + display: block; + text-align: center; + line-height: .25em; + margin-bottom: -0.1em; + font-size: 0.75em; +} +.mq-math-mode .mq-vector-stem { + display: block; +} +.mq-math-mode .mq-large-operator { + text-align: center; +} +.mq-math-mode .mq-large-operator .mq-from, +.mq-math-mode .mq-large-operator big, +.mq-math-mode .mq-large-operator .mq-to { + display: block; +} +.mq-math-mode .mq-large-operator .mq-from, +.mq-math-mode .mq-large-operator .mq-to { + font-size: 80%; +} +.mq-math-mode .mq-large-operator .mq-from { + float: right; + /* take out of normal flow to manipulate baseline */ + width: 100%; +} +.mq-math-mode, +.mq-math-mode .mq-editable-field { + cursor: text; + font-family: Symbola, "Times New Roman", serif; +} +.mq-math-mode .mq-overarrow { + border-top: 1px solid black; + margin-top: 1px; + padding-top: 0.2em; +} +.mq-math-mode .mq-overarrow:before { + display: block; + position: relative; + top: -0.34em; + font-size: 0.5em; + line-height: 0em; + content: '\27A4'; + text-align: right; +} +.mq-math-mode .mq-overarrow.mq-arrow-left:before { + -moz-transform: scaleX(-1); + -o-transform: scaleX(-1); + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + filter: FlipH; + -ms-filter: "FlipH"; +} +.mq-math-mode .mq-selection, +.mq-editable-field .mq-selection, +.mq-math-mode .mq-selection .mq-non-leaf, +.mq-editable-field .mq-selection .mq-non-leaf, +.mq-math-mode .mq-selection .mq-scaled, +.mq-editable-field .mq-selection .mq-scaled { + background: #B4D5FE !important; + background: Highlight !important; + color: HighlightText; + border-color: HighlightText; +} +.mq-math-mode .mq-selection .mq-matrixed, +.mq-editable-field .mq-selection .mq-matrixed { + background: #39F !important; +} +.mq-math-mode .mq-selection .mq-matrixed-container, +.mq-editable-field .mq-selection .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='#3399FF') !important; +} +.mq-math-mode .mq-selection.mq-blur, +.mq-editable-field .mq-selection.mq-blur, +.mq-math-mode .mq-selection.mq-blur .mq-non-leaf, +.mq-editable-field .mq-selection.mq-blur .mq-non-leaf, +.mq-math-mode .mq-selection.mq-blur .mq-scaled, +.mq-editable-field .mq-selection.mq-blur .mq-scaled, +.mq-math-mode .mq-selection.mq-blur .mq-matrixed, +.mq-editable-field .mq-selection.mq-blur .mq-matrixed { + background: #D4D4D4 !important; + color: black; + border-color: black; +} +.mq-math-mode .mq-selection.mq-blur .mq-matrixed-container, +.mq-editable-field .mq-selection.mq-blur .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='#D4D4D4') !important; +} +.mq-editable-field .mq-textarea, +.mq-math-mode .mq-textarea { + position: relative; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; +} +.mq-editable-field .mq-textarea *, +.mq-math-mode .mq-textarea *, +.mq-editable-field .mq-selectable, +.mq-math-mode .mq-selectable { + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + position: absolute; + clip: rect(1em 1em 1em 1em); + -webkit-transform: scale(0); + -moz-transform: scale(0); + -ms-transform: scale(0); + -o-transform: scale(0); + transform: scale(0); + resize: none; + width: 1px; + height: 1px; +} +.mq-math-mode .mq-matrixed { + background: white; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='white'); + margin-top: -0.1em; +} diff --git a/htdocs/js/apps/MathQuill/mathquill-basic.js b/htdocs/js/apps/MathQuill/mathquill-basic.js new file mode 100644 index 0000000000..42a52d6c7a --- /dev/null +++ b/htdocs/js/apps/MathQuill/mathquill-basic.js @@ -0,0 +1,4121 @@ +/** + * MathQuill v0.10.1 http://mathquill.com + * by Han, Jeanine, and Mary maintainers@mathquill.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 = -1; +var 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; + + // To build the jquery collection for a fragment, accumulate elements + // into an array and then call jQ.add once on the result. jQ.add sorts the + // collection according to document order each time it is called, so + // building a collection by folding jQ.add directly takes more than + // quadratic time in the number of elements. + // + // https://github.com/jquery/jquery/blob/2.1.4/src/traversing.js#L112 + var accum = this.fold([], function (accum, el) { + accum.push.apply(accum, el.jQ.get()); + return accum; + }); + + this.jQ = this.jQ.add(accum); + }; + _.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) { + var oldParent = this.parent; + this.parent = parent; + this[dir] = withDir; + this[-dir] = oppDir; + // by contract, .blur() is called after all has been said and done + // and the cursor has actually been moved + if (oldParent !== parent && oldParent.blur) oldParent.blur(); + }; + _.insDirOf = function(dir, el) { + prayDirection(dir); + this.jQ.insDirOf(dir, el.jQ); + this.withDirInsertAt(dir, el.parent, el[dir], el); + this.parent.jQ.addClass('mq-hasCursor'); + 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.jQ.insAtDirEnd(dir, el.jQ); + this.withDirInsertAt(dir, el, 0, el.ends[dir]); + 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(root, container, options) { + this.id = root.id; + this.data = {}; + + this.root = root; + this.container = container; + this.options = options; + + root.controller = this; + + this.cursor = root.cursor = Cursor(root, options); + // TODO: stop depending on root.cursor, and rm it + }; + + _.handle = function(name, dir) { + var handlers = this.options.handlers; + if (handlers && handlers.fns[name]) { + var mq = handlers.APIClasses[this.KIND_OF_MQ](this); + if (dir === L || dir === R) handlers.fns[name](dir, mq); + else handlers.fns[name](mq); + } + }; + + 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. + ********************************************************/ + +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. 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\u20132015) \u2192 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\u20132015) \u2192 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 = + $('').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>/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); + }; + }); + 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'); + }; +} +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(e) { checkTextarea(e); }); + + + // -*- 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) 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; + + if (shouldBeSelected) checkTextareaFor(function(e) { + if (!(e && e.type === 'focusout') && textarea[0].select) { + 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 + }); + + handleKey(); + } + + 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) 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() { + return this.root.foldChildren('', function(text, child) { + return text + child.text(); + }); + }; +}); +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().endSelection(); + 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': + ctrlr.ctrlDeleteDir(L); + 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': + ctrlr.ctrlDeleteDir(R); + 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); + 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); + + 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; + } + }); + } + 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'); + + return this; + }; + _.ctrlDeleteDir = function(dir) { + prayDirection(dir); + var cursor = this.cursor; + if (!cursor[L] || cursor.selection) return ctrlr.deleteDir(); + + this.notify('edit'); + Fragment(cursor.parent.ends[L], cursor[L]).remove(); + cursor.insAtDirEnd(L, cursor.parent); + + if (cursor[L].siblingDeleted) cursor[L].siblingDeleted(cursor.options, R); + if (cursor[R].siblingDeleted) cursor[R].siblingDeleted(cursor.options, L); + cursor.parent.bubble('reflow'); + + 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 either MathCommands, or Fragments of MathCommands + // (either way, something that can be adopted by a MathBlock) + 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 + 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 $('