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