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