MDL-43954 editor_atto: Fix styling for IE8 in clean and standard
[moodle.git] / lib / editor / atto / yui / build / moodle-editor_atto-editor / moodle-editor_atto-editor.js
CommitLineData
adca7326
DW
1YUI.add('moodle-editor_atto-editor', function (Y, NAME) {
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
18/**
19 * Atto editor.
20 *
21 * @package editor_atto
22 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26/**
27 * Classes constants.
28 */
29CSS = {
30 CONTENT: 'editor_atto_content',
31 CONTENTWRAPPER: 'editor_atto_content_wrap',
32 TOOLBAR: 'editor_atto_toolbar',
33 WRAPPER: 'editor_atto'
34};
35
36/**
37 * Atto editor main class.
38 * Common functions required by editor plugins.
39 *
40 * @package editor_atto
41 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43 */
44M.editor_atto = M.editor_atto || {
45
34f5867a
DW
46 /**
47 * List of known block level tags.
48 * Taken from "https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements".
49 *
50 * @type {Array}
51 */
52 BLOCK_TAGS : [
53 'address',
54 'article',
55 'aside',
56 'audio',
57 'blockquote',
58 'canvas',
59 'dd',
60 'div',
61 'dl',
62 'fieldset',
63 'figcaption',
64 'figure',
65 'footer',
66 'form',
67 'h1',
68 'h2',
69 'h3',
70 'h4',
71 'h5',
72 'h6',
73 'header',
74 'hgroup',
75 'hr',
76 'noscript',
77 'ol',
78 'output',
79 'p',
80 'pre',
81 'section',
82 'table',
83 'tfoot',
84 'ul',
85 'video'],
86
adca7326
DW
87 /**
88 * List of attached button handlers to prevent duplicates.
89 */
90 buttonhandlers : {},
91
92 /**
93 * List of attached handlers to add inline editing controls to content.
94 */
95 textupdatedhandlers : {},
96
97 /**
98 * List of YUI overlays for custom menus.
99 */
100 menus : {},
101
102 /**
103 * List of attached menu handlers to prevent duplicates.
104 */
105 menuhandlers : {},
106
107 /**
108 * List of file picker options for specific editor instances.
109 */
110 filepickeroptions : {},
111
112 /**
113 * List of buttons and menus that have been added to the toolbar.
114 */
115 widgets : {},
116
26f8822d
DW
117 /**
118 * List of saved selections per editor instance.
119 */
120 selections : {},
121
122 focusfromclick : false,
123
adca7326
DW
124 /**
125 * Toggle a menu.
126 * @param event e
127 */
128 showhide_menu_handler : function(e) {
129 e.preventDefault();
130 var disabled = this.getAttribute('disabled');
131 var overlayid = this.getAttribute('data-menu');
132 var overlay = M.editor_atto.menus[overlayid];
133 var menu = overlay.get('bodyContent');
134 if (overlay.get('visible') || disabled) {
135 overlay.hide();
136 menu.detach('clickoutside');
137 } else {
138 menu.on('clickoutside', function(ev) {
139 if ((ev.target.ancestor() !== this) && (ev.target !== this)) {
140 if (overlay.get('visible')) {
141 menu.detach('clickoutside');
142 overlay.hide();
143 }
144 }
145 }, this);
0fa78b80
DW
146
147 overlay.align(Y.one(Y.config.doc.body), [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
adca7326 148 overlay.show();
0fa78b80
DW
149 var icon = e.target.ancestor('button', true).one('img');
150 overlay.align(icon, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
26f8822d 151 overlay.get('boundingBox').one('a').focus();
adca7326
DW
152 }
153 },
154
155 /**
156 * Handle clicks on editor buttons.
157 * @param event e
158 */
159 buttonclicked_handler : function(e) {
160 var elementid = this.getAttribute('data-editor');
161 var plugin = this.getAttribute('data-plugin');
162 var handler = this.getAttribute('data-handler');
163 var overlay = M.editor_atto.menus[plugin + '_' + elementid];
164
165 if (overlay) {
166 overlay.hide();
167 }
168
169 if (M.editor_atto.is_enabled(elementid, plugin)) {
170 // Pass it on.
171 handler = M.editor_atto.buttonhandlers[handler];
172 return handler(e, elementid);
173 }
174 },
175
adca7326
DW
176 /**
177 * Disable all buttons and menus in the toolbar.
178 * @param string elementid, the element id of this editor.
179 */
180 disable_all_widgets : function(elementid) {
48bdf86f 181 var plugin, element, toolbar = M.editor_atto.get_toolbar_node(elementid);
adca7326 182 for (plugin in M.editor_atto.widgets) {
48bdf86f 183 element = toolbar.one('.atto_' + plugin + '_button');
adca7326
DW
184
185 if (element) {
186 element.setAttribute('disabled', 'true');
187 }
188 }
189 },
190
48bdf86f
DW
191 /**
192 * Get the node of the original textarea element that this editor replaced.
193 *
194 * @param string elementid, the element id of this editor.
195 * @return Y.Node
196 */
197 get_textarea_node : function(elementid) {
198 // Note - it is not safe to use a CSS selector like '#' + elementid
199 // because the id may have colons in it - e.g. quiz.
200 return Y.one(document.getElementById(elementid));
201 },
202
203 /**
204 * Get the node of the toolbar container for this editor.
205 *
206 * @param string elementid, the element id of this editor.
207 * @return Y.Node
208 */
209 get_toolbar_node : function(elementid) {
210 // Note - it is not safe to use a CSS selector like '#' + elementid
211 // because the id may have colons in it - e.g. quiz.
212 return Y.one(document.getElementById(elementid + '_toolbar'));
213 },
214
215 /**
216 * Get the node of the contenteditable container for this editor.
217 *
218 * @param string elementid, the element id of this editor.
219 * @return Y.Node
220 */
221 get_editable_node : function(elementid) {
222 // Note - it is not safe to use a CSS selector like '#' + elementid
223 // because the id may have colons in it - e.g. quiz.
224 return Y.one(document.getElementById(elementid + 'editable'));
225 },
226
227 /**
228 * Determine if the specified toolbar button/menu is enabled.
229 * @param string elementid, the element id of this editor.
230 * @param string plugin, the plugin that created the button/menu.
231 */
232 is_enabled : function(elementid, plugin) {
233 var element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + plugin + '_button');
234
235 return !element.hasAttribute('disabled');
236 },
237
adca7326
DW
238 /**
239 * Enable a single widget in the toolbar.
240 * @param string elementid, the element id of this editor.
241 * @param string plugin, the name of the plugin that created the widget.
242 */
243 enable_widget : function(elementid, plugin) {
48bdf86f 244 var element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + plugin + '_button');
adca7326
DW
245
246 if (element) {
247 element.removeAttribute('disabled');
248 }
249 },
250
251 /**
252 * Enable all buttons and menus in the toolbar.
253 * @param string elementid, the element id of this editor.
254 */
255 enable_all_widgets : function(elementid) {
256 var plugin, element;
257 for (plugin in M.editor_atto.widgets) {
48bdf86f 258 element = M.editor_atto.get_toolbar_node(elementid).one('.atto_' + plugin + '_button');
adca7326
DW
259
260 if (element) {
261 element.removeAttribute('disabled');
262 }
263 }
264 },
265
266 /**
267 * Add a content update handler to be called whenever the content is updated.
268 * This is used to add inline editing controls to the content that are cleaned on submission.
269 *
270 * @param string elementid - the id of the textarea we created this editor from.
271 * @handler function callback - The function to do the cleaning.
272 * @param object context - the context to set for the callback.
273 * @handler function handler - A function to call when the button is clicked.
274 */
275 add_text_updated_handler : function(elementid, callback) {
276 if (!(elementid in M.editor_atto.textupdatedhandlers)) {
277 M.editor_atto.textupdatedhandlers[elementid] = [];
278 }
279 M.editor_atto.textupdatedhandlers[elementid].push(callback);
280 },
281
282 /**
283 * Add a button to the toolbar belonging to the editor for element with id "elementid".
284 * @param string elementid - the id of the textarea we created this editor from.
285 * @param string plugin - the plugin defining the button
286 * @param string icon - the html used for the content of the button
287 * @param string groupname - the group the button should be appended to.
288 * @param array entries - List of menu entries with the string (entry.text) and the handlers (entry.handler).
0fa78b80 289 * @param int overlaywidth - the overlay width size in 'ems'.
534cf7b7 290 * @param string menucolor - menu icon background color
adca7326 291 */
534cf7b7 292 add_toolbar_menu : function(elementid, plugin, iconurl, groupname, entries, overlaywidth, menucolor) {
48bdf86f
DW
293 var toolbar = M.editor_atto.get_toolbar_node(elementid),
294 group = toolbar.one('.atto_group.' + groupname + '_group'),
adca7326
DW
295 currentfocus,
296 button,
adca7326
DW
297 expimgurl;
298
534cf7b7
RW
299 if ((typeof overlaywidth) === 'undefined') {
300 overlaywidth = '14';
301 }
302 if ((typeof menucolor) === 'undefined') {
303 menucolor = 'transparent';
304 }
305
adca7326
DW
306 if (!group) {
307 group = Y.Node.create('<div class="atto_group ' + groupname + '_group"></div>');
308 toolbar.append(group);
309 }
adca7326
DW
310 expimgurl = M.util.image_url('t/expanded', 'moodle');
311 button = Y.Node.create('<button class="atto_' + plugin + '_button atto_hasmenu" ' +
312 'data-editor="' + Y.Escape.html(elementid) + '" ' +
313 'tabindex="-1" ' +
0012a946 314 'type="button" ' +
adca7326
DW
315 'data-menu="' + plugin + '_' + elementid + '" ' +
316 'title="' + Y.Escape.html(M.util.get_string('pluginname', 'atto_' + plugin)) + '">' +
534cf7b7
RW
317 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" '+
318 'style="background-color:' + menucolor + ';" src="' + iconurl + '"/>' +
adca7326
DW
319 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" src="' + expimgurl + '"/>' +
320 '</button>');
321
322 group.append(button);
323
324 currentfocus = toolbar.getAttribute('aria-activedescendant');
325 if (!currentfocus) {
326 button.setAttribute('tabindex', '0');
327 toolbar.setAttribute('aria-activedescendant', button.generateID());
328 }
329
330 // Save the name of the plugin.
331 M.editor_atto.widgets[plugin] = plugin;
332
333 var menu = Y.Node.create('<div class="atto_' + plugin + '_menu' +
534cf7b7
RW
334 ' atto_menu" data-editor="' + Y.Escape.html(elementid) + '"' +
335 ' style="min-width:' + (overlaywidth-2) + 'em"' +
336 '"></div>');
adca7326
DW
337 var i = 0, entry = {};
338
339 for (i = 0; i < entries.length; i++) {
340 entry = entries[i];
341
342 menu.append(Y.Node.create('<div class="atto_menuentry">' +
343 '<a href="#" class="atto_' + plugin + '_action_' + i + '" ' +
344 'data-editor="' + Y.Escape.html(elementid) + '" ' +
345 'data-plugin="' + Y.Escape.html(plugin) + '" ' +
346 'data-handler="' + Y.Escape.html(plugin + '_action_' + i) + '">' +
347 entry.text +
348 '</a>' +
349 '</div>'));
350 if (!M.editor_atto.buttonhandlers[plugin + '_action_' + i]) {
351 Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_action_' + i);
352 Y.one('body').delegate('key', M.editor_atto.buttonclicked_handler, 'space,enter', '.atto_' + plugin + '_action_' + i);
353 M.editor_atto.buttonhandlers[plugin + '_action_' + i] = entry.handler;
354 }
355 }
356
357 if (!M.editor_atto.buttonhandlers[plugin]) {
358 Y.one('body').delegate('click', M.editor_atto.showhide_menu_handler, '.atto_' + plugin + '_button');
359 M.editor_atto.buttonhandlers[plugin] = true;
360 }
361
362 var overlay = new M.core.dialogue({
363 bodyContent : menu,
364 visible : false,
534cf7b7 365 width: overlaywidth + 'em',
adca7326
DW
366 lightbox: false,
367 closeButton: false,
55c0403c 368 center : false
adca7326
DW
369 });
370
371 M.editor_atto.menus[plugin + '_' + elementid] = overlay;
55c0403c 372 overlay.align(button, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
adca7326
DW
373 overlay.hide();
374 overlay.headerNode.hide();
0fa78b80 375 overlay.render();
adca7326
DW
376 },
377
378 /**
379 * Add a button to the toolbar belonging to the editor for element with id "elementid".
380 * @param string elementid - the id of the textarea we created this editor from.
381 * @param string plugin - the plugin defining the button.
55c0403c 382 * @param string icon - the url to the image for the icon
adca7326
DW
383 * @param string groupname - the group the button should be appended to.
384 * @handler function handler- A function to call when the button is clicked.
385 */
55c0403c 386 add_toolbar_button : function(elementid, plugin, iconurl, groupname, handler) {
48bdf86f
DW
387 var toolbar = M.editor_atto.get_toolbar_node(elementid),
388 group = toolbar.one('.atto_group.' + groupname + '_group'),
adca7326 389 button,
55c0403c 390 currentfocus;
adca7326
DW
391
392 if (!group) {
393 group = Y.Node.create('<div class="atto_group ' + groupname +'_group"></div>');
394 toolbar.append(group);
395 }
adca7326
DW
396 button = Y.Node.create('<button class="atto_' + plugin + '_button" ' +
397 'data-editor="' + Y.Escape.html(elementid) + '" ' +
398 'data-plugin="' + Y.Escape.html(plugin) + '" ' +
399 'tabindex="-1" ' +
400 'data-handler="' + Y.Escape.html(plugin) + '" ' +
401 'title="' + Y.Escape.html(M.util.get_string('pluginname', 'atto_' + plugin)) + '">' +
55c0403c 402 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" src="' + iconurl + '"/>' +
adca7326
DW
403 '</button>');
404
405 group.append(button);
406
407 currentfocus = toolbar.getAttribute('aria-activedescendant');
408 if (!currentfocus) {
409 button.setAttribute('tabindex', '0');
410 toolbar.setAttribute('aria-activedescendant', button.generateID());
411 }
412
413 // We only need to attach this once.
414 if (!M.editor_atto.buttonhandlers[plugin]) {
415 Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_button');
416 M.editor_atto.buttonhandlers[plugin] = handler;
417 }
418
419 // Save the name of the plugin.
420 M.editor_atto.widgets[plugin] = plugin;
421
422 },
423
424 /**
425 * Work out if the cursor is in the editable area for this editor instance.
426 * @param string elementid of this editor
427 * @return bool
428 */
429 is_active : function(elementid) {
430 var selection = M.editor_atto.get_selection();
431
432 if (selection.length) {
433 selection = selection.pop();
434 }
435
436 var node = null;
437 if (selection.parentElement) {
438 node = Y.one(selection.parentElement());
439 } else {
440 node = Y.one(selection.startContainer);
441 }
442
34f5867a
DW
443 var editable = M.editor_atto.get_editable_node(elementid);
444
445 return node && editable.contains(node);
adca7326
DW
446 },
447
448 /**
449 * Focus on the editable area for this editor.
450 * @param string elementid of this editor
451 */
452 focus : function(elementid) {
48bdf86f 453 M.editor_atto.get_editable_node(elementid).focus();
adca7326
DW
454 },
455
456 /**
457 * Initialise the editor
458 * @param object params for this editor instance.
459 */
460 init : function(params) {
adca7326
DW
461 var wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />');
462 var atto = Y.Node.create('<div id="' + params.elementid + 'editable" ' +
463 'contenteditable="true" ' +
26f8822d 464 'role="textbox" ' +
adca7326 465 'spellcheck="true" ' +
26f8822d 466 'aria-live="off" ' +
adca7326
DW
467 'class="' + CSS.CONTENT + '" />');
468
26f8822d 469 var toolbar = Y.Node.create('<div class="' + CSS.TOOLBAR + '" id="' + params.elementid + '_toolbar" role="toolbar" aria-live="off"/>');
adca7326
DW
470
471 // Editable content wrapper.
472 var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />');
48bdf86f 473 var textarea = M.editor_atto.get_textarea_node(params.elementid);
26f8822d
DW
474 var label = Y.one('[for="' + params.elementid + '"]');
475
476 // Add a labelled-by attribute to the contenteditable.
477 if (label) {
478 label.generateID();
479 atto.setAttribute('aria-labelledby', label.get("id"));
480 toolbar.setAttribute('aria-labelledby', label.get("id"));
481 }
b269f635 482
adca7326
DW
483 content.appendChild(atto);
484
485 // Add everything to the wrapper.
486 wrapper.appendChild(toolbar);
487 wrapper.appendChild(content);
488
4c37c1f4 489 // Style the editor.
adca7326
DW
490 atto.setStyle('minHeight', (1.2 * (textarea.getAttribute('rows'))) + 'em');
491
492 // Copy text to editable div.
493 atto.append(textarea.get('value'));
494
d088a835
DW
495 // Clean it.
496 atto.cleanHTML();
497
adca7326
DW
498 // Add the toolbar and editable zone to the page.
499 textarea.get('parentNode').insert(wrapper, textarea);
26f8822d
DW
500
501 // Disable odd inline CSS styles.
502 try {
503 document.execCommand("styleWithCSS", 0, false);
504 } catch (e1) {
505 try {
506 document.execCommand("useCSS", 0, true);
507 } catch (e2) {
508 try {
509 document.execCommand('styleWithCSS', false, false);
510 }
511 catch (e3) {
512 // We did our best.
513 }
514 }
515 }
4c37c1f4 516
adca7326
DW
517 // Hide the old textarea.
518 textarea.hide();
26f8822d
DW
519 atto.on('keydown', this.save_selection, this, params.elementid);
520 atto.on('mouseup', this.save_selection, this, params.elementid);
521 atto.on('focus', this.restore_selection, this, params.elementid);
522 // Do not restore selection when focus is from a click event.
523 atto.on('mousedown', function() { this.focusfromclick = true; }, this);
adca7326 524
26f8822d 525 // Copy the current value back to the textarea when focus leaves us and save the current selection.
adca7326 526 atto.on('blur', function() {
26f8822d 527 this.focusfromclick = false;
48bdf86f 528 this.text_updated(params.elementid);
adca7326
DW
529 }, this);
530
531 // Listen for Arrow left and Arrow right keys.
532 Y.one(Y.config.doc.body).delegate('key',
533 this.keyboard_navigation,
534 'down:37,39',
48bdf86f 535 '#' + params.elementid + '_toolbar',
adca7326 536 this,
48bdf86f 537 params.elementid);
adca7326
DW
538
539 // Save the file picker options for later.
48bdf86f 540 M.editor_atto.filepickeroptions[params.elementid] = params.filepickeroptions;
55c0403c
DW
541
542 // Init each of the plugins
543 var i, j;
544 for (i = 0; i < params.plugins.length; i++) {
545 var group = params.plugins[i].group;
546 for (j = 0; j < params.plugins[i].plugins.length; j++) {
547 var plugin = params.plugins[i].plugins[j];
48bdf86f 548 plugin.params.elementid = params.elementid;
55c0403c
DW
549 plugin.params.group = group;
550
551 M['atto_' + plugin.name].init(plugin.params);
552 }
553 }
adca7326
DW
554 },
555
556 /**
557 * The text in the contenteditable region has been updated,
558 * clean and copy the buffer to the text area.
559 * @param string elementid - the id of the textarea we created this editor from.
560 */
561 text_updated : function(elementid) {
48bdf86f 562 var textarea = M.editor_atto.get_textarea_node(elementid),
adca7326
DW
563 cleancontent = this.get_clean_html(elementid);
564 textarea.set('value', cleancontent);
dad09216
FM
565 // Trigger the onchange callback on the textarea, essentially to notify moodle-core-formchangechecker.
566 textarea.simulate('change');
adca7326
DW
567 // Trigger handlers for this action.
568 var i = 0;
569 if (elementid in M.editor_atto.textupdatedhandlers) {
570 for (i = 0; i < M.editor_atto.textupdatedhandlers[elementid].length; i++) {
571 var callback = M.editor_atto.textupdatedhandlers[elementid][i];
572 callback(elementid);
573 }
574 }
575 },
576
577 /**
578 * Remove all YUI ids from the generated HTML.
579 * @param string elementid - the id of the textarea we created this editor from.
580 * @return string HTML stripped of YUI ids
581 */
582 get_clean_html : function(elementid) {
48bdf86f 583 var atto = M.editor_atto.get_editable_node(elementid).cloneNode(true);
adca7326
DW
584
585 Y.each(atto.all('[id]'), function(node) {
586 var id = node.get('id');
587 if (id.indexOf('yui') === 0) {
588 node.removeAttribute('id');
589 }
590 });
591
592 Y.each(atto.all('.atto_control'), function(node) {
593 node.remove(true);
594 });
595
d088a835
DW
596 // Remove any and all nasties from source.
597 atto.cleanHTML();
598
adca7326
DW
599 return atto.getHTML();
600 },
601
602 /**
603 * Implement arrow key navigation for the buttons in the toolbar.
604 * @param Event e - the keyboard event.
605 * @param string elementid - the id of the textarea we created this editor from.
606 */
607 keyboard_navigation : function(e, elementid) {
608 var buttons,
609 current,
610 currentid,
48bdf86f
DW
611 currentindex,
612 toolbar = M.editor_atto.get_toolbar_node(elementid);
adca7326
DW
613
614 e.preventDefault();
615
48bdf86f
DW
616 buttons = toolbar.all('button');
617 currentid = toolbar.getAttribute('aria-activedescendant');
adca7326
DW
618 if (!currentid) {
619 return;
620 }
621 current = Y.one('#' + currentid);
622 current.setAttribute('tabindex', '-1');
623
624 currentindex = buttons.indexOf(current);
625
626 if (e.keyCode === 37) {
627 // Left
628 currentindex--;
629 if (currentindex < 0) {
630 currentindex = buttons.size()-1;
631 }
632 } else {
633 // Right
634 currentindex++;
635 if (currentindex >= buttons.size()) {
636 currentindex = 0;
637 }
638 }
639
640 current = buttons.item(currentindex);
641 current.setAttribute('tabindex', '0');
642 current.focus();
48bdf86f 643 toolbar.setAttribute('aria-activedescendant', current.generateID());
adca7326
DW
644 },
645
b269f635
DW
646 /**
647 * Should we show the filepicker for this filetype?
648 *
649 * @param string elementid for this editor instance.
650 * @param string type The media type for the file picker
651 * @return boolean
652 */
653 can_show_filepicker : function(elementid, type) {
654 var options = M.editor_atto.filepickeroptions[elementid];
655 return ((typeof options[type]) !== "undefined");
656 },
657
adca7326
DW
658 /**
659 * Show the filepicker.
660 * @param string elementid for this editor instance.
661 * @param string type The media type for the file picker
662 * @param function callback
663 */
664 show_filepicker : function(elementid, type, callback) {
665 Y.use('core_filepicker', function (Y) {
666 var options = M.editor_atto.filepickeroptions[elementid][type];
667
668 options.formcallback = callback;
adca7326
DW
669
670 M.core_filepicker.show(Y, options);
671 });
672 },
673
674 /**
675 * Create a cross browser selection object that represents a yui node.
676 * @param Node yui node for the selection
677 * @return range (browser dependent)
678 */
679 get_selection_from_node: function(node) {
680 var range;
681
682 if (window.getSelection) {
683 range = document.createRange();
684
685 range.setStartBefore(node.getDOMNode());
686 range.setEndAfter(node.getDOMNode());
687 return [range];
688 } else if (document.selection) {
689 range = document.body.createTextRange();
690 range.moveToElementText(node.getDOMNode());
691 return range;
692 }
693 return false;
694 },
695
26f8822d
DW
696 /**
697 * Save the current selection on blur, allows more reliable keyboard navigation.
698 * @param Y.Event event
699 * @param string elementid
700 */
701 save_selection : function(event, elementid) {
702 if (this.is_active(elementid)) {
703 var sel = this.get_selection();
704 if (sel.length > 0) {
705 this.selections[elementid] = sel;
706 }
707 }
708 },
709
710 /**
711 * Restore any current selection when the editor gets focus again.
712 * @param Y.Event event
713 * @param string elementid
714 */
715 restore_selection : function(event, elementid) {
716 event.preventDefault();
717 if (!this.focusfromclick) {
718 if (typeof this.selections[elementid] !== "undefined") {
719 this.set_selection(this.selections[elementid]);
720 }
721 }
722 this.focusfromclick = false;
723 },
724
adca7326
DW
725 /**
726 * Get the selection object that can be passed back to set_selection.
727 * @return range (browser dependent)
728 */
729 get_selection : function() {
730 if (window.getSelection) {
731 var sel = window.getSelection();
732 var ranges = [], i = 0;
733 for (i = 0; i < sel.rangeCount; i++) {
734 ranges.push(sel.getRangeAt(i));
735 }
736 return ranges;
737 } else if (document.selection) {
738 // IE < 9
739 if (document.selection.createRange) {
740 return document.selection.createRange();
741 }
742 }
743 return false;
744 },
745
746 /**
747 * Check that a YUI node it at least partly contained by the selection.
adca7326
DW
748 * @param Y.Node node
749 * @return boolean
750 */
751 selection_contains_node : function(node) {
752 var range, sel;
753 if (window.getSelection) {
754 sel = window.getSelection();
755
756 if (sel.containsNode) {
757 return sel.containsNode(node.getDOMNode(), true);
758 }
759 }
760 sel = document.selection.createRange();
761 range = sel.duplicate();
762 range.moveToElementText(node.getDOMNode());
763 return sel.inRange(range);
764 },
765
766 /**
767 * Get the dom node representing the common anscestor of the selection nodes.
34f5867a 768 * @return DOMNode or false
adca7326
DW
769 */
770 get_selection_parent_node : function() {
771 var selection = M.editor_atto.get_selection();
34f5867a
DW
772 if (selection.length) {
773 selection = selection.pop();
774 }
775
776 if (selection.commonAncestorContainer) {
777 return selection.commonAncestorContainer;
778 } else if (selection.parentElement) {
779 return selection.parentElement();
adca7326 780 }
34f5867a
DW
781 // No selection
782 return false;
adca7326
DW
783 },
784
785 /**
786 * Get the list of child nodes of the selection.
787 * @return DOMNode[]
788 */
789 get_selection_text : function() {
790 var selection = M.editor_atto.get_selection();
791 if (selection.length > 0 && selection[0].cloneContents) {
792 return selection[0].cloneContents();
793 }
794 },
795
796 /**
797 * Set the current selection. Used to restore a selection.
798 */
799 set_selection : function(selection) {
800 var sel, i;
801
802 if (window.getSelection) {
803 sel = window.getSelection();
804 sel.removeAllRanges();
805 for (i = 0; i < selection.length; i++) {
806 sel.addRange(selection[i]);
807 }
808 } else if (document.selection) {
809 // IE < 9
810 if (selection.select) {
811 selection.select();
812 }
813 }
34f5867a
DW
814 },
815
816 /**
817 * Change the formatting for the current selection.
818 * Also changes the selection to the newly formatted block (allows applying multiple styles to a block).
819 *
820 * @param {String} elementid - The editor elementid.
821 * @param {String} blocktag - Change the block level tag to this. Empty string, means do not change the tag.
822 * @param {Object} attributes - The keys and values for attributes to be added/changed in the block tag.
823 * @return Y.Node - if there was a selection.
824 */
825 format_selection_block : function(elementid, blocktag, attributes) {
826 // First find the nearest ancestor of the selection that is a block level element.
827 var selectionparentnode = M.editor_atto.get_selection_parent_node(),
828 boundary,
829 cell,
830 nearestblock,
831 newcontent,
832 match,
833 replacement;
834
835 if (!selectionparentnode) {
836 // No selection, nothing to format.
837 return;
838 }
839
840 boundary = M.editor_atto.get_editable_node(elementid);
841
842 selectionparentnode = Y.one(selectionparentnode);
843
844 // If there is a table cell in between the selectionparentnode and the boundary,
845 // move the boundary to the table cell.
846 // This is because we might have a table in a div, and we select some text in a cell,
847 // want to limit the change in style to the table cell, not the entire table (via the outer div).
848 cell = selectionparentnode.ancestor(function (node) {
849 var tagname = node.get('tagName');
850 if (tagname) {
851 tagname = tagname.toLowerCase();
852 }
853 return (node === boundary) ||
854 (tagname === 'td') ||
855 (tagname === 'th');
856 }, true);
857
858 if (cell) {
859 // Limit the scope to the table cell.
860 boundary = cell;
861 }
862
863 nearestblock = selectionparentnode.ancestor(M.editor_atto.BLOCK_TAGS.join(', '), true);
864 if (nearestblock) {
865 // Check that the block is contained by the boundary.
866 match = nearestblock.ancestor(function (node) {
867 return node === boundary;
868 }, false);
869
870 if (!match) {
871 nearestblock = false;
872 }
873 }
874
875 // No valid block element - make one.
876 if (!nearestblock) {
877 // There is no block node in the content, wrap the content in a p and use that.
878 newcontent = Y.Node.create('<p></p>');
879 boundary.get('childNodes').each(function (child) {
880 newcontent.append(child.remove());
881 });
882 boundary.append(newcontent);
883 nearestblock = newcontent;
884 }
885
886 // Guaranteed to have a valid block level element contained in the contenteditable region.
887 // Change the tag to the new block level tag.
888 if (blocktag && blocktag !== '') {
889 // Change the block level node for a new one.
890 replacement = Y.Node.create('<' + blocktag + '></' + blocktag + '>');
891 // Copy all attributes.
892 replacement.setAttrs(nearestblock.getAttrs());
893 // Copy all children.
894 nearestblock.get('childNodes').each(function (child) {
895 child.remove();
896 replacement.append(child);
897 });
898
899 nearestblock.replace(replacement);
900 nearestblock = replacement;
901 }
902
903 // Set the attributes on the block level tag.
904 if (attributes) {
905 nearestblock.setAttrs(attributes);
906 }
907
908 // Change the selection to the modified block. This makes sense when we might apply multiple styles
909 // to the block.
910 var selection = M.editor_atto.get_selection_from_node(nearestblock);
911 M.editor_atto.set_selection(selection);
912
913 return nearestblock;
adca7326
DW
914 }
915
916};
917var CONTROLMENU_NAME = "Controlmenu",
918 CONTROLMENU;
919
920/**
921 * CONTROLMENU
922 * This is a drop down list of buttons triggered (and aligned to) a button.
923 *
924 * @namespace M.editor_atto.controlmenu
925 * @class controlmenu
926 * @constructor
927 * @extends M.core.dialogue
928 */
929CONTROLMENU = function(config) {
930 config.draggable = false;
55c0403c 931 config.center = false;
adca7326
DW
932 config.width = 'auto';
933 config.lightbox = false;
adca7326
DW
934 config.footerContent = '';
935 CONTROLMENU.superclass.constructor.apply(this, [config]);
936};
937
938Y.extend(CONTROLMENU, M.core.dialogue, {
939
940 /**
941 * Initialise the menu.
942 *
943 * @method initializer
944 * @return void
945 */
946 initializer : function(config) {
947 var body, headertext, bb;
948 CONTROLMENU.superclass.initializer.call(this, config);
949
950 bb = this.get('boundingBox');
951 bb.addClass('editor_atto_controlmenu');
952
953 // Close the menu when clicked outside (excluding the button that opened the menu).
954 body = this.bodyNode;
955
956 headertext = Y.Node.create('<h3/>');
957 headertext.addClass('accesshide');
958 headertext.setHTML(this.get('headerText'));
959 body.prepend(headertext);
960
961 body.on('clickoutside', function(e) {
962 if (this.get('visible')) {
963 // Note: we need to compare ids because for some reason - sometimes button is an Object, not a Y.Node.
964 if (!e.target.ancestor('.atto_control')) {
965 e.preventDefault();
966 this.hide();
967 }
968 }
969 }, this);
970 }
971
972}, {
973 NAME : CONTROLMENU_NAME,
974 ATTRS : {
975 /**
976 * The header for the drop down (only accessible to screen readers).
977 *
978 * @attribute headerText
979 * @type String
980 * @default ''
981 */
982 headerText : {
983 value : ''
984 }
985
986 }
987});
988
989M.editor_atto = M.editor_atto || {};
990M.editor_atto.controlmenu = CONTROLMENU;
d088a835
DW
991/**
992 * Class for cleaning ugly HTML.
993 * Rewritten JS from jquery-clean plugin.
994 *
995 * @module editor_atto
996 * @chainable
997 */
998function cleanHTML() {
999 var cleaned = this.getHTML();
1000
1001 // What are we doing ?
1002 // We are cleaning random HTML from all over the shop into a set of useful html suitable for content.
1003 // We are allowing styles etc, but not e.g. font tags, class="MsoNormal" etc.
1004
1005 var rules = [
1006 // Source: "http://stackoverflow.com/questions/2875027/clean-microsoft-word-pasted-text-using-javascript"
1007 // Source: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work"
1008
1009 // Remove all HTML comments.
1010 {regex: /<!--[\s\S]*?-->/gi, replace: ""},
1011 // Source: "http://www.1stclassmedia.co.uk/developers/clean-ms-word-formatting.php"
1012 // Remove <?xml>, <\?xml>.
1013 {regex: /<\\?\?xml[^>]*>/gi, replace: ""},
1014 // Remove <o:blah>, <\o:blah>.
1015 {regex: /<\/?\w+:[^>]*>/gi, replace: ""}, // e.g. <o:p...
1016 // Remove MSO-blah, MSO:blah (e.g. in style attributes)
1017 {regex: /\s*MSO[-:][^;"']*;?/gi, replace: ""},
1018 // Remove empty spans
1019 {regex: /<span[^>]*>(&nbsp;|\s)*<\/span>/gi, replace: ""},
1020 // Remove class="Msoblah"
1021 {regex: /class="Mso[^"]*"/gi, replace: ""},
1022
1023 // Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"
1024 // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body.
1025 {regex: /<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|!\[)[^>]*?>/gi, replace: ""},
1026
1027 // Source: "http://www.tim-jarrett.com/labs_javascript_scrub_word.php"
1028 // Replace extended chars with simple text.
1029 {regex: new RegExp(String.fromCharCode(8220), 'gi'), replace: '"'},
1030 {regex: new RegExp(String.fromCharCode(8216), 'gi'), replace: "'"},
1031 {regex: new RegExp(String.fromCharCode(8217), 'gi'), replace: "'"},
1032 {regex: new RegExp(String.fromCharCode(8211), 'gi'), replace: '-'},
1033 {regex: new RegExp(String.fromCharCode(8212), 'gi'), replace: '--'},
1034 {regex: new RegExp(String.fromCharCode(189), 'gi'), replace: '1/2'},
1035 {regex: new RegExp(String.fromCharCode(188), 'gi'), replace: '1/4'},
1036 {regex: new RegExp(String.fromCharCode(190), 'gi'), replace: '3/4'},
1037 {regex: new RegExp(String.fromCharCode(169), 'gi'), replace: '(c)'},
1038 {regex: new RegExp(String.fromCharCode(174), 'gi'), replace: '(r)'},
1039 {regex: new RegExp(String.fromCharCode(8230), 'gi'), replace: '...'}
1040 ];
1041
1042 var i = 0, rule;
1043
1044 for (i = 0; i < rules.length; i++) {
1045 rule = rules[i];
1046 cleaned = cleaned.replace(rule.regex, rule.replace);
1047 }
1048
1049 this.setHTML(cleaned);
1050 return this;
1051}
1052
1053Y.Node.addMethod("cleanHTML", cleanHTML);
1054Y.NodeList.importMethod(Y.Node.prototype, "cleanHTML");
adca7326
DW
1055
1056
dad09216 1057}, '@VERSION@', {"requires": ["node", "io", "overlay", "escape", "event", "event-simulate", "moodle-core-notification"]});