Commit | Line | Data |
---|---|---|
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 | 33 | var 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>' + |
1b217025 | 63 | '<div describedby="{{elementid}}_cursorinfo" class="well well-small p-1 fullwidth {{CSS.EQUATION_PREVIEW}}" ' + |
557f44d9 | 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/>' + | |
29551c4b | 68 | '<button class="btn btn-secondary {{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' + |
62467795 AN |
69 | '</div>' + |
70 | '</form>', | |
71 | LIBRARY: '' + | |
72 | '<div class="{{CSS.LIBRARY}}">' + | |
1b217025 | 73 | '<ul class="root nav nav-tabs m-b-1" role="tablist">' + |
62467795 | 74 | '{{#each library}}' + |
1b217025 BB |
75 | '<li class="nav-item">' + |
76 | '<a class="nav-link" href="#{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}" ' + | |
77 | ' role="tab" data-toggle="tab">' + | |
78 | '{{get_string groupname ../component}}' + | |
79 | '</a>' + | |
80 | '</li>' + | |
62467795 AN |
81 | '{{/each}}' + |
82 | '</ul>' + | |
1b217025 | 83 | '<div class="tab-content m-b-1 {{CSS.LIBRARY_GROUPS}}">' + |
62467795 | 84 | '{{#each library}}' + |
1b217025 BB |
85 | '<div data-medium-type="{{CSS.LINK}}" class="tab-pane" ' + |
86 | 'id="{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' + | |
050159dc FM |
87 | '<div role="toolbar">' + |
88 | '{{#split "\n" elements}}' + | |
29551c4b | 89 | '<button class="btn btn-secondary" tabindex="-1" data-tex="{{this}}"' + |
1b217025 | 90 | 'aria-label="{{this}}" title="{{this}}">' + |
0610d2ca DW |
91 | '{{../../DELIMITERS.START}}{{this}}{{../../DELIMITERS.END}}' + |
92 | '</button>' + | |
050159dc FM |
93 | '{{/split}}' + |
94 | '</div>' + | |
62467795 AN |
95 | '</div>' + |
96 | '{{/each}}' + | |
97 | '</div>' + | |
98 | '</div>' | |
99 | }; | |
100 | ||
101 | Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { | |
8bf5ad67 DW |
102 | |
103 | /** | |
104 | * The selection object returned by the browser. | |
105 | * | |
62467795 | 106 | * @property _currentSelection |
8bf5ad67 DW |
107 | * @type Range |
108 | * @default null | |
62467795 | 109 | * @private |
8bf5ad67 | 110 | */ |
62467795 | 111 | _currentSelection: null, |
8bf5ad67 DW |
112 | |
113 | /** | |
62467795 | 114 | * The cursor position in the equation textarea. |
8bf5ad67 | 115 | * |
62467795 AN |
116 | * @property _lastCursorPos |
117 | * @type Number | |
8bf5ad67 | 118 | * @default 0 |
62467795 | 119 | * @private |
8bf5ad67 | 120 | */ |
62467795 | 121 | _lastCursorPos: 0, |
8bf5ad67 DW |
122 | |
123 | /** | |
62467795 | 124 | * A reference to the dialogue content. |
8bf5ad67 | 125 | * |
62467795 AN |
126 | * @property _content |
127 | * @type Node | |
128 | * @private | |
8bf5ad67 | 129 | */ |
62467795 | 130 | _content: null, |
8bf5ad67 | 131 | |
cc90cedc DW |
132 | /** |
133 | * The source equation we are editing in the text. | |
134 | * | |
135 | * @property _sourceEquation | |
ea382dea | 136 | * @type Object |
cc90cedc DW |
137 | * @private |
138 | */ | |
ea382dea | 139 | _sourceEquation: null, |
cc90cedc | 140 | |
050159dc FM |
141 | /** |
142 | * A reference to the tab focus set on each group. | |
143 | * | |
144 | * The keys are the IDs of the group, the value is the Node on which the focus is set. | |
145 | * | |
146 | * @property _groupFocus | |
147 | * @type Object | |
148 | * @private | |
149 | */ | |
150 | _groupFocus: null, | |
151 | ||
ea382dea AN |
152 | /** |
153 | * Regular Expression patterns used to pick out the equations in a String. | |
154 | * | |
155 | * @property _equationPatterns | |
156 | * @type Array | |
157 | * @private | |
158 | */ | |
159 | _equationPatterns: [ | |
160 | // We use space or not space because . does not match new lines. | |
161 | // $$ blah $$. | |
162 | /\$\$([\S\s]+?)\$\$/, | |
163 | // E.g. "\( blah \)". | |
164 | /\\\(([\S\s]+?)\\\)/, | |
165 | // E.g. "\[ blah \]". | |
166 | /\\\[([\S\s]+?)\\\]/, | |
167 | // E.g. "[tex] blah [/tex]". | |
168 | /\[tex\]([\S\s]+?)\[\/tex\]/ | |
169 | ], | |
170 | ||
62467795 | 171 | initializer: function() { |
050159dc | 172 | this._groupFocus = {}; |
0610d2ca | 173 | |
cc90cedc DW |
174 | // If there is a tex filter active - enable this button. |
175 | if (this.get('texfilteractive')) { | |
62467795 AN |
176 | // Add the button to the toolbar. |
177 | this.addButton({ | |
178 | icon: 'e/math', | |
179 | callback: this._displayDialogue | |
8bf5ad67 DW |
180 | }); |
181 | ||
62467795 AN |
182 | // We need custom highlight logic for this button. |
183 | this.get('host').on('atto:selectionchanged', function() { | |
184 | if (this._resolveEquation()) { | |
185 | this.highlightButtons(); | |
186 | } else { | |
187 | this.unHighlightButtons(); | |
188 | } | |
189 | }, this); | |
cc90cedc DW |
190 | |
191 | // We need to convert these to a non dom node based format. | |
5bb4f444 | 192 | this.editor.all('tex').each(function(texNode) { |
557f44d9 AN |
193 | var replacement = Y.Node.create('<span>' + |
194 | DELIMITERS.START + ' ' + texNode.get('text') + ' ' + DELIMITERS.END + | |
195 | '</span>'); | |
cc90cedc DW |
196 | texNode.replace(replacement); |
197 | }); | |
8bf5ad67 | 198 | } |
cc90cedc | 199 | |
8bf5ad67 DW |
200 | }, |
201 | ||
202 | /** | |
62467795 | 203 | * Display the equation editor. |
8bf5ad67 | 204 | * |
62467795 AN |
205 | * @method _displayDialogue |
206 | * @private | |
8bf5ad67 | 207 | */ |
62467795 AN |
208 | _displayDialogue: function() { |
209 | this._currentSelection = this.get('host').getSelection(); | |
8bf5ad67 | 210 | |
62467795 AN |
211 | if (this._currentSelection === false) { |
212 | return; | |
213 | } | |
8bf5ad67 | 214 | |
aca67017 DW |
215 | // This needs to be done before the dialogue is opened because the focus will shift to the dialogue. |
216 | var equation = this._resolveEquation(); | |
217 | ||
62467795 AN |
218 | var dialogue = this.getDialogue({ |
219 | headerContent: M.util.get_string('pluginname', COMPONENTNAME), | |
1eb5839c | 220 | focusAfterHide: true, |
e5ddec38 | 221 | width: 600, |
c1660772 | 222 | focusOnShowSelector: SELECTORS.EQUATION_TEXT |
62467795 AN |
223 | }); |
224 | ||
225 | var content = this._getDialogueContent(); | |
226 | dialogue.set('bodyContent', content); | |
227 | ||
1b217025 | 228 | content.one('.nav-item:first-child .nav-link').getDOMNode().click(); |
62467795 | 229 | |
62467795 | 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 | }); |