MDL-44767 atto_editor: Browser hangs when making a selection
[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}}}' +
53 '<label for="{{elementid}}_{{CSS.EQUATION_TEXT}}">{{get_string "editequation" component}}</label>' +
54 '<textarea class="fullwidth {{CSS.EQUATION_TEXT}}" id="{{elementid}}_{{CSS.EQUATION_TEXT}}" rows="8"></textarea><br/>' +
55 '<p>{{{get_string "editequation_desc" component}}}</p>' +
56 '<label for="{{elementid}}_{{CSS.EQUATION_PREVIEW}}">{{get_string "preview" component}}</label>' +
57 '<div class="fullwidth {{CSS.EQUATION_PREVIEW}}" id="{{elementid}}_{{CSS.EQUATION_PREVIEW}}"></div>' +
58 '<div class="mdl-align">' +
59 '<br/>' +
60 '<button class="{{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' +
61 '</div>' +
62 '</form>',
63 LIBRARY: '' +
64 '<div class="{{CSS.LIBRARY}}">' +
65 '<ul>' +
66 '{{#each library}}' +
67 '<li><a href="#{{elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}{{@key}}">{{get_string groupname ../component}}</a></li>' +
68 '{{/each}}' +
69 '</ul>' +
70 '<div>' +
71 '{{#each library}}' +
72 '<div id="{{elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}{{@key}}">' +
73 '{{#split "\n" elements}}' +
74 '<button data-tex="{{this}}" title="{{this}}">$${{this}}$$</button>' +
75 '{{/split}}' +
76 '</div>' +
77 '{{/each}}' +
78 '</div>' +
79 '</div>'
80 };
81
82Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
8bf5ad67
DW
83
84 /**
85 * The selection object returned by the browser.
86 *
62467795 87 * @property _currentSelection
8bf5ad67
DW
88 * @type Range
89 * @default null
62467795 90 * @private
8bf5ad67 91 */
62467795 92 _currentSelection: null,
8bf5ad67
DW
93
94 /**
62467795 95 * The cursor position in the equation textarea.
8bf5ad67 96 *
62467795
AN
97 * @property _lastCursorPos
98 * @type Number
8bf5ad67 99 * @default 0
62467795 100 * @private
8bf5ad67 101 */
62467795 102 _lastCursorPos: 0,
8bf5ad67
DW
103
104 /**
62467795 105 * A reference to the dialogue content.
8bf5ad67 106 *
62467795
AN
107 * @property _content
108 * @type Node
109 * @private
8bf5ad67 110 */
62467795 111 _content: null,
8bf5ad67 112
62467795
AN
113 initializer: function() {
114 if (this.get('texfilteractive')) {
115 // Add the button to the toolbar.
116 this.addButton({
117 icon: 'e/math',
118 callback: this._displayDialogue
8bf5ad67
DW
119 });
120
62467795
AN
121 // We need custom highlight logic for this button.
122 this.get('host').on('atto:selectionchanged', function() {
123 if (this._resolveEquation()) {
124 this.highlightButtons();
125 } else {
126 this.unHighlightButtons();
127 }
128 }, this);
8bf5ad67
DW
129 }
130 },
131
132 /**
62467795 133 * Display the equation editor.
8bf5ad67 134 *
62467795
AN
135 * @method _displayDialogue
136 * @private
8bf5ad67 137 */
62467795
AN
138 _displayDialogue: function() {
139 this._currentSelection = this.get('host').getSelection();
8bf5ad67 140
62467795
AN
141 if (this._currentSelection === false) {
142 return;
143 }
8bf5ad67 144
62467795
AN
145 var dialogue = this.getDialogue({
146 headerContent: M.util.get_string('pluginname', COMPONENTNAME),
147 focusAfterHide: true
148 });
149
150 var content = this._getDialogueContent();
151 dialogue.set('bodyContent', content);
152
153 var library = content.one(SELECTORS.LIBRARY_GROUP_PREFIX);
154
155 var tabview = new Y.TabView({
156 srcNode: library
157 });
158
159 tabview.render();
160 dialogue.show();
161
162 var equation = this._resolveEquation();
163 if (equation) {
164 content.one(SELECTORS.EQUATION_TEXT).set('text', equation);
8bf5ad67 165 }
62467795 166 this._updatePreview(false);
8bf5ad67
DW
167 },
168
169 /**
170 * If there is selected text and it is part of an equation,
171 * extract the equation (and set it in the form).
172 *
62467795
AN
173 * @method _resolveEquation
174 * @private
3ee53a42 175 * @return {String|Boolean} The equation or false.
8bf5ad67 176 */
62467795
AN
177 _resolveEquation: function() {
178
8bf5ad67 179 // Find the equation in the surrounding text.
62467795 180 var selectedNode = this.get('host').getSelectionParentNode(),
8bf5ad67
DW
181 text,
182 equation;
183
184 // Note this is a document fragment and YUI doesn't like them.
62467795 185 if (!selectedNode) {
3ee53a42 186 return false;
8bf5ad67
DW
187 }
188
62467795 189 text = Y.one(selectedNode).get('text');
8bf5ad67
DW
190 // We use space or not space because . does not match new lines.
191 pattern = /\$\$[\S\s]*\$\$/;
192 equation = pattern.exec(text);
193 if (equation && equation.length) {
194 equation = equation.pop();
195 // Replace the equation.
196 equation = equation.substring(2, equation.length - 2);
3ee53a42 197 return equation;
8bf5ad67 198 }
3ee53a42 199 return false;
8bf5ad67
DW
200 },
201
202 /**
62467795 203 * Handle insertion of a new equation, or update of an existing one.
8bf5ad67 204 *
62467795
AN
205 * @method _setEquation
206 * @param {EventFacade} e
207 * @private
8bf5ad67 208 */
62467795 209 _setEquation: function(e) {
8bf5ad67 210 var input,
62467795 211 selectedNode,
8bf5ad67
DW
212 text,
213 pattern,
214 equation,
215 value;
216
62467795
AN
217 var host = this.get('host');
218
8bf5ad67 219 e.preventDefault();
62467795
AN
220 this.getDialogue({
221 focusAfterHide: null
222 }).hide();
8bf5ad67
DW
223
224 input = e.currentTarget.ancestor('.atto_form').one('textarea');
225
226 value = input.get('value');
227 if (value !== '') {
62467795
AN
228 host.setSelection(this._currentSelection);
229
8bf5ad67 230 value = '$$ ' + value.trim() + ' $$';
62467795
AN
231 selectedNode = Y.one(host.getSelectionParentNode());
232 text = selectedNode.get('text');
8bf5ad67
DW
233 pattern = /\$\$[\S\s]*\$\$/;
234 equation = pattern.exec(text);
235 if (equation && equation.length) {
236 // Replace the equation.
237 equation = equation.pop();
238 text = text.replace(equation, '$$' + value + '$$');
62467795 239 selectedNode.set('text', text);
8bf5ad67
DW
240 } else {
241 // Insert the new equation.
62467795 242 host.insertContentAtFocusPoint(value);
8bf5ad67
DW
243 }
244
245 // Clean the YUI ids from the HTML.
62467795 246 this.markUpdated();
8bf5ad67
DW
247 }
248 },
249
250 /**
251 * Update the preview div to match the current equation.
252 *
62467795
AN
253 * @param {EventFacade} e
254 * @method _updatePreview
255 * @private
8bf5ad67 256 */
62467795
AN
257 _updatePreview: function(e) {
258 var textarea = this._content.one(SELECTORS.EQUATION_TEXT),
259 equation = textarea.get('value'),
260 url,
261 preview,
262 currentPos = textarea.get('selectionStart'),
263 prefix = '',
264 cursorLatex = '\\square ',
265 isChar;
266
267
268 if (e) {
269 e.preventDefault();
270 }
271
272 if (!currentPos) {
273 currentPos = 0;
8bf5ad67
DW
274 }
275 // Move the cursor so it does not break expressions.
276 //
62467795
AN
277 while (equation.charAt(currentPos) === '\\' && currentPos > 0) {
278 currentPos -= 1;
8bf5ad67 279 }
62467795
AN
280 isChar = /[\w\{\}]/;
281 while (isChar.test(equation.charAt(currentPos)) && currentPos < equation.length) {
282 currentPos += 1;
8bf5ad67
DW
283 }
284 // Save the cursor position - for insertion from the library.
62467795
AN
285 this._lastCursorPos = currentPos;
286 equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos);
8bf5ad67
DW
287 url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
288 params = {
289 sesskey: M.cfg.sesskey,
62467795
AN
290 contextid: this.get('contextid'),
291 action: 'filtertext',
292 text: '$$ ' + equation + ' $$'
8bf5ad67
DW
293 };
294
8bf5ad67
DW
295 preview = Y.io(url, { sync: true,
296 data: params });
297 if (preview.status === 200) {
62467795 298 this._content.one(SELECTORS.EQUATION_PREVIEW).setHTML(preview.responseText);
8bf5ad67
DW
299 }
300 },
301
302 /**
62467795
AN
303 * Return the dialogue content for the tool, attaching any required
304 * events.
8bf5ad67 305 *
62467795
AN
306 * @method _getDialogueContent
307 * @return {Node}
308 * @private
8bf5ad67 309 */
62467795
AN
310 _getDialogueContent: function() {
311 var library = this._getLibraryContent(),
312 template = Y.Handlebars.compile(TEMPLATES.FORM);
313
314 this._content = Y.Node.create(template({
315 elementid: this.get('host').get('elementid'),
316 component: COMPONENTNAME,
317 library: library,
318 CSS: CSS
319 }));
320
321 this._content.one(SELECTORS.SUBMIT).on('click', this._setEquation, this);
322 this._content.one(SELECTORS.EQUATION_TEXT).on('valuechange', this._updatePreview, this);
323 this._content.one(SELECTORS.EQUATION_TEXT).on('mouseup', this._updatePreview, this);
324 this._content.one(SELECTORS.EQUATION_TEXT).on('keyup', this._updatePreview, this);
325 this._content.delegate('click', this._selectLibraryItem, SELECTORS.LIBRARY_BUTTON, this);
326
327 return this._content;
8bf5ad67
DW
328 },
329
330 /**
62467795 331 * Reponse to button presses in the TeX library panels.
8bf5ad67 332 *
62467795
AN
333 * @method _selectLibraryItem
334 * @param {EventFacade} e
335 * @return {string}
336 * @private
8bf5ad67 337 */
62467795
AN
338 _selectLibraryItem: function(e) {
339 var tex = e.currentTarget.getAttribute('data-tex');
8bf5ad67 340
62467795 341 e.preventDefault();
8bf5ad67 342
62467795 343 input = e.currentTarget.ancestor('.atto_form').one('textarea');
8bf5ad67
DW
344
345 value = input.get('value');
346
62467795 347 value = value.substring(0, this._lastCursorPos) + tex + value.substring(this._lastCursorPos, value.length);
8bf5ad67
DW
348
349 input.set('value', value);
8bf5ad67 350 input.focus();
9ee8a359 351
62467795 352 var focusPoint = this._lastCursorPos + tex.length,
9ee8a359
AN
353 realInput = input.getDOMNode();
354 if (typeof realInput.selectionStart === "number") {
355 // Modern browsers have selectionStart and selectionEnd to control the cursor position.
356 realInput.selectionStart = realInput.selectionEnd = focusPoint;
357 } else if (typeof realInput.createTextRange !== "undefined") {
358 // Legacy browsers (IE<=9) use createTextRange().
359 var range = realInput.createTextRange();
360 range.moveToPoint(focusPoint);
361 range.select();
362 }
363 // Focus must be set before updating the preview for the cursor box to be in the correct location.
62467795 364 this._updatePreview(false);
8bf5ad67
DW
365 },
366
367 /**
368 * Return the HTML for rendering the library of predefined buttons.
369 *
62467795
AN
370 * @method _getLibraryContent
371 * @return {string}
372 * @private
8bf5ad67 373 */
62467795
AN
374 _getLibraryContent: function() {
375 var template = Y.Handlebars.compile(TEMPLATES.LIBRARY),
376 library = this.get('library'),
377 content = '';
378
379 // Helper to iterate over a newline separated string.
380 Y.Handlebars.registerHelper('split', function(delimiter, str, options) {
381 var parts,
382 current,
383 out;
384 if (typeof delimiter === "undefined" || typeof str === "undefined") {
385 Y.log('Handlebars split helper: String and delimiter are required.', 'debug', 'moodle-atto_equation-button');
386 return '';
8bf5ad67 387 }
62467795
AN
388
389 out = '';
390 parts = str.trim().split(delimiter);
391 while (parts.length > 0) {
392 current = parts.shift();
393 out += options.fn(current);
394 }
395
396 return out;
397 });
398 content = template({
399 elementid: this.get('host').get('elementid'),
400 component: COMPONENTNAME,
401 library: library,
402 CSS: CSS
403 });
8bf5ad67
DW
404
405 var url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
406 var params = {
407 sesskey: M.cfg.sesskey,
62467795
AN
408 contextid: this.get('contextid'),
409 action: 'filtertext',
410 text: content
8bf5ad67
DW
411 };
412
62467795
AN
413 preview = Y.io(url, {
414 sync: true,
415 data: params,
416 method: 'POST'
417 });
8bf5ad67
DW
418
419 if (preview.status === 200) {
420 content = preview.responseText;
421 }
422 return content;
423 }
62467795
AN
424}, {
425 ATTRS: {
426 /**
427 * Whether the TeX filter is currently active.
428 *
429 * @attribute texfilteractive
430 * @type Boolean
431 */
432 texfilteractive: {
433 value: false
434 },
435 /**
436 * The contextid to use when generating this preview.
437 *
438 * @attribute contextid
439 * @type String
440 */
441 contextid: {
442 value: null
443 },
444
445 /**
446 * The content of the example library.
447 *
448 * @attribute library
449 * @type object
450 */
451 library: {
452 value: {}
453 }
454 }
455});