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
 * Tiny Equation UI.
18
 *
19
 * @module      tiny_equation/ui
20
 * @copyright   2022 Huong Nguyen <huongnv13@gmail.com>
21
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import EquationModal from 'tiny_equation/modal';
25
import ModalEvents from 'core/modal_events';
26
import {getContextId, getLibraries, getTexDocsUrl} from 'tiny_equation/options';
27
import {notifyFilterContentUpdated} from 'core/event';
28
import * as TinyEquationRepository from 'tiny_equation/repository';
29
import {exception as displayException} from 'core/notification';
30
import {debounce} from 'core/utils';
31
import Selectors from 'tiny_equation/selectors';
32
import {getSourceEquation, getCurrentEquationData, setEquation} from 'tiny_equation/equation';
33
 
34
let currentForm;
35
let lastCursorPos = 0;
36
 
37
/**
38
 * Handle action
39
 * @param {TinyMCE} editor
40
 */
41
export const handleAction = (editor) => {
42
    displayDialogue(editor);
43
};
44
 
45
/**
46
 * Display the equation editor
47
 * @param {TinyMCE} editor
48
 * @returns {Promise<void>}
49
 */
50
const displayDialogue = async(editor) => {
51
    let data = {};
52
    const currentEquationData = getCurrentEquationData(editor);
53
    if (currentEquationData) {
54
        Object.assign(data, currentEquationData);
55
    }
56
    const modal = await EquationModal.create({
57
        templateContext: getTemplateContext(editor, data),
58
    });
59
 
60
    const $root = await modal.getRoot();
61
    const root = $root[0];
62
    currentForm = root.querySelector(Selectors.elements.form);
63
 
64
    const contextId = getContextId(editor);
65
    const debouncedPreviewUpdater = debounce(() => updatePreview(getContextId(editor)), 500);
66
 
67
    $root.on(ModalEvents.shown, () => {
68
        const library = root.querySelector(Selectors.elements.library);
69
        TinyEquationRepository.filterEquation(contextId, library.innerHTML).then(async data => {
70
            library.innerHTML = data.content;
71
            updatePreview(contextId);
72
            notifyFilter(library);
73
            return data;
74
        }).catch(displayException);
75
    });
76
 
77
    root.addEventListener('click', (e) => {
78
        const libraryItem = e.target.closest(Selectors.elements.libraryItem);
79
        const submitAction = e.target.closest(Selectors.actions.submit);
80
        const textArea = e.target.closest('.tiny_equation_equation');
81
        if (libraryItem) {
82
            e.preventDefault();
83
            selectLibraryItem(libraryItem, contextId);
84
        }
85
        if (submitAction) {
86
            e.preventDefault();
87
            setEquation(currentForm, editor);
88
            modal.destroy();
89
        }
90
        if (textArea) {
91
            debouncedPreviewUpdater();
92
        }
93
    });
94
 
95
    root.addEventListener('keyup', (e) => {
96
        const textArea = e.target.closest(Selectors.elements.equationTextArea);
97
        if (textArea) {
98
            debouncedPreviewUpdater();
99
        }
100
    });
101
 
102
    root.addEventListener('keydown', (e) => {
103
        const libraryItem = e.target.closest(Selectors.elements.libraryItem);
104
        if (libraryItem) {
105
            if (e.keyCode == 37 || e.keyCode == 39) {
106
                groupNavigation(e);
107
            }
108
        }
109
    });
110
};
111
 
112
/**
113
 * Get template context.
114
 * @param {TinyMCE} editor
115
 * @param {Object} data
116
 * @returns {Object}
117
 */
118
const getTemplateContext = (editor, data) => {
119
    const libraries = getLibraries(editor);
120
    const texDocsUrl = getTexDocsUrl(editor);
121
 
122
    return Object.assign({}, {
123
        elementid: editor.id,
124
        elementidescaped: CSS.escape(editor.id),
125
        libraries: libraries,
126
        texdocsurl: texDocsUrl,
127
        delimiters: Selectors.delimiters,
128
    }, data);
129
};
130
 
131
/**
132
 * Handle select library item.
133
 * @param {Object} libraryItem
134
 * @param {number} contextId
135
 */
136
const selectLibraryItem = (libraryItem, contextId) => {
137
    const tex = libraryItem.getAttribute('data-tex');
138
    const input = currentForm.querySelector(Selectors.elements.equationTextArea);
139
    let oldValue;
140
    let newValue;
141
    let focusPoint = 0;
142
 
143
    oldValue = input.value;
144
 
145
    newValue = oldValue.substring(0, lastCursorPos);
146
    if (newValue.charAt(newValue.length - 1) !== ' ') {
147
        newValue += ' ';
148
    }
149
    newValue += tex;
150
    focusPoint = newValue.length;
151
 
152
    if (oldValue.charAt(lastCursorPos) !== ' ') {
153
        newValue += ' ';
154
    }
155
    newValue += oldValue.substring(lastCursorPos, oldValue.length);
156
 
157
    input.value = newValue;
158
    input.focus();
159
 
160
    input.selectionStart = input.selectionEnd = focusPoint;
161
 
162
    updatePreview(contextId);
163
};
164
 
165
/**
166
 * Update the preview section.
167
 * @param {number} contextId
168
 */
169
const updatePreview = (contextId) => {
170
    const textarea = currentForm.querySelector(Selectors.elements.equationTextArea);
171
    const preview = currentForm.querySelector(Selectors.elements.preview);
172
    const prefix = '';
173
    const cursorLatex = Selectors.cursorLatex;
174
    const isChar = /[a-zA-Z{]/;
175
    let currentPos = textarea.selectionStart;
176
    let equation = textarea.value;
177
 
178
    // Move the cursor so it does not break expressions.
179
    // Start at the very beginning.
180
    if (!currentPos) {
181
        currentPos = 0;
182
    }
183
 
184
    if (getSourceEquation()) {
185
        currentPos = equation.length;
186
    }
187
 
188
    // First move back to the beginning of the line.
189
    while (equation.charAt(currentPos) === '\\' && currentPos >= 0) {
190
        currentPos -= 1;
191
    }
192
    if (currentPos !== 0) {
193
        if (equation.charAt(currentPos - 1) != '{') {
194
            // Now match to the end of the line.
195
            while (isChar.test(equation.charAt(currentPos)) &&
196
                    currentPos < equation.length &&
197
                    isChar.test(equation.charAt(currentPos - 1))) {
198
                currentPos += 1;
199
            }
200
        }
201
    }
202
    // Save the cursor position - for insertion from the library.
203
    lastCursorPos = currentPos;
204
    equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos);
205
 
206
    equation = Selectors.delimiters.start + ' ' + equation + ' ' + Selectors.delimiters.end;
207
    TinyEquationRepository.filterEquation(contextId, equation).then((data) => {
208
        preview.innerHTML = data.content;
209
        notifyFilter(preview);
210
 
211
        return data;
212
    }).catch(displayException);
213
};
214
 
215
/**
216
 * Notify the filters about the modified nodes
217
 * @param {Element} element
218
 */
219
const notifyFilter = (element) => {
220
    notifyFilterContentUpdated(element);
221
};
222
 
223
/**
224
 * Callback handling the keyboard navigation in the groups of the library.
225
 * @param {Event} e
226
 */
227
const groupNavigation = (e) => {
228
    e.preventDefault();
229
 
230
    const current = e.target.closest(Selectors.elements.libraryItem);
231
    const parent = current.parentNode; // This must be the <div> containing all the buttons of the group.
232
    const buttons = Array.prototype.slice.call(parent.querySelectorAll(Selectors.elements.libraryItem));
233
    const direction = e.keyCode !== 37 ? 1 : -1;
234
    let index = buttons.indexOf(current);
235
    let nextButton;
236
 
237
    if (index < 0) {
238
        index = 0;
239
    }
240
 
241
    index += direction;
242
    if (index < 0) {
243
        index = buttons.length - 1;
244
    } else if (index >= buttons.length) {
245
        index = 0;
246
    }
247
    nextButton = buttons[index];
248
    nextButton.focus();
249
};