weekly release 3.6dev
[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 */
62467795 33var COMPONENTNAME = 'atto_equation',
050159dc 34 LOGNAME = 'atto_equation',
62467795
AN
35 CSS = {
36 EQUATION_TEXT: 'atto_equation_equation',
37 EQUATION_PREVIEW: 'atto_equation_preview',
38 SUBMIT: 'atto_equation_submit',
39 LIBRARY: 'atto_equation_library',
050159dc
FM
40 LIBRARY_GROUPS: 'atto_equation_groups',
41 LIBRARY_GROUP_PREFIX: 'atto_equation_group'
62467795
AN
42 },
43 SELECTORS = {
050159dc
FM
44 LIBRARY: '.' + CSS.LIBRARY,
45 LIBRARY_GROUP: '.' + CSS.LIBRARY_GROUPS + ' > div > div',
62467795
AN
46 EQUATION_TEXT: '.' + CSS.EQUATION_TEXT,
47 EQUATION_PREVIEW: '.' + CSS.EQUATION_PREVIEW,
48 SUBMIT: '.' + CSS.SUBMIT,
49 LIBRARY_BUTTON: '.' + CSS.LIBRARY + ' button'
50 },
cc90cedc
DW
51 DELIMITERS = {
52 START: '\\(',
53 END: '\\)'
54 },
62467795
AN
55 TEMPLATES = {
56 FORM: '' +
57 '<form class="atto_form">' +
58 '{{{library}}}' +
36beb828 59 '<label for="{{elementid}}_{{CSS.EQUATION_TEXT}}">{{{get_string "editequation" component texdocsurl}}}</label>' +
557f44d9
AN
60 '<textarea class="fullwidth {{CSS.EQUATION_TEXT}}" ' +
61 'id="{{elementid}}_{{CSS.EQUATION_TEXT}}" rows="8"></textarea><br/>' +
62467795 62 '<label for="{{elementid}}_{{CSS.EQUATION_PREVIEW}}">{{get_string "preview" component}}</label>' +
557f44d9
AN
63 '<div describedby="{{elementid}}_cursorinfo" class="well well-small fullwidth {{CSS.EQUATION_PREVIEW}}" ' +
64 'id="{{elementid}}_{{CSS.EQUATION_PREVIEW}}"></div>' +
24183668 65 '<div id="{{elementid}}_cursorinfo">{{get_string "cursorinfo" component}}</div>' +
62467795
AN
66 '<div class="mdl-align">' +
67 '<br/>' +
68 '<button class="{{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' +
69 '</div>' +
70 '</form>',
71 LIBRARY: '' +
72 '<div class="{{CSS.LIBRARY}}">' +
73 '<ul>' +
74 '{{#each library}}' +
050159dc
FM
75 '<li><a href="#{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' +
76 '{{get_string groupname ../component}}' +
77 '</a></li>' +
62467795
AN
78 '{{/each}}' +
79 '</ul>' +
050159dc 80 '<div class="{{CSS.LIBRARY_GROUPS}}">' +
62467795 81 '{{#each library}}' +
050159dc
FM
82 '<div id="{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' +
83 '<div role="toolbar">' +
84 '{{#split "\n" elements}}' +
0610d2ca
DW
85 '<button tabindex="-1" data-tex="{{this}}" aria-label="{{this}}" title="{{this}}">' +
86 '{{../../DELIMITERS.START}}{{this}}{{../../DELIMITERS.END}}' +
87 '</button>' +
050159dc
FM
88 '{{/split}}' +
89 '</div>' +
62467795
AN
90 '</div>' +
91 '{{/each}}' +
92 '</div>' +
93 '</div>'
94 };
95
96Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
8bf5ad67
DW
97
98 /**
99 * The selection object returned by the browser.
100 *
62467795 101 * @property _currentSelection
8bf5ad67
DW
102 * @type Range
103 * @default null
62467795 104 * @private
8bf5ad67 105 */
62467795 106 _currentSelection: null,
8bf5ad67
DW
107
108 /**
62467795 109 * The cursor position in the equation textarea.
8bf5ad67 110 *
62467795
AN
111 * @property _lastCursorPos
112 * @type Number
8bf5ad67 113 * @default 0
62467795 114 * @private
8bf5ad67 115 */
62467795 116 _lastCursorPos: 0,
8bf5ad67
DW
117
118 /**
62467795 119 * A reference to the dialogue content.
8bf5ad67 120 *
62467795
AN
121 * @property _content
122 * @type Node
123 * @private
8bf5ad67 124 */
62467795 125 _content: null,
8bf5ad67 126
cc90cedc
DW
127 /**
128 * The source equation we are editing in the text.
129 *
130 * @property _sourceEquation
ea382dea 131 * @type Object
cc90cedc
DW
132 * @private
133 */
ea382dea 134 _sourceEquation: null,
cc90cedc 135
050159dc
FM
136 /**
137 * A reference to the tab focus set on each group.
138 *
139 * The keys are the IDs of the group, the value is the Node on which the focus is set.
140 *
141 * @property _groupFocus
142 * @type Object
143 * @private
144 */
145 _groupFocus: null,
146
ea382dea
AN
147 /**
148 * Regular Expression patterns used to pick out the equations in a String.
149 *
150 * @property _equationPatterns
151 * @type Array
152 * @private
153 */
154 _equationPatterns: [
155 // We use space or not space because . does not match new lines.
156 // $$ blah $$.
157 /\$\$([\S\s]+?)\$\$/,
158 // E.g. "\( blah \)".
159 /\\\(([\S\s]+?)\\\)/,
160 // E.g. "\[ blah \]".
161 /\\\[([\S\s]+?)\\\]/,
162 // E.g. "[tex] blah [/tex]".
163 /\[tex\]([\S\s]+?)\[\/tex\]/
164 ],
165
62467795 166 initializer: function() {
050159dc 167 this._groupFocus = {};
0610d2ca 168
cc90cedc
DW
169 // If there is a tex filter active - enable this button.
170 if (this.get('texfilteractive')) {
62467795
AN
171 // Add the button to the toolbar.
172 this.addButton({
173 icon: 'e/math',
174 callback: this._displayDialogue
8bf5ad67
DW
175 });
176
62467795
AN
177 // We need custom highlight logic for this button.
178 this.get('host').on('atto:selectionchanged', function() {
179 if (this._resolveEquation()) {
180 this.highlightButtons();
181 } else {
182 this.unHighlightButtons();
183 }
184 }, this);
cc90cedc
DW
185
186 // We need to convert these to a non dom node based format.
5bb4f444 187 this.editor.all('tex').each(function(texNode) {
557f44d9
AN
188 var replacement = Y.Node.create('<span>' +
189 DELIMITERS.START + ' ' + texNode.get('text') + ' ' + DELIMITERS.END +
190 '</span>');
cc90cedc
DW
191 texNode.replace(replacement);
192 });
8bf5ad67 193 }
cc90cedc 194
8bf5ad67
DW
195 },
196
197 /**
62467795 198 * Display the equation editor.
8bf5ad67 199 *
62467795
AN
200 * @method _displayDialogue
201 * @private
8bf5ad67 202 */
62467795
AN
203 _displayDialogue: function() {
204 this._currentSelection = this.get('host').getSelection();
8bf5ad67 205
62467795
AN
206 if (this._currentSelection === false) {
207 return;
208 }
8bf5ad67 209
aca67017
DW
210 // This needs to be done before the dialogue is opened because the focus will shift to the dialogue.
211 var equation = this._resolveEquation();
212
62467795
AN
213 var dialogue = this.getDialogue({
214 headerContent: M.util.get_string('pluginname', COMPONENTNAME),
1eb5839c 215 focusAfterHide: true,
e5ddec38 216 width: 600,
c1660772 217 focusOnShowSelector: SELECTORS.EQUATION_TEXT
62467795
AN
218 });
219
220 var content = this._getDialogueContent();
221 dialogue.set('bodyContent', content);
222
050159dc 223 var library = content.one(SELECTORS.LIBRARY);
62467795
AN
224
225 var tabview = new Y.TabView({
226 srcNode: library
227 });
228
229 tabview.render();
230 dialogue.show();
28de7771
DW
231 // Notify the filters about the modified nodes.
232 require(['core/event'], function(event) {
233 event.notifyFilterContentUpdated(dialogue.get('boundingBox').getDOMNode());
234 });
62467795 235
62467795
AN
236 if (equation) {
237 content.one(SELECTORS.EQUATION_TEXT).set('text', equation);
8bf5ad67 238 }
62467795 239 this._updatePreview(false);
8bf5ad67
DW
240 },
241
242 /**
243 * If there is selected text and it is part of an equation,
244 * extract the equation (and set it in the form).
245 *
62467795
AN
246 * @method _resolveEquation
247 * @private
3ee53a42 248 * @return {String|Boolean} The equation or false.
8bf5ad67 249 */
62467795
AN
250 _resolveEquation: function() {
251
8bf5ad67 252 // Find the equation in the surrounding text.
62467795 253 var selectedNode = this.get('host').getSelectionParentNode(),
ea382dea 254 selection = this.get('host').getSelection(),
8bf5ad67 255 text,
ea382dea
AN
256 returnValue = false;
257
aca67017
DW
258 // Prevent resolving equations when we don't have focus.
259 if (!this.get('host').isActive()) {
260 return false;
261 }
8bf5ad67
DW
262
263 // Note this is a document fragment and YUI doesn't like them.
62467795 264 if (!selectedNode) {
3ee53a42 265 return false;
8bf5ad67
DW
266 }
267
ea382dea
AN
268 // We don't yet have a cursor selection somehow so we can't possible be resolving an equation that has selection.
269 if (!selection || selection.length === 0) {
270 return false;
271 }
aca67017
DW
272
273 this.sourceEquation = null;
274
ea382dea 275 selection = selection[0];
cc90cedc 276
ea382dea 277 text = Y.one(selectedNode).get('text');
cc90cedc 278
557f44d9
AN
279 // For each of these patterns we have a RegExp which captures the inner component of the equation but also
280 // includes the delimiters.
ea382dea
AN
281 // We first run the RegExp adding the global flag ("g"). This ignores the capture, instead matching the entire
282 // equation including delimiters and returning one entry per match of the whole equation.
283 // We have to deal with multiple occurences of the same equation in a String so must be able to loop on the
284 // match results.
285 Y.Array.find(this._equationPatterns, function(pattern) {
286 // For each pattern in turn, find all whole matches (including the delimiters).
287 var patternMatches = text.match(new RegExp(pattern.source, "g"));
288
289 if (patternMatches && patternMatches.length) {
290 // This pattern matches at least once. See if this pattern matches our current position.
291 // Note: We return here to break the Y.Array.find loop - any truthy return will stop any subsequent
292 // searches which is the required behaviour of this function.
293 return Y.Array.find(patternMatches, function(match) {
294 // Check each occurrence of this match.
295 var startIndex = 0;
5bb4f444 296 while (text.indexOf(match, startIndex) !== -1) {
ea382dea
AN
297 // Determine whether the cursor is in the current occurrence of this string.
298 // Note: We do not support a selection exceeding the bounds of an equation.
299 var startOuter = text.indexOf(match, startIndex),
300 endOuter = startOuter + match.length,
301 startMatch = (selection.startOffset >= startOuter && selection.startOffset < endOuter),
302 endMatch = (selection.endOffset <= endOuter && selection.endOffset > startOuter);
303
304 if (startMatch && endMatch) {
305 // This match is in our current position - fetch the innerMatch data.
306 var innerMatch = match.match(pattern);
307 if (innerMatch && innerMatch.length) {
308 // We need the start and end of the inner match for later.
309 var startInner = text.indexOf(innerMatch[1], startOuter),
310 endInner = startInner + innerMatch[1].length;
311
312 // We'll be returning the inner match for use in the editor itself.
313 returnValue = innerMatch[1];
314
315 // Save all data for later.
316 this.sourceEquation = {
317 // Outer match data.
318 startOuterPosition: startOuter,
319 endOuterPosition: endOuter,
320 outerMatch: match,
321
322 // Inner match data.
323 startInnerPosition: startInner,
324 endInnerPosition: endInner,
325 innerMatch: innerMatch
326 };
327
328 // This breaks out of both Y.Array.find functions.
329 return true;
330 }
331 }
332
333 // Update the startIndex to match the end of the current match so that we can continue hunting
334 // for further matches.
335 startIndex = endOuter;
336 }
337 }, this);
cc90cedc 338 }
ea382dea 339 }, this);
cc90cedc 340
aca67017
DW
341 // We trim the equation when we load it and then add spaces when we save it.
342 if (returnValue !== false) {
343 returnValue = returnValue.trim();
344 }
ea382dea 345 return returnValue;
8bf5ad67
DW
346 },
347
348 /**
62467795 349 * Handle insertion of a new equation, or update of an existing one.
8bf5ad67 350 *
62467795
AN
351 * @method _setEquation
352 * @param {EventFacade} e
353 * @private
8bf5ad67 354 */
62467795 355 _setEquation: function(e) {
8bf5ad67 356 var input,
62467795 357 selectedNode,
8bf5ad67 358 text,
cc90cedc 359 value,
557f44d9
AN
360 host,
361 newText;
8bf5ad67 362
cc90cedc 363 host = this.get('host');
62467795 364
8bf5ad67 365 e.preventDefault();
62467795
AN
366 this.getDialogue({
367 focusAfterHide: null
368 }).hide();
8bf5ad67
DW
369
370 input = e.currentTarget.ancestor('.atto_form').one('textarea');
371
372 value = input.get('value');
373 if (value !== '') {
62467795
AN
374 host.setSelection(this._currentSelection);
375
ea382dea 376 if (this.sourceEquation) {
8bf5ad67 377 // Replace the equation.
cc90cedc
DW
378 selectedNode = Y.one(host.getSelectionParentNode());
379 text = selectedNode.get('text');
aca67017 380 value = ' ' + value + ' ';
5bb4f444 381 newText = text.slice(0, this.sourceEquation.startInnerPosition) +
ea382dea
AN
382 value +
383 text.slice(this.sourceEquation.endInnerPosition);
cc90cedc 384
ea382dea 385 selectedNode.set('text', newText);
8bf5ad67
DW
386 } else {
387 // Insert the new equation.
cc90cedc 388 value = DELIMITERS.START + ' ' + value + ' ' + DELIMITERS.END;
62467795 389 host.insertContentAtFocusPoint(value);
8bf5ad67
DW
390 }
391
392 // Clean the YUI ids from the HTML.
62467795 393 this.markUpdated();
8bf5ad67
DW
394 }
395 },
396
cc90cedc
DW
397 /**
398 * Smart throttle, only call a function every delay milli seconds,
441f94b2
DW
399 * and always run the last call. Y.throttle does not work here,
400 * because it calls the function immediately, the first time, and then
401 * ignores repeated calls within X seconds. This does not guarantee
402 * that the last call will be executed (which is required here).
cc90cedc
DW
403 *
404 * @param {function} fn
441f94b2 405 * @param {Number} delay Delay in milliseconds
cc90cedc
DW
406 * @method _throttle
407 * @private
408 */
409 _throttle: function(fn, delay) {
410 var timer = null;
5bb4f444 411 return function() {
cc90cedc
DW
412 var context = this, args = arguments;
413 clearTimeout(timer);
5bb4f444 414 timer = setTimeout(function() {
cc90cedc
DW
415 fn.apply(context, args);
416 }, delay);
417 };
418 },
419
8bf5ad67
DW
420 /**
421 * Update the preview div to match the current equation.
422 *
62467795
AN
423 * @param {EventFacade} e
424 * @method _updatePreview
425 * @private
8bf5ad67 426 */
62467795
AN
427 _updatePreview: function(e) {
428 var textarea = this._content.one(SELECTORS.EQUATION_TEXT),
429 equation = textarea.get('value'),
430 url,
62467795
AN
431 currentPos = textarea.get('selectionStart'),
432 prefix = '',
d8e2dc99 433 cursorLatex = '\\Downarrow ',
441f94b2
DW
434 isChar,
435 params;
62467795 436
62467795
AN
437 if (e) {
438 e.preventDefault();
439 }
440
d926ce71
AN
441 // Move the cursor so it does not break expressions.
442 // Start at the very beginning.
62467795
AN
443 if (!currentPos) {
444 currentPos = 0;
8bf5ad67 445 }
d926ce71
AN
446
447 // First move back to the beginning of the line.
448 while (equation.charAt(currentPos) === '\\' && currentPos >= 0) {
62467795 449 currentPos -= 1;
8bf5ad67 450 }
46045db2 451 isChar = /[a-zA-Z\{]/;
d926ce71 452 if (currentPos !== 0) {
46045db2
DS
453 if (equation.charAt(currentPos - 1) != '{') {
454 // Now match to the end of the line.
455 while (isChar.test(equation.charAt(currentPos)) &&
456 currentPos < equation.length &&
457 isChar.test(equation.charAt(currentPos - 1))) {
458 currentPos += 1;
459 }
d926ce71 460 }
8bf5ad67
DW
461 }
462 // Save the cursor position - for insertion from the library.
62467795
AN
463 this._lastCursorPos = currentPos;
464 equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos);
8939ebac 465
cc90cedc
DW
466 equation = DELIMITERS.START + ' ' + equation + ' ' + DELIMITERS.END;
467 // Make an ajax request to the filter.
468 url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
469 params = {
470 sesskey: M.cfg.sesskey,
471 contextid: this.get('contextid'),
472 action: 'filtertext',
473 text: equation
474 };
475
3086c8ce
DT
476 Y.io(url, {
477 context: this,
478 data: params,
479 timeout: 500,
480 on: {
481 complete: this._loadPreview
482 }
441f94b2 483 });
3086c8ce
DT
484 },
485
486 /**
487 * Load returned preview text into preview
488 *
489 * @param {String} id
490 * @param {EventFacade} e
491 * @method _loadPreview
492 * @private
493 */
494 _loadPreview: function(id, preview) {
495 var previewNode = this._content.one(SELECTORS.EQUATION_PREVIEW);
441f94b2 496
cc90cedc
DW
497 if (preview.status === 200) {
498 previewNode.setHTML(preview.responseText);
3086c8ce 499
28de7771
DW
500 // Notify the filters about the modified nodes.
501 require(['core/event'], function(event) {
502 event.notifyFilterContentUpdated(previewNode.getDOMNode());
503 });
8bf5ad67
DW
504 }
505 },
506
507 /**
62467795
AN
508 * Return the dialogue content for the tool, attaching any required
509 * events.
8bf5ad67 510 *
62467795
AN
511 * @method _getDialogueContent
512 * @return {Node}
513 * @private
8bf5ad67 514 */
62467795
AN
515 _getDialogueContent: function() {
516 var library = this._getLibraryContent(),
3086c8ce 517 throttledUpdate = this._throttle(this._updatePreview, 500),
62467795
AN
518 template = Y.Handlebars.compile(TEMPLATES.FORM);
519
520 this._content = Y.Node.create(template({
521 elementid: this.get('host').get('elementid'),
522 component: COMPONENTNAME,
523 library: library,
36beb828 524 texdocsurl: this.get('texdocsurl'),
62467795
AN
525 CSS: CSS
526 }));
527
050159dc
FM
528 // Sets the default focus.
529 this._content.all(SELECTORS.LIBRARY_GROUP).each(function(group) {
530 // The first button gets the focus.
531 this._setGroupTabFocus(group, group.one('button'));
532 // Sometimes the filter adds an anchor in the button, no tabindex on that.
533 group.all('button a').setAttribute('tabindex', '-1');
534 }, this);
535
536 // Keyboard navigation in groups.
537 this._content.delegate('key', this._groupNavigation, 'down:37,39', SELECTORS.LIBRARY_BUTTON, this);
538
62467795 539 this._content.one(SELECTORS.SUBMIT).on('click', this._setEquation, this);
3086c8ce
DT
540 this._content.one(SELECTORS.EQUATION_TEXT).on('valuechange', throttledUpdate, this);
541 this._content.one(SELECTORS.EQUATION_TEXT).on('mouseup', throttledUpdate, this);
542 this._content.one(SELECTORS.EQUATION_TEXT).on('keyup', throttledUpdate, this);
62467795
AN
543 this._content.delegate('click', this._selectLibraryItem, SELECTORS.LIBRARY_BUTTON, this);
544
545 return this._content;
8bf5ad67
DW
546 },
547
050159dc
FM
548 /**
549 * Callback handling the keyboard navigation in the groups of the library.
550 *
551 * @param {EventFacade} e The event.
552 * @method _groupNavigation
553 * @private
554 */
555 _groupNavigation: function(e) {
556 e.preventDefault();
557
558 var current = e.currentTarget,
559 parent = current.get('parentNode'), // This must be the <div> containing all the buttons of the group.
560 buttons = parent.all('button'),
561 direction = e.keyCode !== 37 ? 1 : -1,
562 index = buttons.indexOf(current),
563 nextButton;
564
565 if (index < 0) {
566 Y.log('Unable to find the current button in the list of buttons', 'debug', LOGNAME);
567 index = 0;
568 }
569
570 index += direction;
571 if (index < 0) {
572 index = buttons.size() - 1;
573 } else if (index >= buttons.size()) {
574 index = 0;
575 }
576 nextButton = buttons.item(index);
577
578 this._setGroupTabFocus(parent, nextButton);
579 nextButton.focus();
580 },
581
582 /**
583 * Sets tab focus for the group.
584 *
585 * @method _setGroupTabFocus
586 * @param {Node} button The node that focus should now be set to.
587 * @private
588 */
589 _setGroupTabFocus: function(parent, button) {
590 var parentId = parent.generateID();
591
592 // Unset the previous entry.
593 if (typeof this._groupFocus[parentId] !== 'undefined') {
594 this._groupFocus[parentId].setAttribute('tabindex', '-1');
595 }
596
597 // Set on the new entry.
598 this._groupFocus[parentId] = button;
599 button.setAttribute('tabindex', 0);
600 parent.setAttribute('aria-activedescendant', button.generateID());
601 },
602
8bf5ad67 603 /**
62467795 604 * Reponse to button presses in the TeX library panels.
8bf5ad67 605 *
62467795
AN
606 * @method _selectLibraryItem
607 * @param {EventFacade} e
608 * @return {string}
609 * @private
8bf5ad67 610 */
62467795 611 _selectLibraryItem: function(e) {
d926ce71
AN
612 var tex = e.currentTarget.getAttribute('data-tex'),
613 oldValue,
614 newValue,
615 input,
616 focusPoint = 0;
8bf5ad67 617
62467795 618 e.preventDefault();
8bf5ad67 619
050159dc
FM
620 // Set the group focus on the button.
621 this._setGroupTabFocus(e.currentTarget.get('parentNode'), e.currentTarget);
622
62467795 623 input = e.currentTarget.ancestor('.atto_form').one('textarea');
8bf5ad67 624
d926ce71
AN
625 oldValue = input.get('value');
626
627 newValue = oldValue.substring(0, this._lastCursorPos);
628 if (newValue.charAt(newValue.length - 1) !== ' ') {
629 newValue += ' ';
630 }
631 newValue += tex;
632 focusPoint = newValue.length;
8bf5ad67 633
d926ce71
AN
634 if (oldValue.charAt(this._lastCursorPos) !== ' ') {
635 newValue += ' ';
636 }
637 newValue += oldValue.substring(this._lastCursorPos, oldValue.length);
8bf5ad67 638
d926ce71 639 input.set('value', newValue);
8bf5ad67 640 input.focus();
9ee8a359 641
d926ce71 642 var realInput = input.getDOMNode();
9ee8a359
AN
643 if (typeof realInput.selectionStart === "number") {
644 // Modern browsers have selectionStart and selectionEnd to control the cursor position.
645 realInput.selectionStart = realInput.selectionEnd = focusPoint;
646 } else if (typeof realInput.createTextRange !== "undefined") {
647 // Legacy browsers (IE<=9) use createTextRange().
648 var range = realInput.createTextRange();
649 range.moveToPoint(focusPoint);
650 range.select();
651 }
652 // Focus must be set before updating the preview for the cursor box to be in the correct location.
62467795 653 this._updatePreview(false);
8bf5ad67
DW
654 },
655
656 /**
657 * Return the HTML for rendering the library of predefined buttons.
658 *
62467795
AN
659 * @method _getLibraryContent
660 * @return {string}
661 * @private
8bf5ad67 662 */
62467795
AN
663 _getLibraryContent: function() {
664 var template = Y.Handlebars.compile(TEMPLATES.LIBRARY),
665 library = this.get('library'),
666 content = '';
667
668 // Helper to iterate over a newline separated string.
669 Y.Handlebars.registerHelper('split', function(delimiter, str, options) {
670 var parts,
671 current,
672 out;
673 if (typeof delimiter === "undefined" || typeof str === "undefined") {
674 Y.log('Handlebars split helper: String and delimiter are required.', 'debug', 'moodle-atto_equation-button');
675 return '';
8bf5ad67 676 }
62467795
AN
677
678 out = '';
679 parts = str.trim().split(delimiter);
680 while (parts.length > 0) {
cc90cedc 681 current = parts.shift().trim();
62467795
AN
682 out += options.fn(current);
683 }
684
685 return out;
686 });
687 content = template({
688 elementid: this.get('host').get('elementid'),
689 component: COMPONENTNAME,
690 library: library,
cc90cedc
DW
691 CSS: CSS,
692 DELIMITERS: DELIMITERS
62467795 693 });
8bf5ad67 694
cc90cedc
DW
695 var url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
696 var params = {
697 sesskey: M.cfg.sesskey,
698 contextid: this.get('contextid'),
699 action: 'filtertext',
700 text: content
701 };
702
557f44d9 703 var preview = Y.io(url, {
cc90cedc
DW
704 sync: true,
705 data: params,
706 method: 'POST'
707 });
8bf5ad67 708
cc90cedc
DW
709 if (preview.status === 200) {
710 content = preview.responseText;
8bf5ad67
DW
711 }
712 return content;
713 }
62467795
AN
714}, {
715 ATTRS: {
716 /**
717 * Whether the TeX filter is currently active.
718 *
719 * @attribute texfilteractive
720 * @type Boolean
721 */
722 texfilteractive: {
723 value: false
724 },
cc90cedc 725
62467795
AN
726 /**
727 * The contextid to use when generating this preview.
728 *
729 * @attribute contextid
730 * @type String
731 */
732 contextid: {
733 value: null
734 },
735
736 /**
737 * The content of the example library.
738 *
739 * @attribute library
740 * @type object
741 */
742 library: {
743 value: {}
36beb828
FM
744 },
745
746 /**
747 * The link to the Moodle Docs page about TeX.
748 *
749 * @attribute texdocsurl
750 * @type string
751 */
752 texdocsurl: {
753 value: null
62467795 754 }
cc90cedc 755
62467795
AN
756 }
757});