MDL-44128 Atto: fix keyboard navigation for the dropdowns
[moodle.git] / lib / editor / atto / plugins / equation / yui / src / button / js / button.js
CommitLineData
8bf5ad67
DW
1// This file is part of Moodle - http://moodle.org/
2//
3// Moodle is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// Moodle is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
15
16/**
17 * Atto text editor equation plugin.
18 *
19 * @package editor-atto
20 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22 */
23M.atto_equation = M.atto_equation || {
24 /**
25 * The window used to get the equation details.
26 *
27 * @property dialogue
28 * @type M.core.dialogue
29 * @default null
30 */
31 dialogue : null,
32
33 /**
34 * The selection object returned by the browser.
35 *
36 * @property selection
37 * @type Range
38 * @default null
39 */
40 selection : null,
41
42 /**
43 * A mapping of elementids to contextids.
44 *
45 * @property contextids
46 * @type Object
47 * @default {}
48 */
49 contextids : {},
50
51 /**
52 * A nested object containing a the configured list of tex examples.
53 *
54 * @property library
55 * @type Object
56 * @default {}
57 */
58 library : {},
59
60 /**
61 * The last cursor index in the source.
62 *
63 * @property lastcursor
64 * @type Integer
65 * @default 0
66 */
67 lastcursor : 0,
68
69 /**
70 * Display the chooser dialogue.
71 *
72 * @method display_chooser
73 * @param Event e
74 * @param string elementid
75 */
76 display_chooser : function(e, elementid) {
77 e.preventDefault();
78 if (!M.editor_atto.is_active(elementid)) {
79 M.editor_atto.focus(elementid);
80 }
81 M.atto_equation.selection = M.editor_atto.get_selection();
d321f68b 82 if (M.atto_equation.selection) {
8bf5ad67
DW
83 var dialogue;
84 if (!M.atto_equation.dialogue) {
85 dialogue = new M.core.dialogue({
86 visible: false,
87 modal: true,
88 close: true,
89 draggable: true,
90 width: '800px'
91 });
92 } else {
93 dialogue = M.atto_equation.dialogue;
94 }
95
96 dialogue.render();
97 dialogue.set('bodyContent', M.atto_equation.get_form_content(elementid));
98 dialogue.set('headerContent', M.util.get_string('pluginname', 'atto_equation'));
99
100 var tabview = new Y.TabView({
101 srcNode: '#atto_equation_library'
102 });
103
104 tabview.render();
105 dialogue.show();
3ee53a42
DW
106 var equation = M.atto_equation.resolve_equation();
107 if (equation) {
108 Y.one('#atto_equation_equation').set('text', equation);
109 }
8bf5ad67
DW
110 M.atto_equation.update_preview(false, elementid);
111 M.atto_equation.dialogue = dialogue;
112 }
113 },
114
115 /**
116 * Add this button to the form.
117 *
118 * @method init
119 * @param {Object} params
120 */
121 init : function(params) {
122 var iconurl = M.util.image_url('e/math', 'core');
123
124 if (params.texfilteractive) {
125 // Save the elementid/contextid mapping.
126 this.contextids[params.elementid] = params.contextid;
127 // Save the button library.
128 this.library = params.library;
129
130 // Add the button to the toolbar.
131 M.editor_atto.add_toolbar_button(params.elementid, 'equation', iconurl, params.group, this.display_chooser);
3ee53a42
DW
132 // Attach an event listner to watch for "changes" in the contenteditable.
133 // This includes cursor changes, we check if the button should be active or not, based
134 // on the text selection.
67d3fe45
SH
135 M.editor_atto.on('atto:selectionchanged', function(e) {
136 if (M.atto_equation.resolve_equation()) {
3ee53a42
DW
137 M.editor_atto.add_widget_highlight(e.elementid, 'equation');
138 } else {
139 M.editor_atto.remove_widget_highlight(e.elementid, 'equation');
140 }
141 });
8bf5ad67
DW
142 }
143 },
144
145 /**
146 * If there is selected text and it is part of an equation,
147 * extract the equation (and set it in the form).
148 *
149 * @method resolve_equation
3ee53a42 150 * @return {String|Boolean} The equation or false.
8bf5ad67
DW
151 */
152 resolve_equation : function() {
153 // Find the equation in the surrounding text.
154 var selectednode = M.editor_atto.get_selection_parent_node(),
155 text,
156 equation;
157
158 // Note this is a document fragment and YUI doesn't like them.
159 if (!selectednode) {
3ee53a42 160 return false;
8bf5ad67
DW
161 }
162
163 text = Y.one(selectednode).get('text');
164 // We use space or not space because . does not match new lines.
165 pattern = /\$\$[\S\s]*\$\$/;
166 equation = pattern.exec(text);
167 if (equation && equation.length) {
168 equation = equation.pop();
169 // Replace the equation.
170 equation = equation.substring(2, equation.length - 2);
3ee53a42 171 return equation;
8bf5ad67 172 }
3ee53a42 173 return false;
8bf5ad67
DW
174 },
175
176 /**
177 * The OK button has been pressed - make the changes to the source.
178 *
179 * @method set_equation
180 * @param {Y.Event} e
181 * @param {String} elementid
182 */
183 set_equation : function(e, elementid) {
184 var input,
185 selectednode,
186 text,
187 pattern,
188 equation,
189 value;
190
191 e.preventDefault();
192 M.atto_equation.dialogue.hide();
193 M.editor_atto.set_selection(M.atto_equation.selection);
194
195 input = e.currentTarget.ancestor('.atto_form').one('textarea');
196
197 value = input.get('value');
198 if (value !== '') {
199 value = '$$ ' + value.trim() + ' $$';
200 selectednode = Y.one(M.editor_atto.get_selection_parent_node()),
201 text = selectednode.get('text');
202 pattern = /\$\$[\S\s]*\$\$/;
203 equation = pattern.exec(text);
204 if (equation && equation.length) {
205 // Replace the equation.
206 equation = equation.pop();
207 text = text.replace(equation, '$$' + value + '$$');
208 selectednode.set('text', text);
209 } else {
210 // Insert the new equation.
a30a40cb 211 M.editor_atto.insert_html_at_focus_point(value);
8bf5ad67
DW
212 }
213
214 // Clean the YUI ids from the HTML.
215 M.editor_atto.text_updated(elementid);
216 }
217 },
218
219 /**
220 * Update the preview div to match the current equation.
221 *
222 * @param Event e - unused
223 * @param String elementid - The editor elementid.
224 * @method update_preview
225 */
226 update_preview : function(e, elementid) {
227 var textarea = Y.one('#atto_equation_equation');
228 var equation = textarea.get('value'), url, preview;
229 var prefix = '';
230 var cursorlatex = '\\square ' ;
231
232 var currentpos = textarea.get('selectionStart');
233 if (!currentpos) {
234 currentpos = 0;
235 }
236 // Move the cursor so it does not break expressions.
237 //
238 while (equation.charAt(currentpos) === '\\' && currentpos > 0) {
239 currentpos -= 1;
240 }
241 var ischar = /[\w\{\}]/;
242 while (ischar.test(equation.charAt(currentpos)) && currentpos < equation.length) {
243 currentpos += 1;
244 }
245 // Save the cursor position - for insertion from the library.
246 this.lastcursorpos = currentpos;
247 equation = prefix + equation.substring(0, currentpos) + cursorlatex + equation.substring(currentpos);
248 if (e) {
249 e.preventDefault();
250 }
251 url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
252 params = {
253 sesskey: M.cfg.sesskey,
254 contextid: this.contextids[elementid],
255 action : 'filtertext',
256 text : '$$ ' + equation + ' $$'
257 };
258
259
260 preview = Y.io(url, { sync: true,
261 data: params });
262 if (preview.status === 200) {
263 Y.one('#atto_equation_preview').setHTML(preview.responseText);
264 }
265 },
266
267 /**
268 * Return the HTML of the form to show in the dialogue.
269 *
270 * @method get_form_content
271 * @param string elementid
272 * @return string
273 */
274 get_form_content : function(elementid) {
275 var content = Y.Node.create('<form class="atto_form">' +
276 this.get_library_html(elementid) +
277 '<label for="atto_equation_equation">' + M.util.get_string('editequation', 'atto_equation') +
278 '</label>' +
279 '<textarea class="fullwidth" id="atto_equation_equation" rows="8"></textarea><br/>' +
280 '<p>' + M.util.get_string('editequation_desc', 'atto_equation') + '</p>' +
281 '<label for="atto_equation_preview">' + M.util.get_string('preview', 'atto_equation') +
282 '</label>' +
283 '<div class="fullwidth" id="atto_equation_preview"></div>' +
284 '<div class="mdl-align">' +
285 '<br/>' +
286 '<button id="atto_equation_submit">' +
287 M.util.get_string('saveequation', 'atto_equation') +
288 '</button>' +
289 '</div>' +
290 '</form>');
291
292 content.one('#atto_equation_submit').on('click', M.atto_equation.set_equation, this, elementid);
293 content.one('#atto_equation_equation').on('valuechange', M.atto_equation.update_preview, this, elementid);
294 content.one('#atto_equation_equation').on('keyup', M.atto_equation.update_preview, this, elementid);
295 content.one('#atto_equation_equation').on('mouseup', M.atto_equation.update_preview, this, elementid);
296
297 content.delegate('click', M.atto_equation.select_library_item, '#atto_equation_library button', this, elementid);
298
299 return content;
300 },
301
302 /**
303 * Reponse to button presses in the tex library panels.
304 *
305 * @method select_library_item
306 * @param Event event
307 * @param string elementid
308 * @return string
309 */
310 select_library_item : function(event, elementid) {
311 var tex = event.currentTarget.getAttribute('data-tex');
312
313 event.preventDefault();
314
315 input = event.currentTarget.ancestor('.atto_form').one('textarea');
316
317 value = input.get('value');
318
319 value = value.substring(0, this.lastcursorpos) + tex + value.substring(this.lastcursorpos, value.length);
320
321 input.set('value', value);
8bf5ad67 322 input.focus();
9ee8a359
AN
323
324 var focusPoint = this.lastcursorpos + tex.length,
325 realInput = input.getDOMNode();
326 if (typeof realInput.selectionStart === "number") {
327 // Modern browsers have selectionStart and selectionEnd to control the cursor position.
328 realInput.selectionStart = realInput.selectionEnd = focusPoint;
329 } else if (typeof realInput.createTextRange !== "undefined") {
330 // Legacy browsers (IE<=9) use createTextRange().
331 var range = realInput.createTextRange();
332 range.moveToPoint(focusPoint);
333 range.select();
334 }
335 // Focus must be set before updating the preview for the cursor box to be in the correct location.
336 M.atto_equation.update_preview(false, elementid);
8bf5ad67
DW
337 },
338
339 /**
340 * Return the HTML for rendering the library of predefined buttons.
341 *
342 * @method get_library_html
343 * @param string elementid
344 * @return string
345 */
346 get_library_html : function(elementid) {
347 var content = '<div id="atto_equation_library">', i = 0, group = 1;
348 content += '<ul>';
349 for (group = 1; group < 5; group++) {
350 content += '<li><a href="#atto_equation_library' + group + '">' + M.util.get_string('librarygroup' + group, 'atto_equation') + '</a></li>';
351 }
352 content += '</ul>';
353 content += '<div>';
354 for (group = 1; group < 5; group++) {
355 content += '<div id="atto_equation_library' + group + '">';
356 var examples = this.library['group' + group].split("\n");
357 for (i = 0; i < examples.length; i++) {
358 if (examples[i]) {
359 examples[i] = Y.Escape.html(examples[i]);
360 content += '<button data-tex="' + examples[i] + '" title="' + examples[i] + '">$$' + examples[i] + '$$</button>';
361 }
362 }
363 content += '</div>';
364 }
365 content += '</div>';
366 content += '</div>';
367
368 var url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
369 var params = {
370 sesskey: M.cfg.sesskey,
371 contextid: this.contextids[elementid],
372 action : 'filtertext',
373 text : content
374 };
375
376 preview = Y.io(url, { sync: true, data: params, method: 'POST'});
377
378 if (preview.status === 200) {
379 content = preview.responseText;
380 }
381 return content;
382 }
383};