Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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