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