MDL-44003 Atto: Set type="button" on atto toolbar buttons.
[moodle.git] / lib / editor / atto / yui / build / moodle-editor_atto-editor / moodle-editor_atto-editor.js
CommitLineData
adca7326
DW
1YUI.add('moodle-editor_atto-editor', function (Y, NAME) {
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
18/**
19 * Atto editor.
20 *
21 * @package editor_atto
22 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26/**
27 * Classes constants.
28 */
29CSS = {
30 CONTENT: 'editor_atto_content',
31 CONTENTWRAPPER: 'editor_atto_content_wrap',
32 TOOLBAR: 'editor_atto_toolbar',
33 WRAPPER: 'editor_atto'
34};
35
36/**
37 * Atto editor main class.
38 * Common functions required by editor plugins.
39 *
40 * @package editor_atto
41 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43 */
44M.editor_atto = M.editor_atto || {
45
46 /**
47 * List of attached button handlers to prevent duplicates.
48 */
49 buttonhandlers : {},
50
51 /**
52 * List of attached handlers to add inline editing controls to content.
53 */
54 textupdatedhandlers : {},
55
56 /**
57 * List of YUI overlays for custom menus.
58 */
59 menus : {},
60
61 /**
62 * List of attached menu handlers to prevent duplicates.
63 */
64 menuhandlers : {},
65
66 /**
67 * List of file picker options for specific editor instances.
68 */
69 filepickeroptions : {},
70
71 /**
72 * List of buttons and menus that have been added to the toolbar.
73 */
74 widgets : {},
75
76 /**
77 * Toggle a menu.
78 * @param event e
79 */
80 showhide_menu_handler : function(e) {
81 e.preventDefault();
82 var disabled = this.getAttribute('disabled');
83 var overlayid = this.getAttribute('data-menu');
84 var overlay = M.editor_atto.menus[overlayid];
85 var menu = overlay.get('bodyContent');
86 if (overlay.get('visible') || disabled) {
87 overlay.hide();
88 menu.detach('clickoutside');
89 } else {
90 menu.on('clickoutside', function(ev) {
91 if ((ev.target.ancestor() !== this) && (ev.target !== this)) {
92 if (overlay.get('visible')) {
93 menu.detach('clickoutside');
94 overlay.hide();
95 }
96 }
97 }, this);
98 overlay.show();
99 overlay.bodyNode.one('a').focus();
100 }
101 },
102
103 /**
104 * Handle clicks on editor buttons.
105 * @param event e
106 */
107 buttonclicked_handler : function(e) {
108 var elementid = this.getAttribute('data-editor');
109 var plugin = this.getAttribute('data-plugin');
110 var handler = this.getAttribute('data-handler');
111 var overlay = M.editor_atto.menus[plugin + '_' + elementid];
112
113 if (overlay) {
114 overlay.hide();
115 }
116
117 if (M.editor_atto.is_enabled(elementid, plugin)) {
118 // Pass it on.
119 handler = M.editor_atto.buttonhandlers[handler];
120 return handler(e, elementid);
121 }
122 },
123
adca7326
DW
124 /**
125 * Disable all buttons and menus in the toolbar.
126 * @param string elementid, the element id of this editor.
127 */
128 disable_all_widgets : function(elementid) {
48bdf86f 129 var plugin, element, toolbar = M.editor_atto.get_toolbar_node(elementid);
adca7326 130 for (plugin in M.editor_atto.widgets) {
48bdf86f 131 element = toolbar.one('.atto_' + plugin + '_button');
adca7326
DW
132
133 if (element) {
134 element.setAttribute('disabled', 'true');
135 }
136 }
137 },
138
48bdf86f
DW
139 /**
140 * Get the node of the original textarea element that this editor replaced.
141 *
142 * @param string elementid, the element id of this editor.
143 * @return Y.Node
144 */
145 get_textarea_node : function(elementid) {
146 // Note - it is not safe to use a CSS selector like '#' + elementid
147 // because the id may have colons in it - e.g. quiz.
148 return Y.one(document.getElementById(elementid));
149 },
150
151 /**
152 * Get the node of the toolbar container for this editor.
153 *
154 * @param string elementid, the element id of this editor.
155 * @return Y.Node
156 */
157 get_toolbar_node : function(elementid) {
158 // Note - it is not safe to use a CSS selector like '#' + elementid
159 // because the id may have colons in it - e.g. quiz.
160 return Y.one(document.getElementById(elementid + '_toolbar'));
161 },
162
163 /**
164 * Get the node of the contenteditable container for this editor.
165 *
166 * @param string elementid, the element id of this editor.
167 * @return Y.Node
168 */
169 get_editable_node : function(elementid) {
170 // Note - it is not safe to use a CSS selector like '#' + elementid
171 // because the id may have colons in it - e.g. quiz.
172 return Y.one(document.getElementById(elementid + 'editable'));
173 },
174
175 /**
176 * Determine if the specified toolbar button/menu is enabled.
177 * @param string elementid, the element id of this editor.
178 * @param string plugin, the plugin that created the button/menu.
179 */
180 is_enabled : function(elementid, plugin) {
181 var element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + plugin + '_button');
182
183 return !element.hasAttribute('disabled');
184 },
185
adca7326
DW
186 /**
187 * Enable a single widget in the toolbar.
188 * @param string elementid, the element id of this editor.
189 * @param string plugin, the name of the plugin that created the widget.
190 */
191 enable_widget : function(elementid, plugin) {
48bdf86f 192 var element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + plugin + '_button');
adca7326
DW
193
194 if (element) {
195 element.removeAttribute('disabled');
196 }
197 },
198
199 /**
200 * Enable all buttons and menus in the toolbar.
201 * @param string elementid, the element id of this editor.
202 */
203 enable_all_widgets : function(elementid) {
204 var plugin, element;
205 for (plugin in M.editor_atto.widgets) {
48bdf86f 206 element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + plugin + '_button');
adca7326
DW
207
208 if (element) {
209 element.removeAttribute('disabled');
210 }
211 }
212 },
213
214 /**
215 * Add a content update handler to be called whenever the content is updated.
216 * This is used to add inline editing controls to the content that are cleaned on submission.
217 *
218 * @param string elementid - the id of the textarea we created this editor from.
219 * @handler function callback - The function to do the cleaning.
220 * @param object context - the context to set for the callback.
221 * @handler function handler - A function to call when the button is clicked.
222 */
223 add_text_updated_handler : function(elementid, callback) {
224 if (!(elementid in M.editor_atto.textupdatedhandlers)) {
225 M.editor_atto.textupdatedhandlers[elementid] = [];
226 }
227 M.editor_atto.textupdatedhandlers[elementid].push(callback);
228 },
229
230 /**
231 * Add a button to the toolbar belonging to the editor for element with id "elementid".
232 * @param string elementid - the id of the textarea we created this editor from.
233 * @param string plugin - the plugin defining the button
234 * @param string icon - the html used for the content of the button
235 * @param string groupname - the group the button should be appended to.
236 * @param array entries - List of menu entries with the string (entry.text) and the handlers (entry.handler).
237 */
55c0403c 238 add_toolbar_menu : function(elementid, plugin, iconurl, groupname, entries) {
48bdf86f
DW
239 var toolbar = M.editor_atto.get_toolbar_node(elementid),
240 group = toolbar.one('.atto_group.' + groupname + '_group'),
adca7326
DW
241 currentfocus,
242 button,
adca7326
DW
243 expimgurl;
244
245 if (!group) {
246 group = Y.Node.create('<div class="atto_group ' + groupname + '_group"></div>');
247 toolbar.append(group);
248 }
adca7326
DW
249 expimgurl = M.util.image_url('t/expanded', 'moodle');
250 button = Y.Node.create('<button class="atto_' + plugin + '_button atto_hasmenu" ' +
251 'data-editor="' + Y.Escape.html(elementid) + '" ' +
252 'tabindex="-1" ' +
0012a946 253 'type="button" ' +
adca7326
DW
254 'data-menu="' + plugin + '_' + elementid + '" ' +
255 'title="' + Y.Escape.html(M.util.get_string('pluginname', 'atto_' + plugin)) + '">' +
55c0403c 256 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" src="' + iconurl + '"/>' +
adca7326
DW
257 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" src="' + expimgurl + '"/>' +
258 '</button>');
259
260 group.append(button);
261
262 currentfocus = toolbar.getAttribute('aria-activedescendant');
263 if (!currentfocus) {
264 button.setAttribute('tabindex', '0');
265 toolbar.setAttribute('aria-activedescendant', button.generateID());
266 }
267
268 // Save the name of the plugin.
269 M.editor_atto.widgets[plugin] = plugin;
270
271 var menu = Y.Node.create('<div class="atto_' + plugin + '_menu' +
272 ' atto_menu" data-editor="' + Y.Escape.html(elementid) + '"></div>');
273 var i = 0, entry = {};
274
275 for (i = 0; i < entries.length; i++) {
276 entry = entries[i];
277
278 menu.append(Y.Node.create('<div class="atto_menuentry">' +
279 '<a href="#" class="atto_' + plugin + '_action_' + i + '" ' +
280 'data-editor="' + Y.Escape.html(elementid) + '" ' +
281 'data-plugin="' + Y.Escape.html(plugin) + '" ' +
282 'data-handler="' + Y.Escape.html(plugin + '_action_' + i) + '">' +
283 entry.text +
284 '</a>' +
285 '</div>'));
286 if (!M.editor_atto.buttonhandlers[plugin + '_action_' + i]) {
287 Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_action_' + i);
288 Y.one('body').delegate('key', M.editor_atto.buttonclicked_handler, 'space,enter', '.atto_' + plugin + '_action_' + i);
289 M.editor_atto.buttonhandlers[plugin + '_action_' + i] = entry.handler;
290 }
291 }
292
293 if (!M.editor_atto.buttonhandlers[plugin]) {
294 Y.one('body').delegate('click', M.editor_atto.showhide_menu_handler, '.atto_' + plugin + '_button');
295 M.editor_atto.buttonhandlers[plugin] = true;
296 }
297
298 var overlay = new M.core.dialogue({
299 bodyContent : menu,
300 visible : false,
301 width: '14em',
302 zindex: 100,
303 lightbox: false,
304 closeButton: false,
55c0403c 305 center : false
adca7326
DW
306 });
307
308 M.editor_atto.menus[plugin + '_' + elementid] = overlay;
309 overlay.render();
55c0403c 310 overlay.align(button, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
adca7326
DW
311 overlay.hide();
312 overlay.headerNode.hide();
313 },
314
315 /**
316 * Add a button to the toolbar belonging to the editor for element with id "elementid".
317 * @param string elementid - the id of the textarea we created this editor from.
318 * @param string plugin - the plugin defining the button.
55c0403c 319 * @param string icon - the url to the image for the icon
adca7326
DW
320 * @param string groupname - the group the button should be appended to.
321 * @handler function handler- A function to call when the button is clicked.
322 */
55c0403c 323 add_toolbar_button : function(elementid, plugin, iconurl, groupname, handler) {
48bdf86f
DW
324 var toolbar = M.editor_atto.get_toolbar_node(elementid),
325 group = toolbar.one('.atto_group.' + groupname + '_group'),
adca7326 326 button,
55c0403c 327 currentfocus;
adca7326
DW
328
329 if (!group) {
330 group = Y.Node.create('<div class="atto_group ' + groupname +'_group"></div>');
331 toolbar.append(group);
332 }
adca7326
DW
333 button = Y.Node.create('<button class="atto_' + plugin + '_button" ' +
334 'data-editor="' + Y.Escape.html(elementid) + '" ' +
335 'data-plugin="' + Y.Escape.html(plugin) + '" ' +
336 'tabindex="-1" ' +
337 'data-handler="' + Y.Escape.html(plugin) + '" ' +
338 'title="' + Y.Escape.html(M.util.get_string('pluginname', 'atto_' + plugin)) + '">' +
55c0403c 339 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" src="' + iconurl + '"/>' +
adca7326
DW
340 '</button>');
341
342 group.append(button);
343
344 currentfocus = toolbar.getAttribute('aria-activedescendant');
345 if (!currentfocus) {
346 button.setAttribute('tabindex', '0');
347 toolbar.setAttribute('aria-activedescendant', button.generateID());
348 }
349
350 // We only need to attach this once.
351 if (!M.editor_atto.buttonhandlers[plugin]) {
352 Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_button');
353 M.editor_atto.buttonhandlers[plugin] = handler;
354 }
355
356 // Save the name of the plugin.
357 M.editor_atto.widgets[plugin] = plugin;
358
359 },
360
361 /**
362 * Work out if the cursor is in the editable area for this editor instance.
363 * @param string elementid of this editor
364 * @return bool
365 */
366 is_active : function(elementid) {
367 var selection = M.editor_atto.get_selection();
368
369 if (selection.length) {
370 selection = selection.pop();
371 }
372
373 var node = null;
374 if (selection.parentElement) {
375 node = Y.one(selection.parentElement());
376 } else {
377 node = Y.one(selection.startContainer);
378 }
379
380 return node && node.ancestor('#' + elementid + 'editable') !== null;
381 },
382
383 /**
384 * Focus on the editable area for this editor.
385 * @param string elementid of this editor
386 */
387 focus : function(elementid) {
48bdf86f 388 M.editor_atto.get_editable_node(elementid).focus();
adca7326
DW
389 },
390
391 /**
392 * Initialise the editor
393 * @param object params for this editor instance.
394 */
395 init : function(params) {
adca7326
DW
396 var wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />');
397 var atto = Y.Node.create('<div id="' + params.elementid + 'editable" ' +
398 'contenteditable="true" ' +
399 'spellcheck="true" ' +
400 'class="' + CSS.CONTENT + '" />');
401
402 var cssfont = '';
403 var toolbar = Y.Node.create('<div class="' + CSS.TOOLBAR + '" id="' + params.elementid + '_toolbar" role="toolbar"/>');
404
405 // Editable content wrapper.
406 var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />');
48bdf86f 407 var textarea = M.editor_atto.get_textarea_node(params.elementid);
b269f635 408
adca7326
DW
409 content.appendChild(atto);
410
411 // Add everything to the wrapper.
412 wrapper.appendChild(toolbar);
413 wrapper.appendChild(content);
414
415 // Bleh - why are we sent a url and not the css to apply directly?
416 var css = Y.io(params.content_css, { sync: true });
417 var pos = css.responseText.indexOf('font:');
418 if (pos) {
419 cssfont = css.responseText.substring(pos + 'font:'.length, css.responseText.length - 1);
420 atto.setStyle('font', cssfont);
421 }
422 atto.setStyle('minHeight', (1.2 * (textarea.getAttribute('rows'))) + 'em');
423
424 // Copy text to editable div.
425 atto.append(textarea.get('value'));
426
d088a835
DW
427 // Clean it.
428 atto.cleanHTML();
429
adca7326
DW
430 // Add the toolbar and editable zone to the page.
431 textarea.get('parentNode').insert(wrapper, textarea);
432 atto.setStyle('color', textarea.getStyle('color'));
433 atto.setStyle('lineHeight', textarea.getStyle('lineHeight'));
434 atto.setStyle('fontSize', textarea.getStyle('fontSize'));
435 // Hide the old textarea.
436 textarea.hide();
437
438 // Copy the current value back to the textarea when focus leaves us.
439 atto.on('blur', function() {
48bdf86f 440 this.text_updated(params.elementid);
adca7326
DW
441 }, this);
442
443 // Listen for Arrow left and Arrow right keys.
444 Y.one(Y.config.doc.body).delegate('key',
445 this.keyboard_navigation,
446 'down:37,39',
48bdf86f 447 '#' + params.elementid + '_toolbar',
adca7326 448 this,
48bdf86f 449 params.elementid);
adca7326
DW
450
451 // Save the file picker options for later.
48bdf86f 452 M.editor_atto.filepickeroptions[params.elementid] = params.filepickeroptions;
55c0403c
DW
453
454 // Init each of the plugins
455 var i, j;
456 for (i = 0; i < params.plugins.length; i++) {
457 var group = params.plugins[i].group;
458 for (j = 0; j < params.plugins[i].plugins.length; j++) {
459 var plugin = params.plugins[i].plugins[j];
48bdf86f 460 plugin.params.elementid = params.elementid;
55c0403c
DW
461 plugin.params.group = group;
462
463 M['atto_' + plugin.name].init(plugin.params);
464 }
465 }
adca7326
DW
466 },
467
468 /**
469 * The text in the contenteditable region has been updated,
470 * clean and copy the buffer to the text area.
471 * @param string elementid - the id of the textarea we created this editor from.
472 */
473 text_updated : function(elementid) {
48bdf86f 474 var textarea = M.editor_atto.get_textarea_node(elementid),
adca7326
DW
475 cleancontent = this.get_clean_html(elementid);
476 textarea.set('value', cleancontent);
477 // Trigger handlers for this action.
478 var i = 0;
479 if (elementid in M.editor_atto.textupdatedhandlers) {
480 for (i = 0; i < M.editor_atto.textupdatedhandlers[elementid].length; i++) {
481 var callback = M.editor_atto.textupdatedhandlers[elementid][i];
482 callback(elementid);
483 }
484 }
485 },
486
487 /**
488 * Remove all YUI ids from the generated HTML.
489 * @param string elementid - the id of the textarea we created this editor from.
490 * @return string HTML stripped of YUI ids
491 */
492 get_clean_html : function(elementid) {
48bdf86f 493 var atto = M.editor_atto.get_editable_node(elementid).cloneNode(true);
adca7326
DW
494
495 Y.each(atto.all('[id]'), function(node) {
496 var id = node.get('id');
497 if (id.indexOf('yui') === 0) {
498 node.removeAttribute('id');
499 }
500 });
501
502 Y.each(atto.all('.atto_control'), function(node) {
503 node.remove(true);
504 });
505
d088a835
DW
506 // Remove any and all nasties from source.
507 atto.cleanHTML();
508
adca7326
DW
509 return atto.getHTML();
510 },
511
512 /**
513 * Implement arrow key navigation for the buttons in the toolbar.
514 * @param Event e - the keyboard event.
515 * @param string elementid - the id of the textarea we created this editor from.
516 */
517 keyboard_navigation : function(e, elementid) {
518 var buttons,
519 current,
520 currentid,
48bdf86f
DW
521 currentindex,
522 toolbar = M.editor_atto.get_toolbar_node(elementid);
adca7326
DW
523
524 e.preventDefault();
525
48bdf86f
DW
526 buttons = toolbar.all('button');
527 currentid = toolbar.getAttribute('aria-activedescendant');
adca7326
DW
528 if (!currentid) {
529 return;
530 }
531 current = Y.one('#' + currentid);
532 current.setAttribute('tabindex', '-1');
533
534 currentindex = buttons.indexOf(current);
535
536 if (e.keyCode === 37) {
537 // Left
538 currentindex--;
539 if (currentindex < 0) {
540 currentindex = buttons.size()-1;
541 }
542 } else {
543 // Right
544 currentindex++;
545 if (currentindex >= buttons.size()) {
546 currentindex = 0;
547 }
548 }
549
550 current = buttons.item(currentindex);
551 current.setAttribute('tabindex', '0');
552 current.focus();
48bdf86f 553 toolbar.setAttribute('aria-activedescendant', current.generateID());
adca7326
DW
554 },
555
b269f635
DW
556 /**
557 * Should we show the filepicker for this filetype?
558 *
559 * @param string elementid for this editor instance.
560 * @param string type The media type for the file picker
561 * @return boolean
562 */
563 can_show_filepicker : function(elementid, type) {
564 var options = M.editor_atto.filepickeroptions[elementid];
565 return ((typeof options[type]) !== "undefined");
566 },
567
adca7326
DW
568 /**
569 * Show the filepicker.
570 * @param string elementid for this editor instance.
571 * @param string type The media type for the file picker
572 * @param function callback
573 */
574 show_filepicker : function(elementid, type, callback) {
575 Y.use('core_filepicker', function (Y) {
576 var options = M.editor_atto.filepickeroptions[elementid][type];
577
578 options.formcallback = callback;
adca7326
DW
579
580 M.core_filepicker.show(Y, options);
581 });
582 },
583
584 /**
585 * Create a cross browser selection object that represents a yui node.
586 * @param Node yui node for the selection
587 * @return range (browser dependent)
588 */
589 get_selection_from_node: function(node) {
590 var range;
591
592 if (window.getSelection) {
593 range = document.createRange();
594
595 range.setStartBefore(node.getDOMNode());
596 range.setEndAfter(node.getDOMNode());
597 return [range];
598 } else if (document.selection) {
599 range = document.body.createTextRange();
600 range.moveToElementText(node.getDOMNode());
601 return range;
602 }
603 return false;
604 },
605
606 /**
607 * Get the selection object that can be passed back to set_selection.
608 * @return range (browser dependent)
609 */
610 get_selection : function() {
611 if (window.getSelection) {
612 var sel = window.getSelection();
613 var ranges = [], i = 0;
614 for (i = 0; i < sel.rangeCount; i++) {
615 ranges.push(sel.getRangeAt(i));
616 }
617 return ranges;
618 } else if (document.selection) {
619 // IE < 9
620 if (document.selection.createRange) {
621 return document.selection.createRange();
622 }
623 }
624 return false;
625 },
626
627 /**
628 * Check that a YUI node it at least partly contained by the selection.
629 * @param Range selection
630 * @param Y.Node node
631 * @return boolean
632 */
633 selection_contains_node : function(node) {
634 var range, sel;
635 if (window.getSelection) {
636 sel = window.getSelection();
637
638 if (sel.containsNode) {
639 return sel.containsNode(node.getDOMNode(), true);
640 }
641 }
642 sel = document.selection.createRange();
643 range = sel.duplicate();
644 range.moveToElementText(node.getDOMNode());
645 return sel.inRange(range);
646 },
647
648 /**
649 * Get the dom node representing the common anscestor of the selection nodes.
650 * @return DOMNode
651 */
652 get_selection_parent_node : function() {
653 var selection = M.editor_atto.get_selection();
654 if (selection.length > 0) {
655 return selection[0].commonAncestorContainer;
656 }
657 },
658
659 /**
660 * Get the list of child nodes of the selection.
661 * @return DOMNode[]
662 */
663 get_selection_text : function() {
664 var selection = M.editor_atto.get_selection();
665 if (selection.length > 0 && selection[0].cloneContents) {
666 return selection[0].cloneContents();
667 }
668 },
669
670 /**
671 * Set the current selection. Used to restore a selection.
672 */
673 set_selection : function(selection) {
674 var sel, i;
675
676 if (window.getSelection) {
677 sel = window.getSelection();
678 sel.removeAllRanges();
679 for (i = 0; i < selection.length; i++) {
680 sel.addRange(selection[i]);
681 }
682 } else if (document.selection) {
683 // IE < 9
684 if (selection.select) {
685 selection.select();
686 }
687 }
688 }
689
690};
691var CONTROLMENU_NAME = "Controlmenu",
692 CONTROLMENU;
693
694/**
695 * CONTROLMENU
696 * This is a drop down list of buttons triggered (and aligned to) a button.
697 *
698 * @namespace M.editor_atto.controlmenu
699 * @class controlmenu
700 * @constructor
701 * @extends M.core.dialogue
702 */
703CONTROLMENU = function(config) {
704 config.draggable = false;
55c0403c 705 config.center = false;
adca7326
DW
706 config.width = 'auto';
707 config.lightbox = false;
adca7326
DW
708 config.footerContent = '';
709 CONTROLMENU.superclass.constructor.apply(this, [config]);
710};
711
712Y.extend(CONTROLMENU, M.core.dialogue, {
713
714 /**
715 * Initialise the menu.
716 *
717 * @method initializer
718 * @return void
719 */
720 initializer : function(config) {
721 var body, headertext, bb;
722 CONTROLMENU.superclass.initializer.call(this, config);
723
724 bb = this.get('boundingBox');
725 bb.addClass('editor_atto_controlmenu');
726
727 // Close the menu when clicked outside (excluding the button that opened the menu).
728 body = this.bodyNode;
729
730 headertext = Y.Node.create('<h3/>');
731 headertext.addClass('accesshide');
732 headertext.setHTML(this.get('headerText'));
733 body.prepend(headertext);
734
735 body.on('clickoutside', function(e) {
736 if (this.get('visible')) {
737 // Note: we need to compare ids because for some reason - sometimes button is an Object, not a Y.Node.
738 if (!e.target.ancestor('.atto_control')) {
739 e.preventDefault();
740 this.hide();
741 }
742 }
743 }, this);
744 }
745
746}, {
747 NAME : CONTROLMENU_NAME,
748 ATTRS : {
749 /**
750 * The header for the drop down (only accessible to screen readers).
751 *
752 * @attribute headerText
753 * @type String
754 * @default ''
755 */
756 headerText : {
757 value : ''
758 }
759
760 }
761});
762
763M.editor_atto = M.editor_atto || {};
764M.editor_atto.controlmenu = CONTROLMENU;
d088a835
DW
765/**
766 * Class for cleaning ugly HTML.
767 * Rewritten JS from jquery-clean plugin.
768 *
769 * @module editor_atto
770 * @chainable
771 */
772function cleanHTML() {
773 var cleaned = this.getHTML();
774
775 // What are we doing ?
776 // We are cleaning random HTML from all over the shop into a set of useful html suitable for content.
777 // We are allowing styles etc, but not e.g. font tags, class="MsoNormal" etc.
778
779 var rules = [
780 // Source: "http://stackoverflow.com/questions/2875027/clean-microsoft-word-pasted-text-using-javascript"
781 // Source: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work"
782
783 // Remove all HTML comments.
784 {regex: /<!--[\s\S]*?-->/gi, replace: ""},
785 // Source: "http://www.1stclassmedia.co.uk/developers/clean-ms-word-formatting.php"
786 // Remove <?xml>, <\?xml>.
787 {regex: /<\\?\?xml[^>]*>/gi, replace: ""},
788 // Remove <o:blah>, <\o:blah>.
789 {regex: /<\/?\w+:[^>]*>/gi, replace: ""}, // e.g. <o:p...
790 // Remove MSO-blah, MSO:blah (e.g. in style attributes)
791 {regex: /\s*MSO[-:][^;"']*;?/gi, replace: ""},
792 // Remove empty spans
793 {regex: /<span[^>]*>(&nbsp;|\s)*<\/span>/gi, replace: ""},
794 // Remove class="Msoblah"
795 {regex: /class="Mso[^"]*"/gi, replace: ""},
796
797 // Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"
798 // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body.
799 {regex: /<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|!\[)[^>]*?>/gi, replace: ""},
800
801 // Source: "http://www.tim-jarrett.com/labs_javascript_scrub_word.php"
802 // Replace extended chars with simple text.
803 {regex: new RegExp(String.fromCharCode(8220), 'gi'), replace: '"'},
804 {regex: new RegExp(String.fromCharCode(8216), 'gi'), replace: "'"},
805 {regex: new RegExp(String.fromCharCode(8217), 'gi'), replace: "'"},
806 {regex: new RegExp(String.fromCharCode(8211), 'gi'), replace: '-'},
807 {regex: new RegExp(String.fromCharCode(8212), 'gi'), replace: '--'},
808 {regex: new RegExp(String.fromCharCode(189), 'gi'), replace: '1/2'},
809 {regex: new RegExp(String.fromCharCode(188), 'gi'), replace: '1/4'},
810 {regex: new RegExp(String.fromCharCode(190), 'gi'), replace: '3/4'},
811 {regex: new RegExp(String.fromCharCode(169), 'gi'), replace: '(c)'},
812 {regex: new RegExp(String.fromCharCode(174), 'gi'), replace: '(r)'},
813 {regex: new RegExp(String.fromCharCode(8230), 'gi'), replace: '...'}
814 ];
815
816 var i = 0, rule;
817
818 for (i = 0; i < rules.length; i++) {
819 rule = rules[i];
820 cleaned = cleaned.replace(rule.regex, rule.replace);
821 }
822
823 this.setHTML(cleaned);
824 return this;
825}
826
827Y.Node.addMethod("cleanHTML", cleanHTML);
828Y.NodeList.importMethod(Y.Node.prototype, "cleanHTML");
adca7326
DW
829
830
831}, '@VERSION@', {"requires": ["node", "io", "overlay", "escape", "event", "moodle-core-notification"]});