MDL-44223 Atto: Setting images to display a place holder when src is broken
[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',
3ee53a42
DW
33 WRAPPER: 'editor_atto',
34 HIGHLIGHT: 'highlight'
adca7326
DW
35};
36
37/**
38 * Atto editor main class.
39 * Common functions required by editor plugins.
40 *
41 * @package editor_atto
42 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
43 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44 */
45M.editor_atto = M.editor_atto || {
46
34f5867a
DW
47 /**
48 * List of known block level tags.
49 * Taken from "https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements".
50 *
51 * @type {Array}
52 */
53 BLOCK_TAGS : [
54 'address',
55 'article',
56 'aside',
57 'audio',
58 'blockquote',
59 'canvas',
60 'dd',
61 'div',
62 'dl',
63 'fieldset',
64 'figcaption',
65 'figure',
66 'footer',
67 'form',
68 'h1',
69 'h2',
70 'h3',
71 'h4',
72 'h5',
73 'h6',
74 'header',
75 'hgroup',
76 'hr',
77 'noscript',
78 'ol',
79 'output',
80 'p',
81 'pre',
82 'section',
83 'table',
84 'tfoot',
85 'ul',
86 'video'],
87
adca7326
DW
88 /**
89 * List of attached button handlers to prevent duplicates.
90 */
91 buttonhandlers : {},
92
93 /**
05843fd3 94 * List of attached handlers.
adca7326
DW
95 */
96 textupdatedhandlers : {},
97
98 /**
99 * List of YUI overlays for custom menus.
100 */
101 menus : {},
102
103 /**
104 * List of attached menu handlers to prevent duplicates.
105 */
106 menuhandlers : {},
107
108 /**
109 * List of file picker options for specific editor instances.
110 */
111 filepickeroptions : {},
112
113 /**
114 * List of buttons and menus that have been added to the toolbar.
115 */
116 widgets : {},
117
26f8822d
DW
118 /**
119 * List of saved selections per editor instance.
120 */
121 selections : {},
122
123 focusfromclick : false,
124
adca7326
DW
125 /**
126 * Toggle a menu.
127 * @param event e
128 */
129 showhide_menu_handler : function(e) {
130 e.preventDefault();
131 var disabled = this.getAttribute('disabled');
132 var overlayid = this.getAttribute('data-menu');
133 var overlay = M.editor_atto.menus[overlayid];
134 var menu = overlay.get('bodyContent');
135 if (overlay.get('visible') || disabled) {
136 overlay.hide();
137 menu.detach('clickoutside');
138 } else {
139 menu.on('clickoutside', function(ev) {
140 if ((ev.target.ancestor() !== this) && (ev.target !== this)) {
141 if (overlay.get('visible')) {
142 menu.detach('clickoutside');
143 overlay.hide();
144 }
145 }
146 }, this);
0fa78b80
DW
147
148 overlay.align(Y.one(Y.config.doc.body), [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
adca7326 149 overlay.show();
0fa78b80
DW
150 var icon = e.target.ancestor('button', true).one('img');
151 overlay.align(icon, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
26f8822d 152 overlay.get('boundingBox').one('a').focus();
adca7326
DW
153 }
154 },
155
156 /**
157 * Handle clicks on editor buttons.
158 * @param event e
159 */
160 buttonclicked_handler : function(e) {
161 var elementid = this.getAttribute('data-editor');
162 var plugin = this.getAttribute('data-plugin');
5ec54dd1 163 var button = this.getAttribute('data-button');
adca7326
DW
164 var handler = this.getAttribute('data-handler');
165 var overlay = M.editor_atto.menus[plugin + '_' + elementid];
fcb5b5c4
DW
166 var toolbar = M.editor_atto.get_toolbar_node(elementid);
167 var currentid = toolbar.getAttribute('aria-activedescendant');
168
169 // Right now, currentid is the id of the previously selected button.
170 if (currentid) {
171 current = Y.one('#' + currentid);
172 // We only ever want one button with a tabindex of 0 at any one time.
173 current.setAttribute('tabindex', '-1');
174 }
175 this.setAttribute('tabindex', 0);
176 // And update the activedescendant to point at the currently selected button.
177 toolbar.setAttribute('aria-activedescendant', this.generateID());
adca7326
DW
178
179 if (overlay) {
180 overlay.hide();
181 }
182
5ec54dd1 183 if (M.editor_atto.is_enabled(elementid, plugin, button)) {
adca7326
DW
184 // Pass it on.
185 handler = M.editor_atto.buttonhandlers[handler];
186 return handler(e, elementid);
187 }
188 },
189
adca7326
DW
190 /**
191 * Disable all buttons and menus in the toolbar.
192 * @param string elementid, the element id of this editor.
193 */
194 disable_all_widgets : function(elementid) {
48bdf86f 195 var plugin, element, toolbar = M.editor_atto.get_toolbar_node(elementid);
adca7326 196 for (plugin in M.editor_atto.widgets) {
48bdf86f 197 element = toolbar.one('.atto_' + plugin + '_button');
adca7326
DW
198
199 if (element) {
200 element.setAttribute('disabled', 'true');
201 }
202 }
203 },
204
48bdf86f
DW
205 /**
206 * Get the node of the original textarea element that this editor replaced.
207 *
208 * @param string elementid, the element id of this editor.
209 * @return Y.Node
210 */
211 get_textarea_node : function(elementid) {
212 // Note - it is not safe to use a CSS selector like '#' + elementid
213 // because the id may have colons in it - e.g. quiz.
214 return Y.one(document.getElementById(elementid));
215 },
216
217 /**
218 * Get the node of the toolbar container for this editor.
219 *
220 * @param string elementid, the element id of this editor.
221 * @return Y.Node
222 */
223 get_toolbar_node : function(elementid) {
224 // Note - it is not safe to use a CSS selector like '#' + elementid
225 // because the id may have colons in it - e.g. quiz.
226 return Y.one(document.getElementById(elementid + '_toolbar'));
227 },
228
229 /**
230 * Get the node of the contenteditable container for this editor.
231 *
232 * @param string elementid, the element id of this editor.
233 * @return Y.Node
234 */
235 get_editable_node : function(elementid) {
236 // Note - it is not safe to use a CSS selector like '#' + elementid
237 // because the id may have colons in it - e.g. quiz.
238 return Y.one(document.getElementById(elementid + 'editable'));
239 },
240
241 /**
242 * Determine if the specified toolbar button/menu is enabled.
243 * @param string elementid, the element id of this editor.
244 * @param string plugin, the plugin that created the button/menu.
5ec54dd1 245 * @param string buttonname, optional - used when a plugin has multiple buttons.
48bdf86f 246 */
5ec54dd1
DW
247 is_enabled : function(elementid, plugin, button) {
248 var buttonpath = plugin;
249 if (button) {
250 buttonpath += '_' + button;
251 }
252 var element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + buttonpath + '_button');
48bdf86f
DW
253
254 return !element.hasAttribute('disabled');
255 },
256
3ee53a42
DW
257 /**
258 * Determine if the specified toolbar button/menu is highlighted.
259 * @param string elementid, the element id of this editor.
260 * @param string plugin, the plugin that created the button/menu.
261 * @param string buttonname, optional - used when a plugin has multiple buttons.
262 */
263 is_highlighted : function(elementid, plugin, button) {
264 var buttonpath = plugin;
265 if (button) {
266 buttonpath += '_' + button;
267 }
268 var element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + buttonpath + '_button');
269
270 return !element.hasClass(CSS.HIGHLIGHT);
271 },
272
adca7326
DW
273 /**
274 * Enable a single widget in the toolbar.
275 * @param string elementid, the element id of this editor.
276 * @param string plugin, the name of the plugin that created the widget.
5ec54dd1 277 * @param string buttonname, optional - used when a plugin has multiple buttons.
adca7326 278 */
5ec54dd1
DW
279 enable_widget : function(elementid, plugin, button) {
280 var buttonpath = plugin;
281 if (button) {
282 buttonpath += '_' + button;
283 }
284 var element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + buttonpath + '_button');
adca7326
DW
285
286 if (element) {
287 element.removeAttribute('disabled');
288 }
289 },
290
3ee53a42
DW
291 /**
292 * Highlight a single widget in the toolbar.
293 * @param string elementid, the element id of this editor.
294 * @param string plugin, the name of the plugin that created the widget.
295 * @param string buttonname, optional - used when a plugin has multiple buttons.
296 */
297 add_widget_highlight : function(elementid, plugin, button) {
298 var buttonpath = plugin;
299 if (button) {
300 buttonpath += '_' + button;
301 }
302 var element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + buttonpath + '_button');
303
304 if (element) {
305 element.addClass(CSS.HIGHLIGHT);
306 }
307 },
308
309 /**
310 * Unhighlight a single widget in the toolbar.
311 * @param string elementid, the element id of this editor.
312 * @param string plugin, the name of the plugin that created the widget.
313 * @param string buttonname, optional - used when a plugin has multiple buttons.
314 */
315 remove_widget_highlight : function(elementid, plugin, button) {
316 var buttonpath = plugin;
317 if (button) {
318 buttonpath += '_' + button;
319 }
320 var element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + buttonpath + '_button');
321
322 if (element) {
323 element.removeClass(CSS.HIGHLIGHT);
324 }
325 },
326
adca7326
DW
327 /**
328 * Enable all buttons and menus in the toolbar.
329 * @param string elementid, the element id of this editor.
330 */
331 enable_all_widgets : function(elementid) {
5ec54dd1
DW
332 var path, element;
333 for (path in M.editor_atto.widgets) {
334 element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + path + '_button');
adca7326
DW
335
336 if (element) {
337 element.removeAttribute('disabled');
338 }
339 }
340 },
341
342 /**
343 * Add a content update handler to be called whenever the content is updated.
adca7326
DW
344 *
345 * @param string elementid - the id of the textarea we created this editor from.
346 * @handler function callback - The function to do the cleaning.
347 * @param object context - the context to set for the callback.
348 * @handler function handler - A function to call when the button is clicked.
349 */
350 add_text_updated_handler : function(elementid, callback) {
351 if (!(elementid in M.editor_atto.textupdatedhandlers)) {
352 M.editor_atto.textupdatedhandlers[elementid] = [];
353 }
354 M.editor_atto.textupdatedhandlers[elementid].push(callback);
355 },
356
357 /**
358 * Add a button to the toolbar belonging to the editor for element with id "elementid".
359 * @param string elementid - the id of the textarea we created this editor from.
360 * @param string plugin - the plugin defining the button
361 * @param string icon - the html used for the content of the button
362 * @param string groupname - the group the button should be appended to.
363 * @param array entries - List of menu entries with the string (entry.text) and the handlers (entry.handler).
5ec54dd1
DW
364 * @param string buttonname - (optional) a name for the button. Required if a plugin creates more than one button.
365 * @param string buttontitle - (optional) a title for the button. Required if a plugin creates more than one button.
0fa78b80 366 * @param int overlaywidth - the overlay width size in 'ems'.
534cf7b7 367 * @param string menucolor - menu icon background color
adca7326 368 */
5ec54dd1 369 add_toolbar_menu : function(elementid, plugin, iconurl, groupname, entries, buttonname, buttontitle, overlaywidth, menucolor) {
48bdf86f
DW
370 var toolbar = M.editor_atto.get_toolbar_node(elementid),
371 group = toolbar.one('.atto_group.' + groupname + '_group'),
adca7326
DW
372 currentfocus,
373 button,
5ec54dd1 374 buttonpath,
adca7326
DW
375 expimgurl;
376
5ec54dd1
DW
377 if (buttonname) {
378 buttonpath = plugin + '_' + buttonname;
379 } else {
380 buttonname = '';
381 buttonpath = plugin;
382 }
383
384 if (!buttontitle) {
385 buttontitle = M.util.get_string('pluginname', 'atto_' + plugin);
386 }
387
534cf7b7
RW
388 if ((typeof overlaywidth) === 'undefined') {
389 overlaywidth = '14';
390 }
391 if ((typeof menucolor) === 'undefined') {
392 menucolor = 'transparent';
393 }
394
adca7326
DW
395 if (!group) {
396 group = Y.Node.create('<div class="atto_group ' + groupname + '_group"></div>');
397 toolbar.append(group);
398 }
adca7326 399 expimgurl = M.util.image_url('t/expanded', 'moodle');
5ec54dd1 400 button = Y.Node.create('<button class="atto_' + buttonpath + '_button atto_hasmenu" ' +
adca7326
DW
401 'data-editor="' + Y.Escape.html(elementid) + '" ' +
402 'tabindex="-1" ' +
0012a946 403 'type="button" ' +
5ec54dd1
DW
404 'data-menu="' + buttonpath + '_' + elementid + '" ' +
405 'title="' + Y.Escape.html(buttontitle) + '">' +
534cf7b7
RW
406 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" '+
407 'style="background-color:' + menucolor + ';" src="' + iconurl + '"/>' +
adca7326
DW
408 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" src="' + expimgurl + '"/>' +
409 '</button>');
410
411 group.append(button);
412
413 currentfocus = toolbar.getAttribute('aria-activedescendant');
414 if (!currentfocus) {
fcb5b5c4 415 // Initially set the first button in the toolbar to be the default on keyboard focus.
adca7326
DW
416 button.setAttribute('tabindex', '0');
417 toolbar.setAttribute('aria-activedescendant', button.generateID());
418 }
419
420 // Save the name of the plugin.
5ec54dd1 421 M.editor_atto.widgets[buttonpath] = buttonpath;
adca7326 422
5ec54dd1 423 var menu = Y.Node.create('<div class="atto_' + buttonpath + '_menu' +
534cf7b7
RW
424 ' atto_menu" data-editor="' + Y.Escape.html(elementid) + '"' +
425 ' style="min-width:' + (overlaywidth-2) + 'em"' +
426 '"></div>');
adca7326
DW
427 var i = 0, entry = {};
428
429 for (i = 0; i < entries.length; i++) {
430 entry = entries[i];
431
432 menu.append(Y.Node.create('<div class="atto_menuentry">' +
5ec54dd1 433 '<a href="#" class="atto_' + buttonpath + '_action_' + i + '" ' +
adca7326
DW
434 'data-editor="' + Y.Escape.html(elementid) + '" ' +
435 'data-plugin="' + Y.Escape.html(plugin) + '" ' +
5ec54dd1
DW
436 'data-button="' + Y.Escape.html(buttonname) + '" ' +
437 'data-handler="' + Y.Escape.html(buttonpath + '_action_' + i) + '">' +
adca7326
DW
438 entry.text +
439 '</a>' +
440 '</div>'));
441 if (!M.editor_atto.buttonhandlers[plugin + '_action_' + i]) {
5ec54dd1
DW
442 Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + buttonpath + '_action_' + i);
443 // Activate the link on space or enter.
444 Y.one('body').delegate('key', M.editor_atto.buttonclicked_handler, '32,enter', '.atto_' + buttonpath + '_action_' + i);
445 M.editor_atto.buttonhandlers[buttonpath + '_action_' + i] = entry.handler;
adca7326
DW
446 }
447 }
448
5ec54dd1
DW
449 if (!M.editor_atto.buttonhandlers[buttonpath]) {
450 Y.one('body').delegate('click', M.editor_atto.showhide_menu_handler, '.atto_' + buttonpath + '_button');
451 M.editor_atto.buttonhandlers[buttonpath] = true;
adca7326
DW
452 }
453
454 var overlay = new M.core.dialogue({
455 bodyContent : menu,
456 visible : false,
534cf7b7 457 width: overlaywidth + 'em',
adca7326
DW
458 lightbox: false,
459 closeButton: false,
55c0403c 460 center : false
adca7326
DW
461 });
462
5ec54dd1 463 M.editor_atto.menus[buttonpath + '_' + elementid] = overlay;
55c0403c 464 overlay.align(button, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
adca7326
DW
465 overlay.hide();
466 overlay.headerNode.hide();
0fa78b80 467 overlay.render();
adca7326
DW
468 },
469
470 /**
471 * Add a button to the toolbar belonging to the editor for element with id "elementid".
472 * @param string elementid - the id of the textarea we created this editor from.
473 * @param string plugin - the plugin defining the button.
55c0403c 474 * @param string icon - the url to the image for the icon
adca7326
DW
475 * @param string groupname - the group the button should be appended to.
476 * @handler function handler- A function to call when the button is clicked.
5ec54dd1
DW
477 * @param string buttonname - (optional) a name for the button. Required if a plugin creates more than one button.
478 * @param string buttontitle - (optional) a title for the button. Required if a plugin creates more than one button.
adca7326 479 */
5ec54dd1 480 add_toolbar_button : function(elementid, plugin, iconurl, groupname, handler, buttonname, buttontitle) {
48bdf86f
DW
481 var toolbar = M.editor_atto.get_toolbar_node(elementid),
482 group = toolbar.one('.atto_group.' + groupname + '_group'),
adca7326 483 button,
5ec54dd1 484 buttonpath,
55c0403c 485 currentfocus;
adca7326 486
5ec54dd1
DW
487 if (buttonname) {
488 buttonpath = plugin + '_' + buttonname;
489 } else {
490 buttonname = '';
491 buttonpath = plugin;
492 }
493
494 if (!buttontitle) {
495 buttontitle = M.util.get_string('pluginname', 'atto_' + plugin);
fe0d2477
JM
496 }
497
adca7326
DW
498 if (!group) {
499 group = Y.Node.create('<div class="atto_group ' + groupname +'_group"></div>');
500 toolbar.append(group);
501 }
5ec54dd1 502 button = Y.Node.create('<button class="atto_' + buttonpath + '_button" ' +
adca7326
DW
503 'data-editor="' + Y.Escape.html(elementid) + '" ' +
504 'data-plugin="' + Y.Escape.html(plugin) + '" ' +
5ec54dd1 505 'data-button="' + Y.Escape.html(buttonname) + '" ' +
adca7326 506 'tabindex="-1" ' +
5ec54dd1
DW
507 'data-handler="' + Y.Escape.html(buttonpath) + '" ' +
508 'title="' + Y.Escape.html(buttontitle) + '">' +
55c0403c 509 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" src="' + iconurl + '"/>' +
adca7326
DW
510 '</button>');
511
512 group.append(button);
513
514 currentfocus = toolbar.getAttribute('aria-activedescendant');
515 if (!currentfocus) {
fcb5b5c4 516 // Initially set the first button in the toolbar to be the default on keyboard focus.
adca7326
DW
517 button.setAttribute('tabindex', '0');
518 toolbar.setAttribute('aria-activedescendant', button.generateID());
519 }
520
521 // We only need to attach this once.
5ec54dd1
DW
522 if (!M.editor_atto.buttonhandlers[buttonpath]) {
523 Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + buttonpath + '_button');
524 M.editor_atto.buttonhandlers[buttonpath] = handler;
adca7326
DW
525 }
526
527 // Save the name of the plugin.
5ec54dd1 528 M.editor_atto.widgets[buttonpath] = buttonpath;
adca7326
DW
529
530 },
531
532 /**
533 * Work out if the cursor is in the editable area for this editor instance.
534 * @param string elementid of this editor
535 * @return bool
536 */
537 is_active : function(elementid) {
538 var selection = M.editor_atto.get_selection();
539
540 if (selection.length) {
541 selection = selection.pop();
542 }
543
544 var node = null;
545 if (selection.parentElement) {
546 node = Y.one(selection.parentElement());
547 } else {
548 node = Y.one(selection.startContainer);
549 }
550
34f5867a
DW
551 var editable = M.editor_atto.get_editable_node(elementid);
552
553 return node && editable.contains(node);
adca7326
DW
554 },
555
556 /**
557 * Focus on the editable area for this editor.
558 * @param string elementid of this editor
559 */
560 focus : function(elementid) {
48bdf86f 561 M.editor_atto.get_editable_node(elementid).focus();
adca7326
DW
562 },
563
564 /**
565 * Initialise the editor
566 * @param object params for this editor instance.
567 */
568 init : function(params) {
adca7326
DW
569 var wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />');
570 var atto = Y.Node.create('<div id="' + params.elementid + 'editable" ' +
571 'contenteditable="true" ' +
26f8822d 572 'role="textbox" ' +
adca7326 573 'spellcheck="true" ' +
26f8822d 574 'aria-live="off" ' +
bdfbdeeb
SH
575 'class="' + CSS.CONTENT + '" '+
576 'data-editor="' + params.elementid + '" />');
adca7326 577
26f8822d 578 var toolbar = Y.Node.create('<div class="' + CSS.TOOLBAR + '" id="' + params.elementid + '_toolbar" role="toolbar" aria-live="off"/>');
adca7326
DW
579
580 // Editable content wrapper.
581 var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />');
48bdf86f 582 var textarea = M.editor_atto.get_textarea_node(params.elementid);
26f8822d
DW
583 var label = Y.one('[for="' + params.elementid + '"]');
584
585 // Add a labelled-by attribute to the contenteditable.
586 if (label) {
587 label.generateID();
588 atto.setAttribute('aria-labelledby', label.get("id"));
589 toolbar.setAttribute('aria-labelledby', label.get("id"));
590 }
b269f635 591
adca7326
DW
592 content.appendChild(atto);
593
594 // Add everything to the wrapper.
595 wrapper.appendChild(toolbar);
596 wrapper.appendChild(content);
597
4c37c1f4 598 // Style the editor.
adca7326
DW
599 atto.setStyle('minHeight', (1.2 * (textarea.getAttribute('rows'))) + 'em');
600
601 // Copy text to editable div.
602 atto.append(textarea.get('value'));
603
d088a835
DW
604 // Clean it.
605 atto.cleanHTML();
606
adca7326
DW
607 // Add the toolbar and editable zone to the page.
608 textarea.get('parentNode').insert(wrapper, textarea);
26f8822d
DW
609
610 // Disable odd inline CSS styles.
f6bef145 611 M.editor_atto.disable_css_styling();
4c37c1f4 612
adca7326
DW
613 // Hide the old textarea.
614 textarea.hide();
3ee53a42
DW
615
616 this.publish_events();
617 atto.on('atto:selectionchanged', this.save_selection, this, params.elementid);
26f8822d
DW
618 atto.on('focus', this.restore_selection, this, params.elementid);
619 // Do not restore selection when focus is from a click event.
620 atto.on('mousedown', function() { this.focusfromclick = true; }, this);
adca7326 621
26f8822d 622 // Copy the current value back to the textarea when focus leaves us and save the current selection.
adca7326 623 atto.on('blur', function() {
26f8822d 624 this.focusfromclick = false;
48bdf86f 625 this.text_updated(params.elementid);
adca7326
DW
626 }, this);
627
628 // Listen for Arrow left and Arrow right keys.
629 Y.one(Y.config.doc.body).delegate('key',
630 this.keyboard_navigation,
631 'down:37,39',
48bdf86f 632 '#' + params.elementid + '_toolbar',
adca7326 633 this,
48bdf86f 634 params.elementid);
adca7326
DW
635
636 // Save the file picker options for later.
48bdf86f 637 M.editor_atto.filepickeroptions[params.elementid] = params.filepickeroptions;
55c0403c
DW
638
639 // Init each of the plugins
fcb5b5c4 640 var i, j, group, plugin;
55c0403c 641 for (i = 0; i < params.plugins.length; i++) {
fcb5b5c4 642 group = params.plugins[i].group;
55c0403c 643 for (j = 0; j < params.plugins[i].plugins.length; j++) {
fcb5b5c4 644 plugin = params.plugins[i].plugins[j];
48bdf86f 645 plugin.params.elementid = params.elementid;
55c0403c
DW
646 plugin.params.group = group;
647
648 M['atto_' + plugin.name].init(plugin.params);
649 }
650 }
fcb5b5c4
DW
651
652 // Let the plugins run some init code once all plugins are in the page.
653 for (i = 0; i < params.plugins.length; i++) {
654 group = params.plugins[i].group;
655 for (j = 0; j < params.plugins[i].plugins.length; j++) {
656 plugin = params.plugins[i].plugins[j];
657 plugin.params.elementid = params.elementid;
658 plugin.params.group = group;
659
660 if (typeof M['atto_' + plugin.name].after_init !== 'undefined') {
661 M['atto_' + plugin.name].after_init(plugin.params);
662 }
663 }
664 }
3ee53a42
DW
665
666 },
667
668 /**
669 * Add code to trigger the a set of custom events on either the toolbar, or the contenteditable node
670 * that can be listened to by plugins (or even this class).
671 * @param string elementid - the id of the textarea we created this editor from.
672 */
673 publish_events : function() {
674 Y.Event.define("atto:selectionchanged", {
675 /**
676 * Catch the keydown/mouseup events, and fire a synthetic event for the change event.
677 * @param {Y.Event} e - The real event that triggers this synthetic one.
678 * @param {Object} params - Object containing the subscription object and the notifier.
679 */
680 changeHandler: function (e, params) {
681 // Add 3 properties to the event.
682 // 1. add the elementid.
683 var elementid = params.sub.node.getAttribute('id');
684 // Strip 'editable' from the end of the id.
685 elementid = elementid.substring(0, elementid.length - 8);
686 e.elementid = elementid;
687
688 // 2. The list of leaf nodes contained in the selection.
689 e.selectedNodes = M.editor_atto.get_selected_nodes(elementid);
690 // 3. The current selection (Range)
691 e.selection = M.editor_atto.get_selection();
692 params.notifier.fire(e);
693 },
694
695 /**
696 * Subscribe to the atto:selectionchanged event.
697 * @param {Y.Node} node - The node for the subscription - must be the contenteditable node.
698 * @param {Y.Subscription} sub - YUI Subscription object.
699 * @param {Y.Event.Notifier} notifier - YUI notifier object.
700 */
701 on: function (node, sub, notifier) {
702 var params = { notifier: notifier, sub: sub };
703 sub.attoKeyDownDetach = node.on('keydown', Y.throttle(this.changeHandler, 100), this, params);
704 sub.attoMouseUpDetach = node.on('mouseup', Y.throttle(this.changeHandler, 100), this, params);
705 sub.attoFocusDetach = node.on('focus', Y.throttle(this.changeHandler, 100), this, params);
706 },
707
708 /**
709 * Detach the atto:selectionchanged event.
710 * @param {Y.Node} node - The node for the subscription - must be the contenteditable node.
711 * @param {Y.Subscription} sub - YUI Subscription object.
712 */
713 detach: function (node, sub) {
714 sub.attoKeyDownDetach.detach();
715 sub.attoMouseUpDetach.detach();
716 sub.attoFocusDetach.detach();
717 },
718
719 /**
720 * Delegate the atto:selectionchanged event.
721 *
722 * @param {Y.Node} node - The node for the subscription - must be the contenteditable node.
723 * @param {Y.Subscription} sub - YUI Subscription object.
724 * @param {Y.Event.Notifier} notifier - YUI notifier object.
725 * @param {String} filter - CSS selector for the filter.
726 */
727 delegate: function(node, sub, notifier, filter) {
728 var params = { notifier: notifier, sub: sub };
729 sub.attoKeyDownDetachDelegate = node.delegate('keydown', Y.throttle(this.changeHandler, 100), filter, this, params);
730 sub.attoMouseUpDetachDelegate = node.delegate('mouseup', Y.throttle(this.changeHandler, 100), filter, this, params);
731 sub.attoFocusDetachDelegate = node.delegate('focus', Y.throttle(this.changeHandler, 100), filter, this, params);
732 },
733
734 /**
735 * Detach a delegated atto:selectionchanged event.
736 *
737 * @param {Y.Node} node - The node for the subscription - must be the contenteditable node.
738 * @param {Y.Subscription} sub - YUI Subscription object.
739 */
740 detachDelegate: function(node, sub) {
741 sub.attoKeyDownDetachDelegate.detach();
742 sub.attoMouseUpDetachDelegate.detach();
743 sub.attoFocusDetachDelegate.detach();
744 }
745 });
adca7326
DW
746 },
747
748 /**
749 * The text in the contenteditable region has been updated,
750 * clean and copy the buffer to the text area.
751 * @param string elementid - the id of the textarea we created this editor from.
752 */
753 text_updated : function(elementid) {
48bdf86f 754 var textarea = M.editor_atto.get_textarea_node(elementid),
adca7326
DW
755 cleancontent = this.get_clean_html(elementid);
756 textarea.set('value', cleancontent);
dad09216
FM
757 // Trigger the onchange callback on the textarea, essentially to notify moodle-core-formchangechecker.
758 textarea.simulate('change');
adca7326
DW
759 // Trigger handlers for this action.
760 var i = 0;
761 if (elementid in M.editor_atto.textupdatedhandlers) {
762 for (i = 0; i < M.editor_atto.textupdatedhandlers[elementid].length; i++) {
763 var callback = M.editor_atto.textupdatedhandlers[elementid][i];
764 callback(elementid);
765 }
766 }
767 },
768
769 /**
770 * Remove all YUI ids from the generated HTML.
771 * @param string elementid - the id of the textarea we created this editor from.
772 * @return string HTML stripped of YUI ids
773 */
774 get_clean_html : function(elementid) {
48bdf86f 775 var atto = M.editor_atto.get_editable_node(elementid).cloneNode(true);
adca7326
DW
776
777 Y.each(atto.all('[id]'), function(node) {
778 var id = node.get('id');
779 if (id.indexOf('yui') === 0) {
780 node.removeAttribute('id');
781 }
782 });
783
d088a835
DW
784 // Remove any and all nasties from source.
785 atto.cleanHTML();
786
adca7326
DW
787 return atto.getHTML();
788 },
789
790 /**
791 * Implement arrow key navigation for the buttons in the toolbar.
792 * @param Event e - the keyboard event.
793 * @param string elementid - the id of the textarea we created this editor from.
794 */
795 keyboard_navigation : function(e, elementid) {
796 var buttons,
797 current,
798 currentid,
48bdf86f
DW
799 currentindex,
800 toolbar = M.editor_atto.get_toolbar_node(elementid);
adca7326
DW
801
802 e.preventDefault();
803
fcb5b5c4
DW
804 // This workaround is because we cannot do ".atto_group:not([hidden]) button" in ie8 (even with selector-css3).
805 // Create an empty NodeList.
806 buttons = toolbar.all('empty');
807 toolbar.all('.atto_group').each(function(group) {
808 if (!group.hasAttribute('hidden')) {
809 // Append the visible buttons to the buttons list.
810 buttons = buttons.concat(group.all('button'));
811 }
812 });
813 // The currentid is the id of the previously selected button.
48bdf86f 814 currentid = toolbar.getAttribute('aria-activedescendant');
adca7326
DW
815 if (!currentid) {
816 return;
817 }
fcb5b5c4 818 // We only ever want one button with a tabindex of 0.
adca7326
DW
819 current = Y.one('#' + currentid);
820 current.setAttribute('tabindex', '-1');
821
822 currentindex = buttons.indexOf(current);
823
824 if (e.keyCode === 37) {
825 // Left
826 currentindex--;
827 if (currentindex < 0) {
828 currentindex = buttons.size()-1;
829 }
830 } else {
831 // Right
832 currentindex++;
833 if (currentindex >= buttons.size()) {
834 currentindex = 0;
835 }
836 }
fcb5b5c4 837 // Currentindex has been updated to point to the new button.
adca7326
DW
838 current = buttons.item(currentindex);
839 current.setAttribute('tabindex', '0');
840 current.focus();
48bdf86f 841 toolbar.setAttribute('aria-activedescendant', current.generateID());
adca7326
DW
842 },
843
b269f635
DW
844 /**
845 * Should we show the filepicker for this filetype?
846 *
847 * @param string elementid for this editor instance.
848 * @param string type The media type for the file picker
849 * @return boolean
850 */
851 can_show_filepicker : function(elementid, type) {
852 var options = M.editor_atto.filepickeroptions[elementid];
853 return ((typeof options[type]) !== "undefined");
854 },
855
adca7326
DW
856 /**
857 * Show the filepicker.
858 * @param string elementid for this editor instance.
859 * @param string type The media type for the file picker
860 * @param function callback
861 */
862 show_filepicker : function(elementid, type, callback) {
863 Y.use('core_filepicker', function (Y) {
864 var options = M.editor_atto.filepickeroptions[elementid][type];
865
866 options.formcallback = callback;
adca7326
DW
867
868 M.core_filepicker.show(Y, options);
869 });
870 },
871
872 /**
873 * Create a cross browser selection object that represents a yui node.
874 * @param Node yui node for the selection
875 * @return range (browser dependent)
876 */
877 get_selection_from_node: function(node) {
878 var range;
879
880 if (window.getSelection) {
881 range = document.createRange();
882
883 range.setStartBefore(node.getDOMNode());
884 range.setEndAfter(node.getDOMNode());
885 return [range];
886 } else if (document.selection) {
887 range = document.body.createTextRange();
888 range.moveToElementText(node.getDOMNode());
889 return range;
890 }
891 return false;
892 },
893
26f8822d
DW
894 /**
895 * Save the current selection on blur, allows more reliable keyboard navigation.
896 * @param Y.Event event
897 * @param string elementid
898 */
899 save_selection : function(event, elementid) {
900 if (this.is_active(elementid)) {
901 var sel = this.get_selection();
902 if (sel.length > 0) {
903 this.selections[elementid] = sel;
904 }
905 }
906 },
907
908 /**
909 * Restore any current selection when the editor gets focus again.
910 * @param Y.Event event
911 * @param string elementid
912 */
913 restore_selection : function(event, elementid) {
914 event.preventDefault();
915 if (!this.focusfromclick) {
916 if (typeof this.selections[elementid] !== "undefined") {
917 this.set_selection(this.selections[elementid]);
918 }
919 }
920 this.focusfromclick = false;
921 },
922
adca7326
DW
923 /**
924 * Get the selection object that can be passed back to set_selection.
925 * @return range (browser dependent)
926 */
927 get_selection : function() {
928 if (window.getSelection) {
929 var sel = window.getSelection();
930 var ranges = [], i = 0;
931 for (i = 0; i < sel.rangeCount; i++) {
932 ranges.push(sel.getRangeAt(i));
933 }
934 return ranges;
935 } else if (document.selection) {
936 // IE < 9
937 if (document.selection.createRange) {
938 return document.selection.createRange();
939 }
940 }
941 return false;
942 },
943
944 /**
945 * Check that a YUI node it at least partly contained by the selection.
adca7326
DW
946 * @param Y.Node node
947 * @return boolean
948 */
949 selection_contains_node : function(node) {
950 var range, sel;
951 if (window.getSelection) {
952 sel = window.getSelection();
953
954 if (sel.containsNode) {
955 return sel.containsNode(node.getDOMNode(), true);
956 }
957 }
958 sel = document.selection.createRange();
959 range = sel.duplicate();
960 range.moveToElementText(node.getDOMNode());
961 return sel.inRange(range);
962 },
963
3ee53a42
DW
964 /**
965 * Runs a selector on each node in the selection and will only return true if all nodes
966 * in the selection match the filter.
967 *
968 * @param {String} elementid
969 * @param {String} selector
970 * @param {NodeList} selectednodes (Optional) - for performance we can pass this instead of looking it up.
971 * @return {Boolean}
972 */
973 selection_filter_matches : function(elementid, selector, selectednodes) {
974 var result = true;
975
976 if (!selectednodes) {
977 // Find this because it was not passed as a param.
978 selectednodes = M.editor_atto.get_selected_nodes(elementid);
979 }
980 selector = '.' + CSS.CONTENT + ' ' + selector;
981
982 if (selectednodes.size() === 0) {
983 return false;
984 }
985 selectednodes.each(function(node) {
986 if (!node.ancestor(selector, true)) {
987 result = false;
988 }
989 });
990 return result;
991 },
992
993 /**
994 * Returns a list of nodes that are ancestors of the selection nodes,
995 * and match the specified css selector (and are contained within the editable div).
996 *
997 * @param {String} elementid
998 * @param {String} selector
999 * @return Y.NodeList
1000 */
1001 selection_filter : function(elementid, selector) {
1002 var selectednodes = M.editor_atto.get_selected_nodes(elementid);
1003 var result = new Y.NodeList();
1004 selector = '.' + CSS.CONTENT + ' ' + selector;
1005 selectednodes.each(function(node) {
1006 node.ancestors(selector, true).each(function(match) {
1007 if (result.indexOf(match) === -1) {
1008 result.push(match);
1009 }
1010 });
1011 });
1012 return result;
1013 },
1014
1015 /**
1016 * Get the deepest possible list of nodes in the current selection.
1017 * @param {String} elementid
1018 * @return Y.NodeList
1019 */
1020 get_selected_nodes : function(elementid) {
1021
1022 var editable = M.editor_atto.get_editable_node(elementid);
1023 var startnode = M.editor_atto.get_selection_start_container();
1024 var endnode = M.editor_atto.get_selection_end_container();
1025
1026 if (!startnode || !endnode) {
1027 return new Y.NodeList();
1028 }
1029
1030 /**
1031 * Recursive function to walk the dom tree between 2 nodes and build
1032 * a list of leaf nodes.
1033 * @param {Y.Node} node - The current node.
1034 * @param {Y.Node} endnode - The node to stop at.
1035 * @param {Y.Node} boundingnode - A node that will contain the selection - do not result results outside this node.
1036 * @param {Integer} skipchildren - When returning from a child to a parent, this is how many children to skip.
1037 * @param {Boolean} isstart - Only true for the starting condition. Used in the case when startnode == endnode.
1038 * @return {Y.NodeList} The list of leaf nodes found.
1039 */
1040 var find_leaf_nodes = function(node, endnode, boundingnode, skipchildren, isstart) {
1041 var leafnodes = new Y.NodeList();
1042 if (endnode === node) {
1043 // If there are child nodes and we didn't come from a child node, then skip this end condition.
1044 if (!(endnode === node && startnode === node && isstart && node.hasChildNodes())) {
1045 // If the end node is a leaf - return it.
1046 if (!node.hasChildNodes()) {
1047 leafnodes.push(node);
1048 }
1049 // We reached the end of the selection.
1050 return leafnodes;
1051 }
1052 }
1053
1054 // Node is not a leaf, look at it's children.
1055 if (node.hasChildNodes()) {
1056 var children = node.get('childNodes');
1057 // Skipchildren is used when returning from a child node, to make the parent look at the next child node.
1058 var child = children.item(skipchildren);
1059 if (child) {
1060 // There was a child node, find all the leaf nodes in it.
1061 leafnodes.concat(find_leaf_nodes(child, endnode, boundingnode, 0, false));
1062 }
1063 } else {
1064 // This node has no children - so save it.
1065 leafnodes.push(node);
1066 }
1067 // If we hit the bounding node - this is an end condition.
1068 if (node === boundingnode) {
1069 return leafnodes;
1070 }
1071 // We have added all the nodes in this branch, look at the next child of the parent.
1072 var parent = node.ancestor();
1073 if (parent) {
1074 var currentindex = parent.get('childNodes').indexOf(node);
1075 leafnodes.concat(find_leaf_nodes(parent, endnode, boundingnode, currentindex + 1, false));
1076 }
1077 return leafnodes;
1078 };
1079
1080 // Kick off the recursive function.
1081 return find_leaf_nodes(startnode, endnode, editable, 0, true);
1082 },
1083
1084 /**
1085 * Get the first node that contains the current selection.
1086 * @return DOMNode or false
1087 */
1088 get_selection_start_container : function() {
1089 var selection = M.editor_atto.get_selection();
1090
1091 if (selection.length) {
1092 selection = selection.pop();
1093 }
1094
1095 if (selection.startContainer) {
1096 return Y.one(selection.startContainer);
1097 } else if (selection.parentElement) {
1098 var range = selection.duplicate();
1099
1100 range.collapse(true);
1101 return Y.one(range.parentElement());
1102 }
1103 },
1104
1105 /**
1106 * Get the last node that contains the current selection.
1107 * @return DOMNode or false
1108 */
1109 get_selection_end_container : function() {
1110 var selection = M.editor_atto.get_selection();
1111
1112 if (selection.length) {
1113 selection = selection.pop();
1114 }
1115
1116 if (selection.endContainer) {
1117 return Y.one(selection.endContainer);
1118 } else if (selection.parentElement) {
1119 var range = selection.duplicate();
1120
1121 range.collapse(false);
1122 return Y.one(range.parentElement());
1123 }
1124 return false;
1125 },
1126
adca7326
DW
1127 /**
1128 * Get the dom node representing the common anscestor of the selection nodes.
34f5867a 1129 * @return DOMNode or false
adca7326
DW
1130 */
1131 get_selection_parent_node : function() {
1132 var selection = M.editor_atto.get_selection();
34f5867a
DW
1133 if (selection.length) {
1134 selection = selection.pop();
1135 }
1136
1137 if (selection.commonAncestorContainer) {
1138 return selection.commonAncestorContainer;
1139 } else if (selection.parentElement) {
1140 return selection.parentElement();
adca7326 1141 }
34f5867a
DW
1142 // No selection
1143 return false;
adca7326
DW
1144 },
1145
1146 /**
1147 * Get the list of child nodes of the selection.
1148 * @return DOMNode[]
1149 */
1150 get_selection_text : function() {
1151 var selection = M.editor_atto.get_selection();
1152 if (selection.length > 0 && selection[0].cloneContents) {
1153 return selection[0].cloneContents();
1154 }
1155 },
1156
1157 /**
1158 * Set the current selection. Used to restore a selection.
1159 */
1160 set_selection : function(selection) {
1161 var sel, i;
1162
1163 if (window.getSelection) {
1164 sel = window.getSelection();
1165 sel.removeAllRanges();
1166 for (i = 0; i < selection.length; i++) {
1167 sel.addRange(selection[i]);
1168 }
1169 } else if (document.selection) {
1170 // IE < 9
1171 if (selection.select) {
1172 selection.select();
1173 }
1174 }
34f5867a
DW
1175 },
1176
1177 /**
1178 * Change the formatting for the current selection.
1179 * Also changes the selection to the newly formatted block (allows applying multiple styles to a block).
1180 *
1181 * @param {String} elementid - The editor elementid.
1182 * @param {String} blocktag - Change the block level tag to this. Empty string, means do not change the tag.
1183 * @param {Object} attributes - The keys and values for attributes to be added/changed in the block tag.
1184 * @return Y.Node - if there was a selection.
1185 */
1186 format_selection_block : function(elementid, blocktag, attributes) {
1187 // First find the nearest ancestor of the selection that is a block level element.
1188 var selectionparentnode = M.editor_atto.get_selection_parent_node(),
1189 boundary,
1190 cell,
1191 nearestblock,
1192 newcontent,
1193 match,
1194 replacement;
1195
1196 if (!selectionparentnode) {
1197 // No selection, nothing to format.
1198 return;
1199 }
1200
1201 boundary = M.editor_atto.get_editable_node(elementid);
1202
1203 selectionparentnode = Y.one(selectionparentnode);
1204
1205 // If there is a table cell in between the selectionparentnode and the boundary,
1206 // move the boundary to the table cell.
1207 // This is because we might have a table in a div, and we select some text in a cell,
1208 // want to limit the change in style to the table cell, not the entire table (via the outer div).
1209 cell = selectionparentnode.ancestor(function (node) {
1210 var tagname = node.get('tagName');
1211 if (tagname) {
1212 tagname = tagname.toLowerCase();
1213 }
1214 return (node === boundary) ||
1215 (tagname === 'td') ||
1216 (tagname === 'th');
1217 }, true);
1218
1219 if (cell) {
1220 // Limit the scope to the table cell.
1221 boundary = cell;
1222 }
1223
1224 nearestblock = selectionparentnode.ancestor(M.editor_atto.BLOCK_TAGS.join(', '), true);
1225 if (nearestblock) {
1226 // Check that the block is contained by the boundary.
1227 match = nearestblock.ancestor(function (node) {
1228 return node === boundary;
1229 }, false);
1230
1231 if (!match) {
1232 nearestblock = false;
1233 }
1234 }
1235
1236 // No valid block element - make one.
1237 if (!nearestblock) {
1238 // There is no block node in the content, wrap the content in a p and use that.
1239 newcontent = Y.Node.create('<p></p>');
1240 boundary.get('childNodes').each(function (child) {
1241 newcontent.append(child.remove());
1242 });
1243 boundary.append(newcontent);
1244 nearestblock = newcontent;
1245 }
1246
1247 // Guaranteed to have a valid block level element contained in the contenteditable region.
1248 // Change the tag to the new block level tag.
1249 if (blocktag && blocktag !== '') {
1250 // Change the block level node for a new one.
1251 replacement = Y.Node.create('<' + blocktag + '></' + blocktag + '>');
1252 // Copy all attributes.
1253 replacement.setAttrs(nearestblock.getAttrs());
1254 // Copy all children.
1255 nearestblock.get('childNodes').each(function (child) {
1256 child.remove();
1257 replacement.append(child);
1258 });
1259
1260 nearestblock.replace(replacement);
1261 nearestblock = replacement;
1262 }
1263
1264 // Set the attributes on the block level tag.
1265 if (attributes) {
1266 nearestblock.setAttrs(attributes);
1267 }
1268
1269 // Change the selection to the modified block. This makes sense when we might apply multiple styles
1270 // to the block.
1271 var selection = M.editor_atto.get_selection_from_node(nearestblock);
1272 M.editor_atto.set_selection(selection);
1273
1274 return nearestblock;
f6bef145
FM
1275 },
1276
1277 /**
1278 * Disable CSS styling.
1279 *
1280 * @return {Void}
1281 */
1282 disable_css_styling: function() {
1283 try {
1284 document.execCommand("styleWithCSS", 0, false);
1285 } catch (e1) {
1286 try {
1287 document.execCommand("useCSS", 0, true);
1288 } catch (e2) {
1289 try {
1290 document.execCommand('styleWithCSS', false, false);
1291 } catch (e3) {
1292 // We did our best.
1293 }
1294 }
1295 }
1296 },
1297
1298 /**
1299 * Enable CSS styling.
1300 *
1301 * @return {Void}
1302 */
1303 enable_css_styling: function() {
1304 try {
1305 document.execCommand("styleWithCSS", 0, true);
1306 } catch (e1) {
1307 try {
1308 document.execCommand("useCSS", 0, false);
1309 } catch (e2) {
1310 try {
1311 document.execCommand('styleWithCSS', false, true);
1312 } catch (e3) {
1313 // We did our best.
1314 }
1315 }
1316 }
2faf4c45
SH
1317 },
1318
1319 /**
1320 * Inserts the given HTML into the editable content at the currently focused point.
1321 * @param {String} html
1322 */
1323 insert_html_at_focus_point: function(html) {
1324 // We check document.selection here as the insertHTML exec command wan't available in IE until version 11.
1325 // In IE document.selection was removed at from version 11. Making it an easy affordable check.
1326 if (document.selection && document.selection.createRange) {
1327 var range = document.selection.createRange();
1328 if (range.pasteHTML) {
1329 range.pasteHTML(html);
1330 }
1331 } else {
1332 // All other browsers are great!
1333 document.execCommand('insertHTML', false, html);
1334 }
adca7326
DW
1335 }
1336
1337};
1338var CONTROLMENU_NAME = "Controlmenu",
1339 CONTROLMENU;
1340
1341/**
1342 * CONTROLMENU
1343 * This is a drop down list of buttons triggered (and aligned to) a button.
1344 *
1345 * @namespace M.editor_atto.controlmenu
1346 * @class controlmenu
1347 * @constructor
1348 * @extends M.core.dialogue
1349 */
1350CONTROLMENU = function(config) {
1351 config.draggable = false;
55c0403c 1352 config.center = false;
adca7326
DW
1353 config.width = 'auto';
1354 config.lightbox = false;
adca7326 1355 config.footerContent = '';
05843fd3
DW
1356 config.hideOn = [ { eventName: 'clickoutside' } ];
1357
adca7326
DW
1358 CONTROLMENU.superclass.constructor.apply(this, [config]);
1359};
1360
1361Y.extend(CONTROLMENU, M.core.dialogue, {
1362
1363 /**
1364 * Initialise the menu.
1365 *
1366 * @method initializer
1367 * @return void
1368 */
1369 initializer : function(config) {
1370 var body, headertext, bb;
1371 CONTROLMENU.superclass.initializer.call(this, config);
1372
1373 bb = this.get('boundingBox');
1374 bb.addClass('editor_atto_controlmenu');
1375
1376 // Close the menu when clicked outside (excluding the button that opened the menu).
1377 body = this.bodyNode;
1378
1379 headertext = Y.Node.create('<h3/>');
1380 headertext.addClass('accesshide');
1381 headertext.setHTML(this.get('headerText'));
1382 body.prepend(headertext);
adca7326
DW
1383 }
1384
1385}, {
1386 NAME : CONTROLMENU_NAME,
1387 ATTRS : {
1388 /**
1389 * The header for the drop down (only accessible to screen readers).
1390 *
1391 * @attribute headerText
1392 * @type String
1393 * @default ''
1394 */
1395 headerText : {
1396 value : ''
1397 }
1398
1399 }
1400});
1401
1402M.editor_atto = M.editor_atto || {};
1403M.editor_atto.controlmenu = CONTROLMENU;
d088a835
DW
1404/**
1405 * Class for cleaning ugly HTML.
1406 * Rewritten JS from jquery-clean plugin.
1407 *
1408 * @module editor_atto
1409 * @chainable
1410 */
1411function cleanHTML() {
1412 var cleaned = this.getHTML();
1413
1414 // What are we doing ?
1415 // We are cleaning random HTML from all over the shop into a set of useful html suitable for content.
1416 // We are allowing styles etc, but not e.g. font tags, class="MsoNormal" etc.
1417
1418 var rules = [
1419 // Source: "http://stackoverflow.com/questions/2875027/clean-microsoft-word-pasted-text-using-javascript"
1420 // Source: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work"
1421
1422 // Remove all HTML comments.
1423 {regex: /<!--[\s\S]*?-->/gi, replace: ""},
1424 // Source: "http://www.1stclassmedia.co.uk/developers/clean-ms-word-formatting.php"
1425 // Remove <?xml>, <\?xml>.
1426 {regex: /<\\?\?xml[^>]*>/gi, replace: ""},
1427 // Remove <o:blah>, <\o:blah>.
1428 {regex: /<\/?\w+:[^>]*>/gi, replace: ""}, // e.g. <o:p...
1429 // Remove MSO-blah, MSO:blah (e.g. in style attributes)
1430 {regex: /\s*MSO[-:][^;"']*;?/gi, replace: ""},
1431 // Remove empty spans
1432 {regex: /<span[^>]*>(&nbsp;|\s)*<\/span>/gi, replace: ""},
1433 // Remove class="Msoblah"
1434 {regex: /class="Mso[^"]*"/gi, replace: ""},
1435
1436 // Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"
1437 // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body.
1438 {regex: /<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|!\[)[^>]*?>/gi, replace: ""},
1439
1440 // Source: "http://www.tim-jarrett.com/labs_javascript_scrub_word.php"
1441 // Replace extended chars with simple text.
1442 {regex: new RegExp(String.fromCharCode(8220), 'gi'), replace: '"'},
1443 {regex: new RegExp(String.fromCharCode(8216), 'gi'), replace: "'"},
1444 {regex: new RegExp(String.fromCharCode(8217), 'gi'), replace: "'"},
1445 {regex: new RegExp(String.fromCharCode(8211), 'gi'), replace: '-'},
1446 {regex: new RegExp(String.fromCharCode(8212), 'gi'), replace: '--'},
1447 {regex: new RegExp(String.fromCharCode(189), 'gi'), replace: '1/2'},
1448 {regex: new RegExp(String.fromCharCode(188), 'gi'), replace: '1/4'},
1449 {regex: new RegExp(String.fromCharCode(190), 'gi'), replace: '3/4'},
1450 {regex: new RegExp(String.fromCharCode(169), 'gi'), replace: '(c)'},
1451 {regex: new RegExp(String.fromCharCode(174), 'gi'), replace: '(r)'},
1452 {regex: new RegExp(String.fromCharCode(8230), 'gi'), replace: '...'}
1453 ];
1454
1455 var i = 0, rule;
1456
1457 for (i = 0; i < rules.length; i++) {
1458 rule = rules[i];
1459 cleaned = cleaned.replace(rule.regex, rule.replace);
1460 }
1461
1462 this.setHTML(cleaned);
1463 return this;
1464}
1465
1466Y.Node.addMethod("cleanHTML", cleanHTML);
1467Y.NodeList.importMethod(Y.Node.prototype, "cleanHTML");
adca7326
DW
1468
1469
3ee53a42
DW
1470}, '@VERSION@', {
1471 "requires": [
1472 "node",
1473 "io",
1474 "overlay",
1475 "escape",
1476 "event",
1477 "event-simulate",
1478 "event-custom",
1479 "yui-throttle",
1480 "moodle-core-notification"
1481 ]
1482});