weekly release 2.7dev
[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/**
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 */
33
34var COMPONENTNAME = '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_GROUP_PREFIX: 'atto_equation_library'
41 },
42 SELECTORS = {
43 LIBRARY_GROUP_PREFIX: '.' + CSS.LIBRARY_GROUP_PREFIX,
44 EQUATION_TEXT: '.' + CSS.EQUATION_TEXT,
45 EQUATION_PREVIEW: '.' + CSS.EQUATION_PREVIEW,
46 SUBMIT: '.' + CSS.SUBMIT,
47 LIBRARY_BUTTON: '.' + CSS.LIBRARY + ' button'
48 },
49 TEMPLATES = {
50 FORM: '' +
51 '<form class="atto_form">' +
52 '{{{library}}}' +
36beb828 53 '<label for="{{elementid}}_{{CSS.EQUATION_TEXT}}">{{{get_string "editequation" component texdocsurl}}}</label>' +
62467795 54 '<textarea class="fullwidth {{CSS.EQUATION_TEXT}}" id="{{elementid}}_{{CSS.EQUATION_TEXT}}" rows="8"></textarea><br/>' +
62467795
AN
55 '<label for="{{elementid}}_{{CSS.EQUATION_PREVIEW}}">{{get_string "preview" component}}</label>' +
56 '<div class="fullwidth {{CSS.EQUATION_PREVIEW}}" id="{{elementid}}_{{CSS.EQUATION_PREVIEW}}"></div>' +
57 '<div class="mdl-align">' +
58 '<br/>' +
59 '<button class="{{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' +
60 '</div>' +
61 '</form>',
62 LIBRARY: '' +
63 '<div class="{{CSS.LIBRARY}}">' +
64 '<ul>' +
65 '{{#each library}}' +
66 '<li><a href="#{{elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}{{@key}}">{{get_string groupname ../component}}</a></li>' +
67 '{{/each}}' +
68 '</ul>' +
69 '<div>' +
70 '{{#each library}}' +
71 '<div id="{{elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}{{@key}}">' +
72 '{{#split "\n" elements}}' +
73 '<button data-tex="{{this}}" title="{{this}}">$${{this}}$$</button>' +
74 '{{/split}}' +
75 '</div>' +
76 '{{/each}}' +
77 '</div>' +
78 '</div>'
79 };
80
81Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
8bf5ad67
DW
82
83 /**
84 * The selection object returned by the browser.
85 *
62467795 86 * @property _currentSelection
8bf5ad67
DW
87 * @type Range
88 * @default null
62467795 89 * @private
8bf5ad67 90 */
62467795 91 _currentSelection: null,
8bf5ad67
DW
92
93 /**
62467795 94 * The cursor position in the equation textarea.
8bf5ad67 95 *
62467795
AN
96 * @property _lastCursorPos
97 * @type Number
8bf5ad67 98 * @default 0
62467795 99 * @private
8bf5ad67 100 */
62467795 101 _lastCursorPos: 0,
8bf5ad67
DW
102
103 /**
62467795 104 * A reference to the dialogue content.
8bf5ad67 105 *
62467795
AN
106 * @property _content
107 * @type Node
108 * @private
8bf5ad67 109 */
62467795 110 _content: null,
8bf5ad67 111
62467795
AN
112 initializer: function() {
113 if (this.get('texfilteractive')) {
114 // Add the button to the toolbar.
115 this.addButton({
116 icon: 'e/math',
117 callback: this._displayDialogue
8bf5ad67
DW
118 });
119
62467795
AN
120 // We need custom highlight logic for this button.
121 this.get('host').on('atto:selectionchanged', function() {
122 if (this._resolveEquation()) {
123 this.highlightButtons();
124 } else {
125 this.unHighlightButtons();
126 }
127 }, this);
8bf5ad67
DW
128 }
129 },
130
131 /**
62467795 132 * Display the equation editor.
8bf5ad67 133 *
62467795
AN
134 * @method _displayDialogue
135 * @private
8bf5ad67 136 */
62467795
AN
137 _displayDialogue: function() {
138 this._currentSelection = this.get('host').getSelection();
8bf5ad67 139
62467795
AN
140 if (this._currentSelection === false) {
141 return;
142 }
8bf5ad67 143
62467795
AN
144 var dialogue = this.getDialogue({
145 headerContent: M.util.get_string('pluginname', COMPONENTNAME),
1eb5839c
FM
146 focusAfterHide: true,
147 width: 600
62467795
AN
148 });
149
150 var content = this._getDialogueContent();
151 dialogue.set('bodyContent', content);
152
153 var library = content.one(SELECTORS.LIBRARY_GROUP_PREFIX);
154
155 var tabview = new Y.TabView({
156 srcNode: library
157 });
158
159 tabview.render();
160 dialogue.show();
161
162 var equation = this._resolveEquation();
163 if (equation) {
164 content.one(SELECTORS.EQUATION_TEXT).set('text', equation);
8bf5ad67 165 }
62467795 166 this._updatePreview(false);
8bf5ad67
DW
167 },
168
169 /**
170 * If there is selected text and it is part of an equation,
171 * extract the equation (and set it in the form).
172 *
62467795
AN
173 * @method _resolveEquation
174 * @private
3ee53a42 175 * @return {String|Boolean} The equation or false.
8bf5ad67 176 */
62467795
AN
177 _resolveEquation: function() {
178
8bf5ad67 179 // Find the equation in the surrounding text.
62467795 180 var selectedNode = this.get('host').getSelectionParentNode(),
8bf5ad67
DW
181 text,
182 equation;
183
184 // Note this is a document fragment and YUI doesn't like them.
62467795 185 if (!selectedNode) {
3ee53a42 186 return false;
8bf5ad67
DW
187 }
188
62467795 189 text = Y.one(selectedNode).get('text');
8bf5ad67
DW
190 // We use space or not space because . does not match new lines.
191 pattern = /\$\$[\S\s]*\$\$/;
192 equation = pattern.exec(text);
193 if (equation && equation.length) {
194 equation = equation.pop();
195 // Replace the equation.
196 equation = equation.substring(2, equation.length - 2);
3ee53a42 197 return equation;
8bf5ad67 198 }
3ee53a42 199 return false;
8bf5ad67
DW
200 },
201
202 /**
62467795 203 * Handle insertion of a new equation, or update of an existing one.
8bf5ad67 204 *
62467795
AN
205 * @method _setEquation
206 * @param {EventFacade} e
207 * @private
8bf5ad67 208 */
62467795 209 _setEquation: function(e) {
8bf5ad67 210 var input,
62467795 211 selectedNode,
8bf5ad67
DW
212 text,
213 pattern,
214 equation,
215 value;
216
62467795
AN
217 var host = this.get('host');
218
8bf5ad67 219 e.preventDefault();
62467795
AN
220 this.getDialogue({
221 focusAfterHide: null
222 }).hide();
8bf5ad67
DW
223
224 input = e.currentTarget.ancestor('.atto_form').one('textarea');
225
226 value = input.get('value');
227 if (value !== '') {
62467795
AN
228 host.setSelection(this._currentSelection);
229
8bf5ad67 230 value = '$$ ' + value.trim() + ' $$';
62467795
AN
231 selectedNode = Y.one(host.getSelectionParentNode());
232 text = selectedNode.get('text');
8bf5ad67
DW
233 pattern = /\$\$[\S\s]*\$\$/;
234 equation = pattern.exec(text);
235 if (equation && equation.length) {
236 // Replace the equation.
237 equation = equation.pop();
238 text = text.replace(equation, '$$' + value + '$$');
62467795 239 selectedNode.set('text', text);
8bf5ad67
DW
240 } else {
241 // Insert the new equation.
62467795 242 host.insertContentAtFocusPoint(value);
8bf5ad67
DW
243 }
244
245 // Clean the YUI ids from the HTML.
62467795 246 this.markUpdated();
8bf5ad67
DW
247 }
248 },
249
250 /**
251 * Update the preview div to match the current equation.
252 *
62467795
AN
253 * @param {EventFacade} e
254 * @method _updatePreview
255 * @private
8bf5ad67 256 */
62467795
AN
257 _updatePreview: function(e) {
258 var textarea = this._content.one(SELECTORS.EQUATION_TEXT),
259 equation = textarea.get('value'),
260 url,
261 preview,
262 currentPos = textarea.get('selectionStart'),
263 prefix = '',
264 cursorLatex = '\\square ',
265 isChar;
266
267
268 if (e) {
269 e.preventDefault();
270 }
271
272 if (!currentPos) {
273 currentPos = 0;
8bf5ad67
DW
274 }
275 // Move the cursor so it does not break expressions.
276 //
62467795
AN
277 while (equation.charAt(currentPos) === '\\' && currentPos > 0) {
278 currentPos -= 1;
8bf5ad67 279 }
62467795
AN
280 isChar = /[\w\{\}]/;
281 while (isChar.test(equation.charAt(currentPos)) && currentPos < equation.length) {
282 currentPos += 1;
8bf5ad67
DW
283 }
284 // Save the cursor position - for insertion from the library.
62467795
AN
285 this._lastCursorPos = currentPos;
286 equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos);
8bf5ad67
DW
287 url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
288 params = {
289 sesskey: M.cfg.sesskey,
62467795
AN
290 contextid: this.get('contextid'),
291 action: 'filtertext',
292 text: '$$ ' + equation + ' $$'
8bf5ad67
DW
293 };
294
8bf5ad67
DW
295 preview = Y.io(url, { sync: true,
296 data: params });
297 if (preview.status === 200) {
62467795 298 this._content.one(SELECTORS.EQUATION_PREVIEW).setHTML(preview.responseText);
8bf5ad67
DW
299 }
300 },
301
302 /**
62467795
AN
303 * Return the dialogue content for the tool, attaching any required
304 * events.
8bf5ad67 305 *
62467795
AN
306 * @method _getDialogueContent
307 * @return {Node}
308 * @private
8bf5ad67 309 */
62467795
AN
310 _getDialogueContent: function() {
311 var library = this._getLibraryContent(),
312 template = Y.Handlebars.compile(TEMPLATES.FORM);
313
314 this._content = Y.Node.create(template({
315 elementid: this.get('host').get('elementid'),
316 component: COMPONENTNAME,
317 library: library,
36beb828 318 texdocsurl: this.get('texdocsurl'),
62467795
AN
319 CSS: CSS
320 }));
321
322 this._content.one(SELECTORS.SUBMIT).on('click', this._setEquation, this);
323 this._content.one(SELECTORS.EQUATION_TEXT).on('valuechange', this._updatePreview, this);
324 this._content.one(SELECTORS.EQUATION_TEXT).on('mouseup', this._updatePreview, this);
325 this._content.one(SELECTORS.EQUATION_TEXT).on('keyup', this._updatePreview, this);
326 this._content.delegate('click', this._selectLibraryItem, SELECTORS.LIBRARY_BUTTON, this);
327
328 return this._content;
8bf5ad67
DW
329 },
330
331 /**
62467795 332 * Reponse to button presses in the TeX library panels.
8bf5ad67 333 *
62467795
AN
334 * @method _selectLibraryItem
335 * @param {EventFacade} e
336 * @return {string}
337 * @private
8bf5ad67 338 */
62467795
AN
339 _selectLibraryItem: function(e) {
340 var tex = e.currentTarget.getAttribute('data-tex');
8bf5ad67 341
62467795 342 e.preventDefault();
8bf5ad67 343
62467795 344 input = e.currentTarget.ancestor('.atto_form').one('textarea');
8bf5ad67
DW
345
346 value = input.get('value');
347
62467795 348 value = value.substring(0, this._lastCursorPos) + tex + value.substring(this._lastCursorPos, value.length);
8bf5ad67
DW
349
350 input.set('value', value);
8bf5ad67 351 input.focus();
9ee8a359 352
62467795 353 var focusPoint = this._lastCursorPos + tex.length,
9ee8a359
AN
354 realInput = input.getDOMNode();
355 if (typeof realInput.selectionStart === "number") {
356 // Modern browsers have selectionStart and selectionEnd to control the cursor position.
357 realInput.selectionStart = realInput.selectionEnd = focusPoint;
358 } else if (typeof realInput.createTextRange !== "undefined") {
359 // Legacy browsers (IE<=9) use createTextRange().
360 var range = realInput.createTextRange();
361 range.moveToPoint(focusPoint);
362 range.select();
363 }
364 // Focus must be set before updating the preview for the cursor box to be in the correct location.
62467795 365 this._updatePreview(false);
8bf5ad67
DW
366 },
367
368 /**
369 * Return the HTML for rendering the library of predefined buttons.
370 *
62467795
AN
371 * @method _getLibraryContent
372 * @return {string}
373 * @private
8bf5ad67 374 */
62467795
AN
375 _getLibraryContent: function() {
376 var template = Y.Handlebars.compile(TEMPLATES.LIBRARY),
377 library = this.get('library'),
378 content = '';
379
380 // Helper to iterate over a newline separated string.
381 Y.Handlebars.registerHelper('split', function(delimiter, str, options) {
382 var parts,
383 current,
384 out;
385 if (typeof delimiter === "undefined" || typeof str === "undefined") {
386 Y.log('Handlebars split helper: String and delimiter are required.', 'debug', 'moodle-atto_equation-button');
387 return '';
8bf5ad67 388 }
62467795
AN
389
390 out = '';
391 parts = str.trim().split(delimiter);
392 while (parts.length > 0) {
393 current = parts.shift();
394 out += options.fn(current);
395 }
396
397 return out;
398 });
399 content = template({
400 elementid: this.get('host').get('elementid'),
401 component: COMPONENTNAME,
402 library: library,
403 CSS: CSS
404 });
8bf5ad67
DW
405
406 var url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
407 var params = {
408 sesskey: M.cfg.sesskey,
62467795
AN
409 contextid: this.get('contextid'),
410 action: 'filtertext',
411 text: content
8bf5ad67
DW
412 };
413
62467795
AN
414 preview = Y.io(url, {
415 sync: true,
416 data: params,
417 method: 'POST'
418 });
8bf5ad67
DW
419
420 if (preview.status === 200) {
421 content = preview.responseText;
422 }
423 return content;
424 }
62467795
AN
425}, {
426 ATTRS: {
427 /**
428 * Whether the TeX filter is currently active.
429 *
430 * @attribute texfilteractive
431 * @type Boolean
432 */
433 texfilteractive: {
434 value: false
435 },
436 /**
437 * The contextid to use when generating this preview.
438 *
439 * @attribute contextid
440 * @type String
441 */
442 contextid: {
443 value: null
444 },
445
446 /**
447 * The content of the example library.
448 *
449 * @attribute library
450 * @type object
451 */
452 library: {
453 value: {}
36beb828
FM
454 },
455
456 /**
457 * The link to the Moodle Docs page about TeX.
458 *
459 * @attribute texdocsurl
460 * @type string
461 */
462 texdocsurl: {
463 value: null
62467795
AN
464 }
465 }
466});