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