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