MDL-43856 MathJax: Add to standard plugins list
[moodle.git] / lib / editor / atto / plugins / equation / yui / src / button / js / button.js
CommitLineData
8bf5ad67
DW
1// This file is part of Moodle - http://moodle.org/
2//
3// Moodle is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// Moodle is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
15
16/**
62467795 17 * @package atto_equation
8bf5ad67
DW
18 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
19 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
20 */
62467795
AN
21
22/**
23 * Atto text editor equation plugin.
24 */
25
26/**
27 * Atto equation editor.
28 *
29 * @namespace M.atto_equation
30 * @class Button
31 * @extends M.editor_atto.EditorPlugin
32 */
33
34var COMPONENTNAME = 'atto_equation',
35 CSS = {
36 EQUATION_TEXT: 'atto_equation_equation',
37 EQUATION_PREVIEW: 'atto_equation_preview',
38 SUBMIT: 'atto_equation_submit',
39 LIBRARY: 'atto_equation_library',
40 LIBRARY_GROUP_PREFIX: 'atto_equation_library'
41 },
42 SELECTORS = {
43 LIBRARY_GROUP_PREFIX: '.' + CSS.LIBRARY_GROUP_PREFIX,
44 EQUATION_TEXT: '.' + CSS.EQUATION_TEXT,
45 EQUATION_PREVIEW: '.' + CSS.EQUATION_PREVIEW,
46 SUBMIT: '.' + CSS.SUBMIT,
47 LIBRARY_BUTTON: '.' + CSS.LIBRARY + ' button'
48 },
49 TEMPLATES = {
50 FORM: '' +
51 '<form class="atto_form">' +
52 '{{{library}}}' +
36beb828 53 '<label for="{{elementid}}_{{CSS.EQUATION_TEXT}}">{{{get_string "editequation" component texdocsurl}}}</label>' +
62467795 54 '<textarea class="fullwidth {{CSS.EQUATION_TEXT}}" id="{{elementid}}_{{CSS.EQUATION_TEXT}}" rows="8"></textarea><br/>' +
62467795
AN
55 '<label for="{{elementid}}_{{CSS.EQUATION_PREVIEW}}">{{get_string "preview" component}}</label>' +
56 '<div class="fullwidth {{CSS.EQUATION_PREVIEW}}" id="{{elementid}}_{{CSS.EQUATION_PREVIEW}}"></div>' +
57 '<div class="mdl-align">' +
58 '<br/>' +
59 '<button class="{{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' +
60 '</div>' +
61 '</form>',
62 LIBRARY: '' +
63 '<div class="{{CSS.LIBRARY}}">' +
64 '<ul>' +
65 '{{#each library}}' +
66 '<li><a href="#{{elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}{{@key}}">{{get_string groupname ../component}}</a></li>' +
67 '{{/each}}' +
68 '</ul>' +
69 '<div>' +
70 '{{#each library}}' +
71 '<div id="{{elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}{{@key}}">' +
72 '{{#split "\n" elements}}' +
73 '<button data-tex="{{this}}" title="{{this}}">$${{this}}$$</button>' +
74 '{{/split}}' +
75 '</div>' +
76 '{{/each}}' +
77 '</div>' +
78 '</div>'
79 };
80
81Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
8bf5ad67
DW
82
83 /**
84 * The selection object returned by the browser.
85 *
62467795 86 * @property _currentSelection
8bf5ad67
DW
87 * @type Range
88 * @default null
62467795 89 * @private
8bf5ad67 90 */
62467795 91 _currentSelection: null,
8bf5ad67
DW
92
93 /**
62467795 94 * The cursor position in the equation textarea.
8bf5ad67 95 *
62467795
AN
96 * @property _lastCursorPos
97 * @type Number
8bf5ad67 98 * @default 0
62467795 99 * @private
8bf5ad67 100 */
62467795 101 _lastCursorPos: 0,
8bf5ad67
DW
102
103 /**
62467795 104 * A reference to the dialogue content.
8bf5ad67 105 *
62467795
AN
106 * @property _content
107 * @type Node
108 * @private
8bf5ad67 109 */
62467795 110 _content: null,
8bf5ad67 111
62467795 112 initializer: function() {
8939ebac
DW
113 // If there is a tex filter active - or if MathJax is loaded in
114 // the page - enable this button.
115 if (this.get('texfilteractive') || (typeof MathJax !== "undefined")) {
62467795
AN
116 // Add the button to the toolbar.
117 this.addButton({
118 icon: 'e/math',
119 callback: this._displayDialogue
8bf5ad67
DW
120 });
121
62467795
AN
122 // We need custom highlight logic for this button.
123 this.get('host').on('atto:selectionchanged', function() {
124 if (this._resolveEquation()) {
125 this.highlightButtons();
126 } else {
127 this.unHighlightButtons();
128 }
129 }, this);
8bf5ad67
DW
130 }
131 },
132
133 /**
62467795 134 * Display the equation editor.
8bf5ad67 135 *
62467795
AN
136 * @method _displayDialogue
137 * @private
8bf5ad67 138 */
62467795
AN
139 _displayDialogue: function() {
140 this._currentSelection = this.get('host').getSelection();
8bf5ad67 141
62467795
AN
142 if (this._currentSelection === false) {
143 return;
144 }
8bf5ad67 145
62467795
AN
146 var dialogue = this.getDialogue({
147 headerContent: M.util.get_string('pluginname', COMPONENTNAME),
1eb5839c
FM
148 focusAfterHide: true,
149 width: 600
62467795
AN
150 });
151
152 var content = this._getDialogueContent();
153 dialogue.set('bodyContent', content);
154
155 var library = content.one(SELECTORS.LIBRARY_GROUP_PREFIX);
156
157 var tabview = new Y.TabView({
158 srcNode: library
159 });
160
161 tabview.render();
162 dialogue.show();
8939ebac
DW
163 // If MathJax is available, get it to render all the library buttons at once.
164 if (typeof MathJax !== "undefined") {
165 MathJax.Hub.Queue(["Typeset", MathJax.Hub, library.getDOMNode()]);
166 }
62467795
AN
167
168 var equation = this._resolveEquation();
169 if (equation) {
170 content.one(SELECTORS.EQUATION_TEXT).set('text', equation);
8bf5ad67 171 }
62467795 172 this._updatePreview(false);
8bf5ad67
DW
173 },
174
175 /**
176 * If there is selected text and it is part of an equation,
177 * extract the equation (and set it in the form).
178 *
62467795
AN
179 * @method _resolveEquation
180 * @private
3ee53a42 181 * @return {String|Boolean} The equation or false.
8bf5ad67 182 */
62467795
AN
183 _resolveEquation: function() {
184
8bf5ad67 185 // Find the equation in the surrounding text.
62467795 186 var selectedNode = this.get('host').getSelectionParentNode(),
8bf5ad67
DW
187 text,
188 equation;
189
190 // Note this is a document fragment and YUI doesn't like them.
62467795 191 if (!selectedNode) {
3ee53a42 192 return false;
8bf5ad67
DW
193 }
194
62467795 195 text = Y.one(selectedNode).get('text');
8bf5ad67
DW
196 // We use space or not space because . does not match new lines.
197 pattern = /\$\$[\S\s]*\$\$/;
198 equation = pattern.exec(text);
199 if (equation && equation.length) {
200 equation = equation.pop();
201 // Replace the equation.
202 equation = equation.substring(2, equation.length - 2);
3ee53a42 203 return equation;
8bf5ad67 204 }
3ee53a42 205 return false;
8bf5ad67
DW
206 },
207
208 /**
62467795 209 * Handle insertion of a new equation, or update of an existing one.
8bf5ad67 210 *
62467795
AN
211 * @method _setEquation
212 * @param {EventFacade} e
213 * @private
8bf5ad67 214 */
62467795 215 _setEquation: function(e) {
8bf5ad67 216 var input,
62467795 217 selectedNode,
8bf5ad67
DW
218 text,
219 pattern,
220 equation,
221 value;
222
62467795
AN
223 var host = this.get('host');
224
8bf5ad67 225 e.preventDefault();
62467795
AN
226 this.getDialogue({
227 focusAfterHide: null
228 }).hide();
8bf5ad67
DW
229
230 input = e.currentTarget.ancestor('.atto_form').one('textarea');
231
232 value = input.get('value');
233 if (value !== '') {
62467795
AN
234 host.setSelection(this._currentSelection);
235
8bf5ad67 236 value = '$$ ' + value.trim() + ' $$';
62467795
AN
237 selectedNode = Y.one(host.getSelectionParentNode());
238 text = selectedNode.get('text');
8bf5ad67
DW
239 pattern = /\$\$[\S\s]*\$\$/;
240 equation = pattern.exec(text);
241 if (equation && equation.length) {
242 // Replace the equation.
243 equation = equation.pop();
244 text = text.replace(equation, '$$' + value + '$$');
62467795 245 selectedNode.set('text', text);
8bf5ad67
DW
246 } else {
247 // Insert the new equation.
62467795 248 host.insertContentAtFocusPoint(value);
8bf5ad67
DW
249 }
250
251 // Clean the YUI ids from the HTML.
62467795 252 this.markUpdated();
8bf5ad67
DW
253 }
254 },
255
256 /**
257 * Update the preview div to match the current equation.
258 *
62467795
AN
259 * @param {EventFacade} e
260 * @method _updatePreview
261 * @private
8bf5ad67 262 */
62467795
AN
263 _updatePreview: function(e) {
264 var textarea = this._content.one(SELECTORS.EQUATION_TEXT),
265 equation = textarea.get('value'),
266 url,
267 preview,
268 currentPos = textarea.get('selectionStart'),
269 prefix = '',
270 cursorLatex = '\\square ',
271 isChar;
272
273
274 if (e) {
275 e.preventDefault();
276 }
277
278 if (!currentPos) {
279 currentPos = 0;
8bf5ad67
DW
280 }
281 // Move the cursor so it does not break expressions.
282 //
62467795
AN
283 while (equation.charAt(currentPos) === '\\' && currentPos > 0) {
284 currentPos -= 1;
8bf5ad67 285 }
62467795
AN
286 isChar = /[\w\{\}]/;
287 while (isChar.test(equation.charAt(currentPos)) && currentPos < equation.length) {
288 currentPos += 1;
8bf5ad67
DW
289 }
290 // Save the cursor position - for insertion from the library.
62467795
AN
291 this._lastCursorPos = currentPos;
292 equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos);
8939ebac
DW
293
294 var previewNode = this._content.one(SELECTORS.EQUATION_PREVIEW);
295 if (typeof MathJax !== "undefined") {
296 // If MathJax is available, get it to update the preview without an ajax request.
297 previewNode.setHTML('$$' + equation + '$$');
298 MathJax.Hub.Queue(["Typeset", MathJax.Hub, previewNode.getDOMNode()]);
299 } else {
300 // Fall back on an ajax request to the filter.
301 url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
302 params = {
303 sesskey: M.cfg.sesskey,
304 contextid: this.get('contextid'),
305 action: 'filtertext',
306 text: '$$ ' + equation + ' $$'
307 };
308
309 preview = Y.io(url, { sync: true,
310 data: params });
311 if (preview.status === 200) {
312 previewNode.setHTML(preview.responseText);
313 }
8bf5ad67
DW
314 }
315 },
316
317 /**
62467795
AN
318 * Return the dialogue content for the tool, attaching any required
319 * events.
8bf5ad67 320 *
62467795
AN
321 * @method _getDialogueContent
322 * @return {Node}
323 * @private
8bf5ad67 324 */
62467795
AN
325 _getDialogueContent: function() {
326 var library = this._getLibraryContent(),
327 template = Y.Handlebars.compile(TEMPLATES.FORM);
328
329 this._content = Y.Node.create(template({
330 elementid: this.get('host').get('elementid'),
331 component: COMPONENTNAME,
332 library: library,
36beb828 333 texdocsurl: this.get('texdocsurl'),
62467795
AN
334 CSS: CSS
335 }));
336
337 this._content.one(SELECTORS.SUBMIT).on('click', this._setEquation, this);
338 this._content.one(SELECTORS.EQUATION_TEXT).on('valuechange', this._updatePreview, this);
339 this._content.one(SELECTORS.EQUATION_TEXT).on('mouseup', this._updatePreview, this);
340 this._content.one(SELECTORS.EQUATION_TEXT).on('keyup', this._updatePreview, this);
341 this._content.delegate('click', this._selectLibraryItem, SELECTORS.LIBRARY_BUTTON, this);
342
343 return this._content;
8bf5ad67
DW
344 },
345
346 /**
62467795 347 * Reponse to button presses in the TeX library panels.
8bf5ad67 348 *
62467795
AN
349 * @method _selectLibraryItem
350 * @param {EventFacade} e
351 * @return {string}
352 * @private
8bf5ad67 353 */
62467795
AN
354 _selectLibraryItem: function(e) {
355 var tex = e.currentTarget.getAttribute('data-tex');
8bf5ad67 356
62467795 357 e.preventDefault();
8bf5ad67 358
62467795 359 input = e.currentTarget.ancestor('.atto_form').one('textarea');
8bf5ad67
DW
360
361 value = input.get('value');
362
62467795 363 value = value.substring(0, this._lastCursorPos) + tex + value.substring(this._lastCursorPos, value.length);
8bf5ad67
DW
364
365 input.set('value', value);
8bf5ad67 366 input.focus();
9ee8a359 367
62467795 368 var focusPoint = this._lastCursorPos + tex.length,
9ee8a359
AN
369 realInput = input.getDOMNode();
370 if (typeof realInput.selectionStart === "number") {
371 // Modern browsers have selectionStart and selectionEnd to control the cursor position.
372 realInput.selectionStart = realInput.selectionEnd = focusPoint;
373 } else if (typeof realInput.createTextRange !== "undefined") {
374 // Legacy browsers (IE<=9) use createTextRange().
375 var range = realInput.createTextRange();
376 range.moveToPoint(focusPoint);
377 range.select();
378 }
379 // Focus must be set before updating the preview for the cursor box to be in the correct location.
62467795 380 this._updatePreview(false);
8bf5ad67
DW
381 },
382
383 /**
384 * Return the HTML for rendering the library of predefined buttons.
385 *
62467795
AN
386 * @method _getLibraryContent
387 * @return {string}
388 * @private
8bf5ad67 389 */
62467795
AN
390 _getLibraryContent: function() {
391 var template = Y.Handlebars.compile(TEMPLATES.LIBRARY),
392 library = this.get('library'),
393 content = '';
394
395 // Helper to iterate over a newline separated string.
396 Y.Handlebars.registerHelper('split', function(delimiter, str, options) {
397 var parts,
398 current,
399 out;
400 if (typeof delimiter === "undefined" || typeof str === "undefined") {
401 Y.log('Handlebars split helper: String and delimiter are required.', 'debug', 'moodle-atto_equation-button');
402 return '';
8bf5ad67 403 }
62467795
AN
404
405 out = '';
406 parts = str.trim().split(delimiter);
407 while (parts.length > 0) {
408 current = parts.shift();
409 out += options.fn(current);
410 }
411
412 return out;
413 });
414 content = template({
415 elementid: this.get('host').get('elementid'),
416 component: COMPONENTNAME,
417 library: library,
418 CSS: CSS
419 });
8bf5ad67 420
8939ebac
DW
421 // Only render the library via ajax is MathJax is not available.
422 if (typeof MathJax === "undefined") {
423 var url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
424 var params = {
425 sesskey: M.cfg.sesskey,
426 contextid: this.get('contextid'),
427 action: 'filtertext',
428 text: content
429 };
430
431 preview = Y.io(url, {
432 sync: true,
433 data: params,
434 method: 'POST'
435 });
8bf5ad67 436
8939ebac
DW
437 if (preview.status === 200) {
438 content = preview.responseText;
439 }
8bf5ad67
DW
440 }
441 return content;
442 }
62467795
AN
443}, {
444 ATTRS: {
445 /**
446 * Whether the TeX filter is currently active.
447 *
448 * @attribute texfilteractive
449 * @type Boolean
450 */
451 texfilteractive: {
452 value: false
453 },
454 /**
455 * The contextid to use when generating this preview.
456 *
457 * @attribute contextid
458 * @type String
459 */
460 contextid: {
461 value: null
462 },
463
464 /**
465 * The content of the example library.
466 *
467 * @attribute library
468 * @type object
469 */
470 library: {
471 value: {}
36beb828
FM
472 },
473
474 /**
475 * The link to the Moodle Docs page about TeX.
476 *
477 * @attribute texdocsurl
478 * @type string
479 */
480 texdocsurl: {
481 value: null
62467795
AN
482 }
483 }
484});