MDL-64573 output: ajax form event
[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/>.
ad3f8cd1 17/* eslint-disable no-unused-vars */
adca7326
DW
18
19/**
62467795 20 * The Atto WYSIWG pluggable editor, written for Moodle.
adca7326 21 *
62467795 22 * @module moodle-editor_atto-editor
adca7326
DW
23 * @package editor_atto
24 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
62467795 26 * @main moodle-editor_atto-editor
adca7326
DW
27 */
28
29/**
62467795
AN
30 * @module moodle-editor_atto-editor
31 * @submodule editor-base
adca7326 32 */
62467795
AN
33
34var LOGNAME = 'moodle-editor_atto-editor';
35var CSS = {
36 CONTENT: 'editor_atto_content',
37 CONTENTWRAPPER: 'editor_atto_content_wrap',
38 TOOLBAR: 'editor_atto_toolbar',
39 WRAPPER: 'editor_atto',
40 HIGHLIGHT: 'highlight'
557f44d9
AN
41 },
42 rangy = window.rangy;
adca7326
DW
43
44/**
62467795 45 * The Atto editor for Moodle.
adca7326 46 *
62467795
AN
47 * @namespace M.editor_atto
48 * @class Editor
49 * @constructor
50 * @uses M.editor_atto.EditorClean
51 * @uses M.editor_atto.EditorFilepicker
52 * @uses M.editor_atto.EditorSelection
53 * @uses M.editor_atto.EditorStyling
54 * @uses M.editor_atto.EditorTextArea
55 * @uses M.editor_atto.EditorToolbar
56 * @uses M.editor_atto.EditorToolbarNav
adca7326 57 */
62467795
AN
58
59function Editor() {
60 Editor.superclass.constructor.apply(this, arguments);
61}
62
63Y.extend(Editor, Y.Base, {
adca7326 64
34f5867a
DW
65 /**
66 * List of known block level tags.
67 * Taken from "https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements".
68 *
62467795 69 * @property BLOCK_TAGS
34f5867a
DW
70 * @type {Array}
71 */
3a0bc0fd 72 BLOCK_TAGS: [
34f5867a
DW
73 'address',
74 'article',
75 'aside',
76 'audio',
77 'blockquote',
78 'canvas',
79 'dd',
80 'div',
81 'dl',
82 'fieldset',
83 'figcaption',
84 'figure',
85 'footer',
86 'form',
87 'h1',
88 'h2',
89 'h3',
90 'h4',
91 'h5',
92 'h6',
93 'header',
94 'hgroup',
95 'hr',
96 'noscript',
97 'ol',
98 'output',
99 'p',
100 'pre',
101 'section',
102 'table',
103 'tfoot',
104 'ul',
62467795
AN
105 'video'
106 ],
d321f68b 107
af6a2e94 108 PLACEHOLDER_CLASS: 'atto-tmp-class',
bed1abbc
AD
109 ALL_NODES_SELECTOR: '[style],font[face]',
110 FONT_FAMILY: 'fontFamily',
34f5867a 111
adca7326 112 /**
62467795
AN
113 * The wrapper containing the editor.
114 *
115 * @property _wrapper
116 * @type Node
117 * @private
adca7326 118 */
62467795 119 _wrapper: null,
adca7326
DW
120
121 /**
62467795
AN
122 * A reference to the content editable Node.
123 *
124 * @property editor
125 * @type Node
adca7326 126 */
62467795 127 editor: null,
adca7326
DW
128
129 /**
62467795
AN
130 * A reference to the original text area.
131 *
132 * @property textarea
133 * @type Node
adca7326 134 */
62467795 135 textarea: null,
adca7326
DW
136
137 /**
62467795
AN
138 * A reference to the label associated with the original text area.
139 *
140 * @property textareaLabel
141 * @type Node
adca7326 142 */
62467795 143 textareaLabel: null,
adca7326
DW
144
145 /**
62467795
AN
146 * A reference to the list of plugins.
147 *
148 * @property plugins
149 * @type object
adca7326 150 */
62467795 151 plugins: null,
adca7326 152
4dbc02f6
AN
153 /**
154 * Event Handles to clear on editor destruction.
155 *
156 * @property _eventHandles
157 * @private
158 */
159 _eventHandles: null,
160
62467795
AN
161 initializer: function() {
162 var template;
adca7326 163
62467795
AN
164 // Note - it is not safe to use a CSS selector like '#' + elementid because the id
165 // may have colons in it - e.g. quiz.
166 this.textarea = Y.one(document.getElementById(this.get('elementid')));
26f8822d 167
62467795
AN
168 if (!this.textarea) {
169 // No text area found.
170 return;
171 }
26f8822d 172
c1ae9079
DW
173 var extraclasses = this.textarea.getAttribute('class');
174
4dbc02f6
AN
175 this._eventHandles = [];
176
62467795
AN
177 this._wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />');
178 template = Y.Handlebars.compile('<div id="{{elementid}}editable" ' +
179 'contenteditable="true" ' +
180 'role="textbox" ' +
181 'spellcheck="true" ' +
182 'aria-live="off" ' +
c1ae9079 183 'class="{{CSS.CONTENT}} ' + extraclasses + '" ' +
62467795
AN
184 '/>');
185 this.editor = Y.Node.create(template({
186 elementid: this.get('elementid'),
187 CSS: CSS
188 }));
67d3fe45 189
6d9a83c2
DW
190 // Add a labelled-by attribute to the contenteditable.
191 this.textareaLabel = Y.one('[for="' + this.get('elementid') + '"]');
192 if (this.textareaLabel) {
193 this.textareaLabel.generateID();
194 this.editor.setAttribute('aria-labelledby', this.textareaLabel.get("id"));
195 }
67d3fe45 196
62467795
AN
197 // Add everything to the wrapper.
198 this.setupToolbar();
67d3fe45 199
62467795
AN
200 // Editable content wrapper.
201 var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />');
202 content.appendChild(this.editor);
203 this._wrapper.appendChild(content);
0fa78b80 204
2f0a1236
FM
205 // Style the editor. According to the styles.css: 20 is the line-height, 8 is padding-top + padding-bottom.
206 this.editor.setStyle('minHeight', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px');
207
208 if (Y.UA.ie === 0) {
209 // We set a height here to force the overflow because decent browsers allow the CSS property resize.
210 this.editor.setStyle('height', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px');
211 }
212
62467795
AN
213 // Disable odd inline CSS styles.
214 this.disableCssStyling();
adca7326 215
e92a39cf
DP
216 // Use paragraphs not divs.
217 if (document.queryCommandSupported('DefaultParagraphSeparator')) {
218 document.execCommand('DefaultParagraphSeparator', false, 'p');
219 }
220
62467795 221 // Add the toolbar and editable zone to the page.
369a63ac
DC
222 this.textarea.get('parentNode').insert(this._wrapper, this.textarea).
223 setAttribute('class', 'editor_atto_wrap');
adca7326 224
62467795
AN
225 // Hide the old textarea.
226 this.textarea.hide();
adca7326 227
b6976f1a
JH
228 // Set up custom event for editor updated.
229 Y.mix(Y.Node.DOM_EVENTS, {'form:editorUpdated': true});
230 this.textarea.on('form:editorUpdated', function() {
231 this.updateEditorState();
232 }, this);
233
62467795
AN
234 // Copy the text to the contenteditable div.
235 this.updateFromTextArea();
adca7326 236
62467795
AN
237 // Publish the events that are defined by this editor.
238 this.publishEvents();
adca7326 239
62467795
AN
240 // Add handling for saving and restoring selections on cursor/focus changes.
241 this.setupSelectionWatchers();
adca7326 242
4dbc02f6
AN
243 // Add polling to update the textarea periodically when typing long content.
244 this.setupAutomaticPolling();
245
62467795
AN
246 // Setup plugins.
247 this.setupPlugins();
2ba6706d
DW
248
249 // Initialize the auto-save timer.
250 this.setupAutosave();
8a5db547
DW
251 // Preload the icons for the notifications.
252 this.setupNotifications();
48bdf86f
DW
253 },
254
255 /**
62467795 256 * Focus on the editable area for this editor.
48bdf86f 257 *
62467795
AN
258 * @method focus
259 * @chainable
48bdf86f 260 */
62467795
AN
261 focus: function() {
262 this.editor.focus();
263
264 return this;
48bdf86f
DW
265 },
266
267 /**
62467795 268 * Publish events for this editor instance.
48bdf86f 269 *
62467795
AN
270 * @method publishEvents
271 * @private
272 * @chainable
48bdf86f 273 */
62467795
AN
274 publishEvents: function() {
275 /**
276 * Fired when changes are made within the editor.
277 *
278 * @event change
279 */
280 this.publish('change', {
281 broadcast: true,
282 preventable: true
283 });
48bdf86f 284
62467795
AN
285 /**
286 * Fired when all plugins have completed loading.
287 *
288 * @event pluginsloaded
289 */
290 this.publish('pluginsloaded', {
291 fireOnce: true
292 });
48bdf86f 293
62467795
AN
294 this.publish('atto:selectionchanged', {
295 prefix: 'atto'
296 });
48bdf86f 297
62467795 298 return this;
3ee53a42
DW
299 },
300
4dbc02f6
AN
301 /**
302 * Set up automated polling of the text area to update the textarea.
303 *
304 * @method setupAutomaticPolling
305 * @chainable
306 */
307 setupAutomaticPolling: function() {
a7fdadc9
EM
308 this._registerEventHandle(this.editor.on(['keyup', 'cut'], this.updateOriginal, this));
309 this._registerEventHandle(this.editor.on('paste', this.pasteCleanup, this));
4dbc02f6 310
10fae277
PN
311 // Call this.updateOriginal after dropped content has been processed.
312 this._registerEventHandle(this.editor.on('drop', this.updateOriginalDelayed, this));
313
314 return this;
315 },
316
317 /**
318 * Calls updateOriginal on a short timer to allow native event handlers to run first.
319 *
320 * @method updateOriginalDelayed
321 * @chainable
322 */
323 updateOriginalDelayed: function() {
324 Y.soon(Y.bind(this.updateOriginal, this));
325
4dbc02f6
AN
326 return this;
327 },
328
62467795
AN
329 setupPlugins: function() {
330 // Clear the list of plugins.
331 this.plugins = {};
332
333 var plugins = this.get('plugins');
adca7326 334
62467795
AN
335 var groupIndex,
336 group,
337 pluginIndex,
338 plugin,
339 pluginConfig;
340
341 for (groupIndex in plugins) {
342 group = plugins[groupIndex];
343 if (!group.plugins) {
344 // No plugins in this group - skip it.
345 continue;
346 }
347 for (pluginIndex in group.plugins) {
348 plugin = group.plugins[pluginIndex];
349
350 pluginConfig = Y.mix({
351 name: plugin.name,
352 group: group.group,
353 editor: this.editor,
354 toolbar: this.toolbar,
355 host: this
356 }, plugin);
357
358 // Add a reference to the current editor.
359 if (typeof Y.M['atto_' + plugin.name] === "undefined") {
360 continue;
361 }
362 this.plugins[plugin.name] = new Y.M['atto_' + plugin.name].Button(pluginConfig);
363 }
adca7326 364 }
62467795
AN
365
366 // Some plugins need to perform actions once all plugins have loaded.
367 this.fire('pluginsloaded');
368
369 return this;
adca7326
DW
370 },
371
62467795
AN
372 enablePlugins: function(plugin) {
373 this._setPluginState(true, plugin);
374 },
3ee53a42 375
62467795
AN
376 disablePlugins: function(plugin) {
377 this._setPluginState(false, plugin);
3ee53a42
DW
378 },
379
62467795
AN
380 _setPluginState: function(enable, plugin) {
381 var target = 'disableButtons';
382 if (enable) {
383 target = 'enableButtons';
3ee53a42 384 }
3ee53a42 385
62467795
AN
386 if (plugin) {
387 this.plugins[plugin][target]();
388 } else {
389 Y.Object.each(this.plugins, function(currentPlugin) {
390 currentPlugin[target]();
391 }, this);
3ee53a42 392 }
4dbc02f6
AN
393 },
394
b6976f1a
JH
395 /**
396 * Update the state of the editor.
397 */
398 updateEditorState: function() {
399 var disabled = this.textarea.hasAttribute('readonly'),
400 editorfield = Y.one('#' + this.get('elementid') + 'editable');
401 // Enable/Disable all plugins.
402 this._setPluginState(!disabled);
403 // Enable/Disable content of editor.
404 if (editorfield) {
405 editorfield.setAttribute('contenteditable', !disabled);
406 }
407 },
408
4dbc02f6
AN
409 /**
410 * Register an event handle for disposal in the destructor.
411 *
412 * @method _registerEventHandle
413 * @param {EventHandle} The Event Handle as returned by Y.on, and Y.delegate.
414 * @private
415 */
416 _registerEventHandle: function(handle) {
417 this._eventHandles.push(handle);
62467795 418 }
3ee53a42 419
62467795
AN
420}, {
421 NS: 'editor_atto',
422 ATTRS: {
423 /**
424 * The unique identifier for the form element representing the editor.
425 *
426 * @attribute elementid
427 * @type String
428 * @writeOnce
429 */
430 elementid: {
431 value: null,
432 writeOnce: true
433 },
adca7326 434
2ba6706d
DW
435 /**
436 * The contextid of the form.
437 *
438 * @attribute contextid
439 * @type Integer
440 * @writeOnce
441 */
442 contextid: {
443 value: null,
444 writeOnce: true
445 },
446
62467795
AN
447 /**
448 * Plugins with their configuration.
449 *
450 * The plugins structure is:
451 *
452 * [
453 * {
454 * "group": "groupName",
455 * "plugins": [
456 * "pluginName": {
457 * "configKey": "configValue"
458 * },
459 * "pluginName": {
460 * "configKey": "configValue"
461 * }
462 * ]
463 * },
464 * {
465 * "group": "groupName",
466 * "plugins": [
467 * "pluginName": {
468 * "configKey": "configValue"
469 * }
470 * ]
471 * }
472 * ]
473 *
474 * @attribute plugins
475 * @type Object
476 * @writeOnce
477 */
478 plugins: {
479 value: {},
480 writeOnce: true
adca7326 481 }
62467795
AN
482 }
483});
484
485// The Editor publishes custom events that can be subscribed to.
486Y.augment(Editor, Y.EventTarget);
487
488Y.namespace('M.editor_atto').Editor = Editor;
489
490// Function for Moodle's initialisation.
491Y.namespace('M.editor_atto.Editor').init = function(config) {
492 return new Y.M.editor_atto.Editor(config);
493};
494// This file is part of Moodle - http://moodle.org/
495//
496// Moodle is free software: you can redistribute it and/or modify
497// it under the terms of the GNU General Public License as published by
498// the Free Software Foundation, either version 3 of the License, or
499// (at your option) any later version.
500//
501// Moodle is distributed in the hope that it will be useful,
502// but WITHOUT ANY WARRANTY; without even the implied warranty of
503// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
504// GNU General Public License for more details.
75c86d13
DW
505//
506// You should have received a copy of the GNU General Public License
507// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
508
509/**
510 * A notify function for the Atto editor.
511 *
512 * @module moodle-editor_atto-notify
19549f8b 513 * @submodule notify
75c86d13
DW
514 * @package editor_atto
515 * @copyright 2014 Damyon Wiese
516 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
517 */
518
19549f8b
DW
519var LOGNAME_NOTIFY = 'moodle-editor_atto-editor-notify',
520 NOTIFY_INFO = 'info',
521 NOTIFY_WARNING = 'warning';
522
75c86d13
DW
523function EditorNotify() {}
524
3a0bc0fd 525EditorNotify.ATTRS = {
75c86d13
DW
526};
527
75c86d13
DW
528EditorNotify.prototype = {
529
530 /**
19549f8b 531 * A single Y.Node for this editor. There is only ever one - it is replaced if a new message comes in.
75c86d13
DW
532 *
533 * @property messageOverlay
19549f8b 534 * @type {Node}
75c86d13
DW
535 */
536 messageOverlay: null,
537
8a5db547
DW
538 /**
539 * A single timer object that can be used to cancel the hiding behaviour.
540 *
541 * @property hideTimer
542 * @type {timer}
543 */
544 hideTimer: null,
545
546 /**
547 * Initialize the notifications.
548 *
549 * @method setupNotifications
550 * @chainable
551 */
552 setupNotifications: function() {
553 var preload1 = new Image(),
554 preload2 = new Image();
555
556 preload1.src = M.util.image_url('i/warning', 'moodle');
557 preload2.src = M.util.image_url('i/info', 'moodle');
558
559 return this;
560 },
561
75c86d13
DW
562 /**
563 * Show a notification in a floaty overlay somewhere in the atto editor text area.
564 *
565 * @method showMessage
19549f8b
DW
566 * @param {String} message The translated message (use get_string)
567 * @param {String} type Must be either "info" or "warning"
568 * @param {Number} timeout Time in milliseconds to show this message for.
75c86d13
DW
569 * @chainable
570 */
8a5db547 571 showMessage: function(message, type, timeout) {
19549f8b
DW
572 var messageTypeIcon = '',
573 intTimeout,
574 bodyContent;
75c86d13
DW
575
576 if (this.messageOverlay === null) {
577 this.messageOverlay = Y.Node.create('<div class="editor_atto_notification"></div>');
578
f1018cd6 579 this.messageOverlay.hide(true);
3a7887ba 580 this.textarea.get('parentNode').append(this.messageOverlay);
75c86d13
DW
581
582 this.messageOverlay.on('click', function() {
f1018cd6 583 this.messageOverlay.hide(true);
75c86d13 584 }, this);
8a5db547 585 }
75c86d13 586
8a5db547
DW
587 if (this.hideTimer !== null) {
588 this.hideTimer.cancel();
75c86d13
DW
589 }
590
19549f8b 591 if (type === NOTIFY_WARNING) {
6bfd450a 592 messageTypeIcon = '<img src="' +
75c86d13
DW
593 M.util.image_url('i/warning', 'moodle') +
594 '" alt="' + M.util.get_string('warning', 'moodle') + '"/>';
19549f8b 595 } else if (type === NOTIFY_INFO) {
6bfd450a 596 messageTypeIcon = '<img src="' +
75c86d13
DW
597 M.util.image_url('i/info', 'moodle') +
598 '" alt="' + M.util.get_string('info', 'moodle') + '"/>';
8a5db547
DW
599 } else {
600 }
601
602 // Parse the timeout value.
19549f8b
DW
603 intTimeout = parseInt(timeout, 10);
604 if (intTimeout <= 0) {
605 intTimeout = 60000;
75c86d13
DW
606 }
607
8a5db547
DW
608 // Convert class to atto_info (for example).
609 type = 'atto_' + type;
610
19549f8b 611 bodyContent = Y.Node.create('<div class="' + type + '" role="alert" aria-live="assertive">' +
75c86d13
DW
612 messageTypeIcon + ' ' +
613 Y.Escape.html(message) +
614 '</div>');
615 this.messageOverlay.empty();
616 this.messageOverlay.append(bodyContent);
f1018cd6 617 this.messageOverlay.show(true);
75c86d13 618
19549f8b 619 this.hideTimer = Y.later(intTimeout, this, function() {
8a5db547 620 this.hideTimer = null;
9e12b0fa
JD
621 if (this.messageOverlay.inDoc()) {
622 this.messageOverlay.hide(true);
623 }
8a5db547
DW
624 });
625
75c86d13
DW
626 return this;
627 }
628
629};
630
631Y.Base.mix(Y.M.editor_atto.Editor, [EditorNotify]);
632// This file is part of Moodle - http://moodle.org/
633//
634// Moodle is free software: you can redistribute it and/or modify
635// it under the terms of the GNU General Public License as published by
636// the Free Software Foundation, either version 3 of the License, or
637// (at your option) any later version.
638//
639// Moodle is distributed in the hope that it will be useful,
640// but WITHOUT ANY WARRANTY; without even the implied warranty of
641// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
642// GNU General Public License for more details.
62467795
AN
643//
644// You should have received a copy of the GNU General Public License
645// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
646
647/**
648 * @module moodle-editor_atto-editor
649 * @submodule textarea
650 */
adca7326 651
62467795
AN
652/**
653 * Textarea functions for the Atto editor.
654 *
655 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
656 *
657 * @namespace M.editor_atto
658 * @class EditorTextArea
659 */
660
661function EditorTextArea() {}
662
3a0bc0fd 663EditorTextArea.ATTRS = {
62467795
AN
664};
665
666EditorTextArea.prototype = {
4dbc02f6
AN
667
668 /**
669 * Return the appropriate empty content value for the current browser.
670 *
671 * Different browsers use a different content when they are empty and
672 * we must set this reliable across the board.
673 *
674 * @method _getEmptyContent
675 * @return String The content to use representing no user-provided content
676 * @private
677 */
678 _getEmptyContent: function() {
679 if (Y.UA.ie && Y.UA.ie < 10) {
680 return '<p></p>';
681 } else {
682 return '<p><br></p>';
683 }
684 },
685
adca7326 686 /**
62467795 687 * Copy and clean the text from the textarea into the contenteditable div.
adca7326 688 *
62467795
AN
689 * If the text is empty, provide a default paragraph tag to hold the content.
690 *
691 * @method updateFromTextArea
692 * @chainable
adca7326 693 */
62467795
AN
694 updateFromTextArea: function() {
695 // Clear it first.
696 this.editor.setHTML('');
697
9029ce75
EM
698 // Copy cleaned HTML to editable div.
699 this.editor.append(this._cleanHTML(this.textarea.get('value')));
62467795
AN
700
701 // Insert a paragraph in the empty contenteditable div.
702 if (this.editor.getHTML() === '') {
4dbc02f6 703 this.editor.setHTML(this._getEmptyContent());
adca7326 704 }
9029ce75
EM
705
706 return this;
adca7326
DW
707 },
708
709 /**
62467795
AN
710 * Copy the text from the contenteditable to the textarea which it replaced.
711 *
712 * @method updateOriginal
713 * @chainable
adca7326 714 */
3a0bc0fd 715 updateOriginal: function() {
4dbc02f6
AN
716 // Get the previous and current value to compare them.
717 var oldValue = this.textarea.get('value'),
718 newValue = this.getCleanHTML();
719
720 if (newValue === "" && this.isActive()) {
721 // The content was entirely empty so get the empty content placeholder.
722 newValue = this._getEmptyContent();
723 }
5ec54dd1 724
4dbc02f6
AN
725 // Only call this when there has been an actual change to reduce processing.
726 if (oldValue !== newValue) {
727 // Insert the cleaned content.
728 this.textarea.set('value', newValue);
5ec54dd1 729
4dbc02f6
AN
730 // Trigger the onchange callback on the textarea, essentially to notify moodle-core-formchangechecker.
731 this.textarea.simulate('change');
732
733 // Trigger handlers for this action.
734 this.fire('change');
735 }
736
737 return this;
62467795
AN
738 }
739};
534cf7b7 740
62467795
AN
741Y.Base.mix(Y.M.editor_atto.Editor, [EditorTextArea]);
742// This file is part of Moodle - http://moodle.org/
743//
744// Moodle is free software: you can redistribute it and/or modify
745// it under the terms of the GNU General Public License as published by
746// the Free Software Foundation, either version 3 of the License, or
747// (at your option) any later version.
748//
749// Moodle is distributed in the hope that it will be useful,
750// but WITHOUT ANY WARRANTY; without even the implied warranty of
751// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
752// GNU General Public License for more details.
753//
754// You should have received a copy of the GNU General Public License
755// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
ad3f8cd1
DP
756/* global NOTIFY_WARNING, NOTIFY_INFO */
757/* eslint-disable no-unused-vars */
adca7326 758
2ba6706d
DW
759/**
760 * A autosave function for the Atto editor.
761 *
762 * @module moodle-editor_atto-autosave
763 * @submodule autosave-base
764 * @package editor_atto
765 * @copyright 2014 Damyon Wiese
766 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
767 */
768
6bfd450a 769var SUCCESS_MESSAGE_TIMEOUT = 5000,
19549f8b
DW
770 RECOVER_MESSAGE_TIMEOUT = 60000,
771 LOGNAME_AUTOSAVE = 'moodle-editor_atto-editor-autosave';
2ba6706d
DW
772
773function EditorAutosave() {}
774
3a0bc0fd 775EditorAutosave.ATTRS = {
6bfd450a
DW
776 /**
777 * Enable/Disable auto save for this instance.
778 *
779 * @attribute autosaveEnabled
780 * @type Boolean
781 * @writeOnce
782 */
783 autosaveEnabled: {
784 value: true,
785 writeOnce: true
786 },
787
788 /**
789 * The time between autosaves (in seconds).
790 *
791 * @attribute autosaveFrequency
19549f8b 792 * @type Number
6bfd450a
DW
793 * @default 60
794 * @writeOnce
795 */
796 autosaveFrequency: {
797 value: 60,
798 writeOnce: true
56579fb6
DW
799 },
800
c07f86ce
DW
801 /**
802 * Unique hash for this page instance. Calculated from $PAGE->url in php.
803 *
804 * @attribute pageHash
805 * @type String
806 * @writeOnce
807 */
808 pageHash: {
809 value: '',
810 writeOnce: true
6bfd450a 811 }
2ba6706d
DW
812};
813
814EditorAutosave.prototype = {
815
816 /**
817 * The text that was auto saved in the last request.
818 *
819 * @property lastText
820 * @type string
821 */
e924e1b3 822 lastText: "",
2ba6706d
DW
823
824 /**
825 * Autosave instance.
826 *
827 * @property autosaveInstance
828 * @type string
829 */
830 autosaveInstance: null,
831
b803df81
DW
832 /**
833 * Autosave Timer.
834 *
835 * @property autosaveTimer
836 * @type object
837 */
838 autosaveTimer: null,
839
2ba6706d
DW
840 /**
841 * Initialize the autosave process
842 *
843 * @method setupAutosave
844 * @chainable
845 */
846 setupAutosave: function() {
6bfd450a 847 var draftid = -1,
c4e2c671 848 form,
6bfd450a 849 optiontype = null,
557f44d9 850 options = this.get('filepickeroptions'),
240a9de4 851 params;
2ba6706d
DW
852
853 if (!this.get('autosaveEnabled')) {
854 // Autosave disabled for this instance.
855 return;
856 }
857
2ba6706d 858 this.autosaveInstance = Y.stamp(this);
2ba6706d
DW
859 for (optiontype in options) {
860 if (typeof options[optiontype].itemid !== "undefined") {
861 draftid = options[optiontype].itemid;
862 }
863 }
864
865 // First see if there are any saved drafts.
866 // Make an ajax request.
2ba6706d 867 params = {
2ba6706d
DW
868 contextid: this.get('contextid'),
869 action: 'resume',
2ba6706d
DW
870 draftid: draftid,
871 elementid: this.get('elementid'),
872 pageinstance: this.autosaveInstance,
c07f86ce 873 pagehash: this.get('pageHash')
2ba6706d
DW
874 };
875
240a9de4
FM
876 this.autosaveIo(params, this, {
877 success: function(response) {
878 if (response === null) {
879 // This can happen when there is nothing to resume from.
880 return;
881 } else if (!response) {
882 return;
883 }
884
885 // Revert untouched editor contents to an empty string.
886 // Check for FF and Chrome.
887 if (response.result === '<p></p>' || response.result === '<p><br></p>' ||
888 response.result === '<br>') {
889 response.result = '';
890 }
891
892 // Check for IE 9 and 10.
893 if (response.result === '<p>&nbsp;</p>' || response.result === '<p><br>&nbsp;</p>') {
894 response.result = '';
895 }
896
897 if (response.error || typeof response.result === 'undefined') {
557f44d9
AN
898 this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
899 NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
240a9de4
FM
900 } else if (response.result !== this.textarea.get('value') &&
901 response.result !== '') {
902 this.recoverText(response.result);
2ba6706d 903 }
240a9de4
FM
904 this._fireSelectionChanged();
905
906 },
907 failure: function() {
908 this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
909 NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
19549f8b 910 }
2ba6706d
DW
911 });
912
913 // Now setup the timer for periodic saves.
6bfd450a 914 var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000;
b803df81 915 this.autosaveTimer = Y.later(delay, this, this.saveDraft, false, true);
2ba6706d
DW
916
917 // Now setup the listener for form submission.
c4e2c671
AN
918 form = this.textarea.ancestor('form');
919 if (form) {
240a9de4
FM
920 this.autosaveIoOnSubmit(form, {
921 action: 'reset',
922 contextid: this.get('contextid'),
923 elementid: this.get('elementid'),
924 pageinstance: this.autosaveInstance,
925 pagehash: this.get('pageHash')
926 });
c4e2c671 927 }
2ba6706d
DW
928 return this;
929 },
930
2ba6706d 931 /**
6bfd450a 932 * Recover a previous version of this text and show a message.
2ba6706d
DW
933 *
934 * @method recoverText
935 * @param {String} text
936 * @chainable
937 */
938 recoverText: function(text) {
6bfd450a
DW
939 this.editor.setHTML(text);
940 this.saveSelection();
941 this.updateOriginal();
942 this.lastText = text;
943
557f44d9
AN
944 this.showMessage(M.util.get_string('textrecovered', 'editor_atto'),
945 NOTIFY_INFO, RECOVER_MESSAGE_TIMEOUT);
2ba6706d 946
b5594973
AG
947 // Fire an event that the editor content has changed.
948 require(['core/event'], function(event) {
949 event.notifyEditorContentRestored();
950 });
951
2ba6706d
DW
952 return this;
953 },
954
955 /**
956 * Save a single draft via ajax.
957 *
958 * @method saveDraft
959 * @chainable
960 */
961 saveDraft: function() {
557f44d9 962 var url, params;
b803df81
DW
963
964 if (!this.editor.getDOMNode()) {
965 // Stop autosaving if the editor was removed from the page.
966 this.autosaveTimer.cancel();
967 return;
968 }
3a7887ba
DW
969 // Only copy the text from the div to the textarea if the textarea is not currently visible.
970 if (!this.editor.get('hidden')) {
971 this.updateOriginal();
972 }
2ba6706d
DW
973 var newText = this.textarea.get('value');
974
975 if (newText !== this.lastText) {
976
977 // Make an ajax request.
56579fb6 978 url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
2ba6706d
DW
979 params = {
980 sesskey: M.cfg.sesskey,
981 contextid: this.get('contextid'),
982 action: 'save',
983 drafttext: newText,
984 elementid: this.get('elementid'),
c07f86ce 985 pagehash: this.get('pageHash'),
2ba6706d
DW
986 pageinstance: this.autosaveInstance
987 };
19549f8b
DW
988
989 // Reusable error handler - must be passed the correct context.
240a9de4 990 var ajaxErrorFunction = function(response) {
19549f8b
DW
991 var errorDuration = parseInt(this.get('autosaveFrequency'), 10) * 1000;
992 this.showMessage(M.util.get_string('autosavefailed', 'editor_atto'), NOTIFY_WARNING, errorDuration);
993 };
2ba6706d 994
240a9de4
FM
995 this.autosaveIo(params, this, {
996 failure: ajaxErrorFunction,
997 success: function(response) {
998 if (response && response.error) {
999 Y.soon(Y.bind(ajaxErrorFunction, this, [response]));
1000 } else {
1001 // All working.
1002 this.lastText = newText;
1003 this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
1004 NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
2ba6706d 1005 }
19549f8b 1006 }
2ba6706d 1007 });
2ba6706d
DW
1008 }
1009 return this;
1010 }
1011};
1012
1013Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosave]);
1014// This file is part of Moodle - http://moodle.org/
1015//
1016// Moodle is free software: you can redistribute it and/or modify
1017// it under the terms of the GNU General Public License as published by
1018// the Free Software Foundation, either version 3 of the License, or
1019// (at your option) any later version.
1020//
1021// Moodle is distributed in the hope that it will be useful,
1022// but WITHOUT ANY WARRANTY; without even the implied warranty of
1023// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1024// GNU General Public License for more details.
1025//
1026// You should have received a copy of the GNU General Public License
1027// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1028
240a9de4
FM
1029/**
1030 * A autosave function for the Atto editor.
1031 *
1032 * @module moodle-editor_atto-autosave-io
1033 * @submodule autosave-io
1034 * @package editor_atto
1035 * @copyright 2016 Frédéric Massart
1036 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1037 */
1038
1039var EditorAutosaveIoDispatcherInstance = null;
1040
1041function EditorAutosaveIoDispatcher() {
1042 EditorAutosaveIoDispatcher.superclass.constructor.apply(this, arguments);
1043 this._submitEvents = {};
1044 this._queue = [];
1045 this._throttle = null;
1046}
1047EditorAutosaveIoDispatcher.NAME = 'EditorAutosaveIoDispatcher';
1048EditorAutosaveIoDispatcher.ATTRS = {
1049
1050 /**
1051 * The relative path to the ajax script.
1052 *
1053 * @attribute autosaveAjaxScript
1054 * @type String
1055 * @default '/lib/editor/atto/autosave-ajax.php'
1056 * @readOnly
1057 */
1058 autosaveAjaxScript: {
1059 value: '/lib/editor/atto/autosave-ajax.php',
1060 readOnly: true
1061 },
1062
1063 /**
1064 * The time buffer for the throttled requested.
1065 *
1066 * @attribute delay
1067 * @type Number
1068 * @default 50
1069 * @readOnly
1070 */
1071 delay: {
1072 value: 50,
1073 readOnly: true
1074 }
1075
1076};
1077Y.extend(EditorAutosaveIoDispatcher, Y.Base, {
1078
1079 /**
1080 * Dispatch an IO request.
1081 *
1082 * This method will put the requests in a queue in order to attempt to bulk them.
1083 *
1084 * @param {Object} params The parameters of the request.
1085 * @param {Object} context The context in which the callbacks are called.
1086 * @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
1087 * optional keys defining the callbacks to call. Success and Complete
1088 * functions will receive the response as parameter. Success and Complete
1089 * may receive an object containing the error key, use this to confirm
1090 * that no errors occured.
1091 * @return {Void}
1092 */
1093 dispatch: function(params, context, callbacks) {
1094 if (this._throttle) {
1095 this._throttle.cancel();
1096 }
1097
1098 this._throttle = Y.later(this.get('delay'), this, this._processDispatchQueue);
1099 this._queue.push([params, context, callbacks]);
1100 },
1101
1102 /**
1103 * Dispatches the requests in the queue.
1104 *
1105 * @return {Void}
1106 */
1107 _processDispatchQueue: function() {
1108 var queue = this._queue,
1109 data = {};
1110
1111 this._queue = [];
1112 if (queue.length < 1) {
1113 return;
1114 }
1115
1116 Y.Array.each(queue, function(item, index) {
1117 data[index] = item[0];
1118 });
1119
1120 Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
1121 method: 'POST',
1122 data: Y.QueryString.stringify({
1123 actions: data,
1124 sesskey: M.cfg.sesskey
1125 }),
1126 on: {
1127 start: this._makeIoEventCallback('start', queue),
1128 complete: this._makeIoEventCallback('complete', queue),
1129 failure: this._makeIoEventCallback('failure', queue),
1130 end: this._makeIoEventCallback('end', queue),
1131 success: this._makeIoEventCallback('success', queue)
1132 }
1133 });
1134 },
1135
1136 /**
1137 * Creates a function that dispatches an IO response to callbacks.
1138 *
1139 * @param {String} event The type of event.
1140 * @param {Array} queue The queue.
1141 * @return {Function}
1142 */
1143 _makeIoEventCallback: function(event, queue) {
1144 var noop = function() {};
1145 return function() {
1146 var response = arguments[1],
1147 parsed = {};
1148
1149 if ((event == 'complete' || event == 'success') && (typeof response !== 'undefined'
1150 && typeof response.responseText !== 'undefined' && response.responseText !== '')) {
1151
1152 // Success and complete events need to parse the response.
1153 parsed = JSON.parse(response.responseText) || {};
1154 }
1155
1156 Y.Array.each(queue, function(item, index) {
1157 var context = item[1],
1158 cb = (item[2] && item[2][event]) || noop,
1159 arg;
1160
1161 if (parsed && parsed.error) {
1162 // The response is an error, we send it to everyone.
1163 arg = parsed;
1164 } else if (parsed) {
1165 // The response was parsed, we only communicate the relevant portion of the response.
1166 arg = parsed[index];
1167 }
1168
1169 cb.apply(context, [arg]);
1170 });
1171 };
1172 },
1173
1174 /**
1175 * Form submit handler.
1176 *
1177 * @param {EventFacade} e The event.
1178 * @return {Void}
1179 */
1180 _onSubmit: function(e) {
1181 var data = {},
1182 id = e.currentTarget.generateID(),
1183 params = this._submitEvents[id];
1184
1185 if (!params || params.ios.length < 1) {
1186 return;
1187 }
1188
1189 Y.Array.each(params.ios, function(param, index) {
1190 data[index] = param;
1191 });
1192
1193 Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
1194 method: 'POST',
1195 data: Y.QueryString.stringify({
1196 actions: data,
1197 sesskey: M.cfg.sesskey
1198 }),
1199 sync: true
1200 });
1201 },
1202
1203 /**
1204 * Registers a request to be made on form submission.
1205 *
1206 * @param {Node} node The forum node we will listen to.
1207 * @param {Object} params Parameters for the IO request.
1208 * @return {Void}
1209 */
1210 whenSubmit: function(node, params) {
1211 if (typeof this._submitEvents[node.generateID()] === 'undefined') {
1212 this._submitEvents[node.generateID()] = {
1213 event: node.on('submit', this._onSubmit, this),
260565e3 1214 ajaxEvent: node.on(M.core.event.FORM_SUBMIT_AJAX, this._onSubmit, this),
240a9de4
FM
1215 ios: []
1216 };
1217 }
1218 this._submitEvents[node.get('id')].ios.push([params]);
1219 }
1220
1221});
1222EditorAutosaveIoDispatcherInstance = new EditorAutosaveIoDispatcher();
1223
1224
1225function EditorAutosaveIo() {}
1226EditorAutosaveIo.prototype = {
1227
1228 /**
1229 * Dispatch an IO request.
1230 *
1231 * This method will put the requests in a queue in order to attempt to bulk them.
1232 *
1233 * @param {Object} params The parameters of the request.
1234 * @param {Object} context The context in which the callbacks are called.
1235 * @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
1236 * optional keys defining the callbacks to call. Success and Complete
1237 * functions will receive the response as parameter. Success and Complete
1238 * may receive an object containing the error key, use this to confirm
1239 * that no errors occured.
1240 * @return {Void}
1241 */
1242 autosaveIo: function(params, context, callbacks) {
1243 EditorAutosaveIoDispatcherInstance.dispatch(params, context, callbacks);
1244 },
1245
1246 /**
1247 * Registers a request to be made on form submission.
1248 *
1249 * @param {Node} form The forum node we will listen to.
1250 * @param {Object} params Parameters for the IO request.
1251 * @return {Void}
1252 */
1253 autosaveIoOnSubmit: function(form, params) {
1254 EditorAutosaveIoDispatcherInstance.whenSubmit(form, params);
1255 }
1256
1257};
1258Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosaveIo]);
1259// This file is part of Moodle - http://moodle.org/
1260//
1261// Moodle is free software: you can redistribute it and/or modify
1262// it under the terms of the GNU General Public License as published by
1263// the Free Software Foundation, either version 3 of the License, or
1264// (at your option) any later version.
1265//
1266// Moodle is distributed in the hope that it will be useful,
1267// but WITHOUT ANY WARRANTY; without even the implied warranty of
1268// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1269// GNU General Public License for more details.
1270//
1271// You should have received a copy of the GNU General Public License
1272// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
ad3f8cd1 1273/* global LOGNAME */
240a9de4 1274
62467795
AN
1275/**
1276 * @module moodle-editor_atto-editor
1277 * @submodule clean
1278 */
adca7326 1279
62467795
AN
1280/**
1281 * Functions for the Atto editor to clean the generated content.
1282 *
1283 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1284 *
1285 * @namespace M.editor_atto
1286 * @class EditorClean
1287 */
adca7326 1288
62467795 1289function EditorClean() {}
adca7326 1290
3a0bc0fd 1291EditorClean.ATTRS = {
62467795 1292};
adca7326 1293
62467795 1294EditorClean.prototype = {
8951d614 1295 /**
62467795
AN
1296 * Clean the generated HTML content without modifying the editor content.
1297 *
1298 * This includes removes all YUI ids from the generated content.
8951d614 1299 *
62467795 1300 * @return {string} The cleaned HTML content.
8951d614 1301 */
62467795
AN
1302 getCleanHTML: function() {
1303 // Clone the editor so that we don't actually modify the real content.
9389ff57
FM
1304 var editorClone = this.editor.cloneNode(true),
1305 html;
8951d614 1306
62467795
AN
1307 // Remove all YUI IDs.
1308 Y.each(editorClone.all('[id^="yui"]'), function(node) {
1309 node.removeAttribute('id');
1310 });
8951d614 1311
62467795 1312 editorClone.all('.atto_control').remove(true);
9389ff57
FM
1313 html = editorClone.get('innerHTML');
1314
1315 // Revert untouched editor contents to an empty string.
1316 if (html === '<p></p>' || html === '<p><br></p>') {
1317 return '';
1318 }
8951d614 1319
62467795 1320 // Remove any and all nasties from source.
9389ff57 1321 return this._cleanHTML(html);
8951d614
JM
1322 },
1323
adca7326 1324 /**
62467795
AN
1325 * Clean the HTML content of the editor.
1326 *
1327 * @method cleanEditorHTML
1328 * @chainable
adca7326 1329 */
62467795
AN
1330 cleanEditorHTML: function() {
1331 var startValue = this.editor.get('innerHTML');
1332 this.editor.set('innerHTML', this._cleanHTML(startValue));
5ec54dd1 1333
62467795
AN
1334 return this;
1335 },
fe0d2477 1336
62467795
AN
1337 /**
1338 * Clean the specified HTML content and remove any content which could cause issues.
1339 *
1340 * @method _cleanHTML
1341 * @private
1342 * @param {String} content The content to clean
1343 * @return {String} The cleaned HTML
1344 */
1345 _cleanHTML: function(content) {
3ef96361 1346 // Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc.
62467795
AN
1347
1348 var rules = [
3ef96361
EM
1349 // Remove any style blocks. Some browsers do not work well with them in a contenteditable.
1350 // Plus style blocks are not allowed in body html, except with "scoped", which most browsers don't support as of 2015.
1351 // Reference: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work"
1352 {regex: /<style[^>]*>[\s\S]*?<\/style>/gi, replace: ""},
1353
a7fdadc9
EM
1354 // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout.
1355 {regex: /<!--(?![\s\S]*?-->)/gi, replace: ""},
62467795
AN
1356
1357 // Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"
a7fdadc9 1358 // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link.
d784f5ed 1359 {regex: /<\/?(?:title|meta|style|st\d|head|font|html|body|link)[^>]*?>/gi, replace: ""}
62467795 1360 ];
adca7326 1361
3ef96361
EM
1362 return this._filterContentWithRules(content, rules);
1363 },
1364
1365 /**
1366 * Take the supplied content and run on the supplied regex rules.
1367 *
1368 * @method _filterContentWithRules
1369 * @private
1370 * @param {String} content The content to clean
1371 * @param {Array} rules An array of structures: [ {regex: /something/, replace: "something"}, {...}, ...]
1372 * @return {String} The cleaned content
1373 */
1374 _filterContentWithRules: function(content, rules) {
62467795
AN
1375 var i = 0;
1376 for (i = 0; i < rules.length; i++) {
1377 content = content.replace(rules[i].regex, rules[i].replace);
adca7326
DW
1378 }
1379
62467795 1380 return content;
a7fdadc9
EM
1381 },
1382
1383 /**
1384 * Intercept and clean html paste events.
1385 *
1386 * @method pasteCleanup
1387 * @param {Object} sourceEvent The YUI EventFacade object
1388 * @return {Boolean} True if the passed event should continue, false if not.
1389 */
1390 pasteCleanup: function(sourceEvent) {
1391 // We only expect paste events, but we will check anyways.
1392 if (sourceEvent.type === 'paste') {
1393 // The YUI event wrapper doesn't provide paste event info, so we need the underlying event.
1394 var event = sourceEvent._event;
1395 // Check if we have a valid clipboardData object in the event.
1396 // IE has a clipboard object at window.clipboardData, but as of IE 11, it does not provide HTML content access.
73e14433
EM
1397 if (event && event.clipboardData && event.clipboardData.getData && event.clipboardData.types) {
1398 // Check if there is HTML type to be pasted, if we can get it, we want to scrub before insert.
a7fdadc9
EM
1399 var types = event.clipboardData.types;
1400 var isHTML = false;
73e14433
EM
1401 // Different browsers use different containers to hold the types, so test various functions.
1402 if (typeof types.contains === 'function') {
a7fdadc9
EM
1403 isHTML = types.contains('text/html');
1404 } else if (typeof types.indexOf === 'function') {
1405 isHTML = (types.indexOf('text/html') > -1);
a7fdadc9
EM
1406 }
1407
726adbc2 1408 var content;
a7fdadc9
EM
1409 if (isHTML) {
1410 // Get the clipboard content.
a7fdadc9
EM
1411 try {
1412 content = event.clipboardData.getData('text/html');
1413 } catch (error) {
1414 // Something went wrong. Fallback.
1415 this.fallbackPasteCleanupDelayed();
1416 return true;
1417 }
1418
1419 // Stop the original paste.
1420 sourceEvent.preventDefault();
1421
1422 // Scrub the paste content.
3ef96361 1423 content = this._cleanPasteHTML(content);
a7fdadc9
EM
1424
1425 // Save the current selection.
1426 // Using saveSelection as it produces a more consistent experience.
1427 var selection = window.rangy.saveSelection();
1428
1429 // Insert the content.
1430 this.insertContentAtFocusPoint(content);
1431
1432 // Restore the selection, and collapse to end.
1433 window.rangy.restoreSelection(selection);
1434 window.rangy.getSelection().collapseToEnd();
1435
1436 // Update the text area.
1437 this.updateOriginal();
1438 return false;
1439 } else {
726adbc2
DB
1440 try {
1441 // Plaintext clipboard content can only be retrieved this way.
1442 content = event.clipboardData.getData('text');
1443 } catch (error) {
1444 // Something went wrong. Fallback.
1445 // Due to poor cross browser clipboard compatibility, the failure to find html doesn't mean it isn't there.
1446 // Wait for the clipboard event to finish then fallback clean the entire editor.
1447 this.fallbackPasteCleanupDelayed();
1448 return true;
1449 }
a7fdadc9
EM
1450 }
1451 } else {
1452 // If we reached a here, this probably means the browser has limited (or no) clipboard support.
73e14433 1453 // Wait for the clipboard event to finish then fallback clean the entire editor.
a7fdadc9
EM
1454 this.fallbackPasteCleanupDelayed();
1455 return true;
1456 }
1457 }
1458
1459 // We should never get here - we must have received a non-paste event for some reason.
1460 // Um, just call updateOriginalDelayed() - it's safe.
1461 this.updateOriginalDelayed();
1462 return true;
1463 },
1464
1465 /**
1466 * Cleanup code after a paste event if we couldn't intercept the paste content.
1467 *
1468 * @method fallbackPasteCleanup
1469 * @chainable
1470 */
1471 fallbackPasteCleanup: function() {
1472
1473 // Save the current selection (cursor position).
1474 var selection = window.rangy.saveSelection();
1475
1476 // Get, clean, and replace the content in the editable.
1477 var content = this.editor.get('innerHTML');
3ef96361 1478 this.editor.set('innerHTML', this._cleanPasteHTML(content));
a7fdadc9
EM
1479
1480 // Update the textarea.
1481 this.updateOriginal();
1482
1483 // Restore the selection (cursor position).
1484 window.rangy.restoreSelection(selection);
1485
1486 return this;
1487 },
1488
1489 /**
1490 * Calls fallbackPasteCleanup on a short timer to allow the paste event handlers to complete.
1491 *
1492 * @method fallbackPasteCleanupDelayed
1493 * @chainable
1494 */
1495 fallbackPasteCleanupDelayed: function() {
1496 Y.soon(Y.bind(this.fallbackPasteCleanup, this));
1497
1498 return this;
3ef96361
EM
1499 },
1500
1501 /**
1502 * Cleanup html that comes from WYSIWYG paste events. These are more likely to contain messy code that we should strip.
1503 *
1504 * @method _cleanPasteHTML
1505 * @private
1506 * @param {String} content The html content to clean
1507 * @return {String} The cleaned HTML
1508 */
1509 _cleanPasteHTML: function(content) {
1510 // Return an empty string if passed an invalid or empty object.
1511 if (!content || content.length === 0) {
1512 return "";
1513 }
1514
1515 // Rules that get rid of the real-nasties and don't care about normalize code (correct quotes, white spaces, etc).
1516 var rules = [
6ea68e23 1517 // Stuff that is specifically from MS Word and similar office packages.
cfb32192
DM
1518 // Remove all garbage after closing html tag.
1519 {regex: /<\s*\/html\s*>([\s\S]+)$/gi, replace: ""},
6ea68e23
EM
1520 // Remove if comment blocks.
1521 {regex: /<!--\[if[\s\S]*?endif\]-->/gi, replace: ""},
1522 // Remove start and end fragment comment blocks.
1523 {regex: /<!--(Start|End)Fragment-->/gi, replace: ""},
3ef96361
EM
1524 // Remove any xml blocks.
1525 {regex: /<xml[^>]*>[\s\S]*?<\/xml>/gi, replace: ""},
1526 // Remove any <?xml><\?xml> blocks.
1527 {regex: /<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi, replace: ""},
1528 // Remove <o:blah>, <\o:blah>.
df7a9fd4 1529 {regex: /<\/?\w+:[^>]*>/gi, replace: ""}
3ef96361
EM
1530 ];
1531
1532 // Apply the first set of harsher rules.
1533 content = this._filterContentWithRules(content, rules);
1534
1535 // Apply the standard rules, which mainly cleans things like headers, links, and style blocks.
1536 content = this._cleanHTML(content);
1537
1538 // Check if the string is empty or only contains whitespace.
1539 if (content.length === 0 || !content.match(/\S/)) {
1540 return content;
1541 }
1542
1543 // Now we let the browser normalize the code by loading it into the DOM and then get the html back.
1544 // This gives us well quoted, well formatted code to continue our work on. Word may provide very poorly formatted code.
1545 var holder = document.createElement('div');
1546 holder.innerHTML = content;
1547 content = holder.innerHTML;
1548 // Free up the DOM memory.
1549 holder.innerHTML = "";
1550
1551 // Run some more rules that care about quotes and whitespace.
1552 rules = [
8e202bd8
EM
1553 // Get all class attributes so we can work on them.
1554 {regex: /(<[^>]*?class\s*?=\s*?")([^>"]*)(")/gi, replace: function(match, group1, group2, group3) {
1555 // Remove MSO classes.
3a0bc0fd 1556 group2 = group2.replace(/(?:^|[\s])[\s]*MSO[_a-zA-Z0-9\-]*/gi, "");
8e202bd8 1557 // Remove Apple- classes.
3a0bc0fd 1558 group2 = group2.replace(/(?:^|[\s])[\s]*Apple-[_a-zA-Z0-9\-]*/gi, "");
8e202bd8
EM
1559 return group1 + group2 + group3;
1560 }},
3ef96361 1561 // Remove OLE_LINK# anchors that may litter the code.
1b6ce030 1562 {regex: /<a [^>]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi, replace: ""}
3ef96361
EM
1563 ];
1564
91445c63
DW
1565 // Clean all style attributes from the text.
1566 content = this._cleanStyles(content);
1567
3ef96361
EM
1568 // Apply the rules.
1569 content = this._filterContentWithRules(content, rules);
1570
1571 // Reapply the standard cleaner to the content.
1572 content = this._cleanHTML(content);
1573
1b6ce030
EM
1574 // Clean unused spans out of the content.
1575 content = this._cleanSpans(content);
1576
3ef96361 1577 return content;
1b6ce030
EM
1578 },
1579
91445c63
DW
1580 /**
1581 * Clean all inline styles from pasted text.
1582 *
1583 * This code intentionally doesn't use YUI Nodes. YUI was quite a bit slower at this, so using raw DOM objects instead.
1584 *
1585 * @method _cleanStyles
1586 * @private
1587 * @param {String} content The content to clean
1588 * @return {String} The cleaned HTML
1589 */
1590 _cleanStyles: function(content) {
1591 var holder = document.createElement('div');
1592 holder.innerHTML = content;
1593 var elementsWithStyle = holder.querySelectorAll('[style]');
1594 var i = 0;
1595
1596 for (i = 0; i < elementsWithStyle.length; i++) {
1597 elementsWithStyle[i].removeAttribute('style');
1598 }
1599
1600 var elementsWithClass = holder.querySelectorAll('[class]');
1601 for (i = 0; i < elementsWithClass.length; i++) {
1602 elementsWithClass[i].removeAttribute('class');
1603 }
1604
1605 return holder.innerHTML;
1606 },
1b6ce030
EM
1607 /**
1608 * Clean empty or un-unused spans from passed HTML.
1609 *
1610 * This code intentionally doesn't use YUI Nodes. YUI was quite a bit slower at this, so using raw DOM objects instead.
1611 *
1612 * @method _cleanSpans
1613 * @private
1614 * @param {String} content The content to clean
1615 * @return {String} The cleaned HTML
1616 */
1617 _cleanSpans: function(content) {
1618 // Return an empty string if passed an invalid or empty object.
1619 if (!content || content.length === 0) {
1620 return "";
1621 }
1622 // Check if the string is empty or only contains whitespace.
1623 if (content.length === 0 || !content.match(/\S/)) {
1624 return content;
1625 }
1626
1627 var rules = [
1628 // Remove unused class, style, or id attributes. This will make empty tag detection easier later.
1629 {regex: /(<[^>]*?)(?:[\s]*(?:class|style|id)\s*?=\s*?"\s*?")+/gi, replace: "$1"}
1630 ];
1631 // Apply the rules.
1632 content = this._filterContentWithRules(content, rules);
1633
1634 // Reference: "http://stackoverflow.com/questions/8131396/remove-nested-span-without-id"
1635
1636 // This is better to run detached from the DOM, so the browser doesn't try to update on each change.
1637 var holder = document.createElement('div');
1638 holder.innerHTML = content;
1639 var spans = holder.getElementsByTagName('span');
1640
1641 // Since we will be removing elements from the list, we should copy it to an array, making it static.
1642 var spansarr = Array.prototype.slice.call(spans, 0);
1643
1644 spansarr.forEach(function(span) {
1645 if (!span.hasAttributes()) {
1646 // If no attributes (id, class, style, etc), this span is has no effect.
1647 // Move each child (if they exist) to the parent in place of this span.
1648 while (span.firstChild) {
1649 span.parentNode.insertBefore(span.firstChild, span);
1650 }
1651
1652 // Remove the now empty span.
1653 span.parentNode.removeChild(span);
1654 }
1655 });
1656
1657 return holder.innerHTML;
62467795
AN
1658 }
1659};
adca7326 1660
62467795
AN
1661Y.Base.mix(Y.M.editor_atto.Editor, [EditorClean]);
1662// This file is part of Moodle - http://moodle.org/
1663//
1664// Moodle is free software: you can redistribute it and/or modify
1665// it under the terms of the GNU General Public License as published by
1666// the Free Software Foundation, either version 3 of the License, or
1667// (at your option) any later version.
1668//
1669// Moodle is distributed in the hope that it will be useful,
1670// but WITHOUT ANY WARRANTY; without even the implied warranty of
1671// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1672// GNU General Public License for more details.
1673//
1674// You should have received a copy of the GNU General Public License
1675// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
adca7326 1676
dc9ca4ac
DT
1677/**
1678 * @module moodle-editor_atto-editor
1679 * @submodule commands
1680 */
1681
1682/**
1683 * Selection functions for the Atto editor.
1684 *
1685 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1686 *
1687 * @namespace M.editor_atto
1688 * @class EditorCommand
1689 */
1690
1691function EditorCommand() {}
1692
3a0bc0fd 1693EditorCommand.ATTRS = {
dc9ca4ac
DT
1694};
1695
1696EditorCommand.prototype = {
1697 /**
1698 * Applies a callback method to editor if selection is uncollapsed or waits for input to select first.
1699 * @method applyFormat
1700 * @param e EventTarget Event to be passed to callback if selection is uncollapsed
1701 * @param method callback A callback method which changes editor when text is selected.
1702 * @param object context Context to be used for callback method
1703 * @param array args Array of arguments to pass to callback
1704 */
1705 applyFormat: function(e, callback, context, args) {
1706 function handleInsert(e, callback, context, args, anchorNode, anchorOffset) {
1707 // After something is inputed, select it and apply the formating function.
1708 Y.soon(Y.bind(function(e, callback, context, args, anchorNode, anchorOffset) {
1709 var selection = window.rangy.getSelection();
1710
1711 // Set the start of the selection to where it was when the method was first called.
1712 var range = selection.getRangeAt(0);
1713 range.setStart(anchorNode, anchorOffset);
1714 selection.setSingleRange(range);
1715
1716 // Now apply callback to the new text that is selected.
1717 callback.apply(context, [e, args]);
1718
1719 // Collapse selection so cursor is at end of inserted material.
1720 selection.collapseToEnd();
c8719db1
DT
1721
1722 // Save save selection and editor contents.
1723 this.saveSelection();
1724 this.updateOriginal();
dc9ca4ac
DT
1725 }, this, e, callback, context, args, anchorNode, anchorOffset));
1726 }
1727
1728 // Set default context for the method.
1729 context = context || this;
1730
1731 // Check whether range is collapsed.
1732 var selection = window.rangy.getSelection();
1733
1734 if (selection.isCollapsed) {
1735 // Selection is collapsed so listen for input into editor.
1736 var handle = this.editor.once('input', handleInsert, this, callback, context, args,
1737 selection.anchorNode, selection.anchorOffset);
1738
1739 // Cancel if selection changes before input.
1740 this.editor.onceAfter(['click', 'selectstart'], handle.detach, handle);
1741
1742 return;
1743 }
1744
1745 // The range is not collapsed; so apply callback method immediately.
1746 callback.apply(context, [e, args]);
1747
c8719db1
DT
1748 // Save save selection and editor contents.
1749 this.saveSelection();
1750 this.updateOriginal();
dc9ca4ac
DT
1751 },
1752
1753 /**
1754 * Replaces all the tags in a node list with new type.
1755 * @method replaceTags
1756 * @param NodeList nodelist
1757 * @param String tag
1758 */
1759 replaceTags: function(nodelist, tag) {
1760 // We mark elements in the node list for iterations.
1761 nodelist.setAttribute('data-iterate', true);
1762 var node = this.editor.one('[data-iterate="true"]');
1763 while (node) {
1764 var clone = Y.Node.create('<' + tag + ' />')
1765 .setAttrs(node.getAttrs())
1766 .removeAttribute('data-iterate');
1767 // Copy class and style if not blank.
1768 if (node.getAttribute('style')) {
1769 clone.setAttribute('style', node.getAttribute('style'));
1770 }
1771 if (node.getAttribute('class')) {
1772 clone.setAttribute('class', node.getAttribute('class'));
1773 }
1774 // We use childNodes here because we are interested in both type 1 and 3 child nodes.
3a0bc0fd
DP
1775 var children = node.getDOMNode().childNodes;
1776 var child;
dc9ca4ac
DT
1777 child = children[0];
1778 while (typeof child !== "undefined") {
1779 clone.append(child);
1780 child = children[0];
1781 }
1782 node.replace(clone);
1783 node = this.editor.one('[data-iterate="true"]');
1784 }
1785 },
1786
1787 /**
1788 * Change all tags with given type to a span with CSS class attribute.
1789 * @method changeToCSS
1790 * @param String tag Tag type to be changed to span
1791 * @param String markerClass CSS class that corresponds to desired tag
1792 */
1793 changeToCSS: function(tag, markerClass) {
1794 // Save the selection.
1795 var selection = window.rangy.saveSelection();
1796
1797 // Remove display:none from rangy markers so browser doesn't delete them.
1798 this.editor.all('.rangySelectionBoundary').setStyle('display', null);
1799
1800 // Replace tags with CSS classes.
1801 this.editor.all(tag).addClass(markerClass);
1802 this.replaceTags(this.editor.all('.' + markerClass), 'span');
1803
1804 // Restore selection and toggle class.
1805 window.rangy.restoreSelection(selection);
1806 },
1807
1808 /**
1809 * Change spans with CSS classes in editor into elements with given tag.
1810 * @method changeToCSS
1811 * @param String markerClass CSS class that corresponds to desired tag
1812 * @param String tag New tag type to be created
1813 */
1814 changeToTags: function(markerClass, tag) {
1815 // Save the selection.
1816 var selection = window.rangy.saveSelection();
1817
1818 // Remove display:none from rangy markers so browser doesn't delete them.
1819 this.editor.all('.rangySelectionBoundary').setStyle('display', null);
1820
1821 // Replace spans with given tag.
1822 this.replaceTags(this.editor.all('span[class="' + markerClass + '"]'), tag);
1823 this.editor.all(tag + '[class="' + markerClass + '"]').removeAttribute('class');
1824 this.editor.all('.' + markerClass).each(function(n) {
1825 n.wrap('<' + tag + '/>');
1826 n.removeClass(markerClass);
1827 });
1828
1829 // Remove CSS classes.
1830 this.editor.all('[class="' + markerClass + '"]').removeAttribute('class');
1831 this.editor.all(tag).removeClass(markerClass);
1832
1833 // Restore selection.
1834 window.rangy.restoreSelection(selection);
1835 }
1836};
1837
1838Y.Base.mix(Y.M.editor_atto.Editor, [EditorCommand]);
1839// This file is part of Moodle - http://moodle.org/
1840//
1841// Moodle is free software: you can redistribute it and/or modify
1842// it under the terms of the GNU General Public License as published by
1843// the Free Software Foundation, either version 3 of the License, or
1844// (at your option) any later version.
1845//
1846// Moodle is distributed in the hope that it will be useful,
1847// but WITHOUT ANY WARRANTY; without even the implied warranty of
1848// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1849// GNU General Public License for more details.
1850//
1851// You should have received a copy of the GNU General Public License
1852// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1853
62467795
AN
1854/**
1855 * @module moodle-editor_atto-editor
1856 * @submodule toolbar
1857 */
adca7326 1858
62467795
AN
1859/**
1860 * Toolbar functions for the Atto editor.
1861 *
1862 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1863 *
1864 * @namespace M.editor_atto
1865 * @class EditorToolbar
1866 */
adca7326 1867
62467795 1868function EditorToolbar() {}
34f5867a 1869
3a0bc0fd 1870EditorToolbar.ATTRS = {
62467795 1871};
adca7326 1872
62467795 1873EditorToolbar.prototype = {
adca7326 1874 /**
62467795
AN
1875 * A reference to the toolbar Node.
1876 *
1877 * @property toolbar
1878 * @type Node
adca7326 1879 */
62467795 1880 toolbar: null,
adca7326 1881
c63f9053
AN
1882 /**
1883 * A reference to any currently open menus in the toolbar.
1884 *
1885 * @property openMenus
1886 * @type Array
1887 */
1888 openMenus: null,
1889
86a83e3a 1890 /**
62467795 1891 * Setup the toolbar on the editor.
86a83e3a 1892 *
62467795
AN
1893 * @method setupToolbar
1894 * @chainable
86a83e3a 1895 */
62467795
AN
1896 setupToolbar: function() {
1897 this.toolbar = Y.Node.create('<div class="' + CSS.TOOLBAR + '" role="toolbar" aria-live="off"/>');
c63f9053 1898 this.openMenus = [];
62467795 1899 this._wrapper.appendChild(this.toolbar);
86a83e3a 1900
62467795
AN
1901 if (this.textareaLabel) {
1902 this.toolbar.setAttribute('aria-labelledby', this.textareaLabel.get("id"));
1903 }
86a83e3a 1904
62467795
AN
1905 // Add keyboard navigation for the toolbar.
1906 this.setupToolbarNavigation();
86a83e3a 1907
62467795
AN
1908 return this;
1909 }
1910};
86a83e3a 1911
62467795
AN
1912Y.Base.mix(Y.M.editor_atto.Editor, [EditorToolbar]);
1913// This file is part of Moodle - http://moodle.org/
1914//
1915// Moodle is free software: you can redistribute it and/or modify
1916// it under the terms of the GNU General Public License as published by
1917// the Free Software Foundation, either version 3 of the License, or
1918// (at your option) any later version.
1919//
1920// Moodle is distributed in the hope that it will be useful,
1921// but WITHOUT ANY WARRANTY; without even the implied warranty of
1922// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1923// GNU General Public License for more details.
1924//
1925// You should have received a copy of the GNU General Public License
1926// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
ad3f8cd1 1927/* global LOGNAME */
62467795
AN
1928
1929/**
1930 * @module moodle-editor_atto-editor
1931 * @submodule toolbarnav
1932 */
1933
1934/**
1935 * Toolbar Navigation functions for the Atto editor.
1936 *
1937 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1938 *
1939 * @namespace M.editor_atto
1940 * @class EditorToolbarNav
1941 */
1942
1943function EditorToolbarNav() {}
1944
3a0bc0fd 1945EditorToolbarNav.ATTRS = {
62467795
AN
1946};
1947
1948EditorToolbarNav.prototype = {
1949 /**
1950 * The current focal point for tabbing.
1951 *
1952 * @property _tabFocus
1953 * @type Node
1954 * @default null
1955 * @private
1956 */
1957 _tabFocus: null,
1958
1959 /**
1960 * Set up the watchers for toolbar navigation.
1961 *
1962 * @method setupToolbarNavigation
1963 * @chainable
1964 */
1965 setupToolbarNavigation: function() {
1966 // Listen for Arrow left and Arrow right keys.
1967 this._wrapper.delegate('key',
1968 this.toolbarKeyboardNavigation,
1969 'down:37,39',
1970 '.' + CSS.TOOLBAR,
1971 this);
c63f9053
AN
1972 this._wrapper.delegate('focus',
1973 function(e) {
1974 this._setTabFocus(e.currentTarget);
1975 }, '.' + CSS.TOOLBAR + ' button', this);
62467795
AN
1976
1977 return this;
86a83e3a
DW
1978 },
1979
adca7326 1980 /**
62467795
AN
1981 * Implement arrow key navigation for the buttons in the toolbar.
1982 *
1983 * @method toolbarKeyboardNavigation
1984 * @param {EventFacade} e - the keyboard event.
adca7326 1985 */
62467795
AN
1986 toolbarKeyboardNavigation: function(e) {
1987 // Prevent the default browser behaviour.
1988 e.preventDefault();
adca7326 1989
62467795 1990 // On cursor moves we loops through the buttons.
b9d065ed 1991 var buttons = this.toolbar.all('button'),
62467795 1992 direction = 1,
af31595b 1993 button,
62467795
AN
1994 current = e.target.ancestor('button', true);
1995
b9d065ed
FM
1996 if (e.keyCode === 37) {
1997 // Moving left so reverse the direction.
1998 direction = -1;
26f8822d 1999 }
b269f635 2000
b9d065ed
FM
2001 button = this._findFirstFocusable(buttons, current, direction);
2002 if (button) {
2003 button.focus();
5e543b4f 2004 this._setTabFocus(button);
b9d065ed 2005 } else {
62467795 2006 }
b9d065ed 2007 },
adca7326 2008
b9d065ed
FM
2009 /**
2010 * Find the first focusable button.
2011 *
2012 * @param {NodeList} buttons A list of nodes.
2013 * @param {Node} startAt The node in the list to start the search from.
2014 * @param {Number} direction The direction in which to search (1 or -1).
2015 * @return {Node | Undefined} The Node or undefined.
2016 * @method _findFirstFocusable
2017 * @private
2018 */
2019 _findFirstFocusable: function(buttons, startAt, direction) {
2020 var checkCount = 0,
2021 group,
2022 candidate,
2023 button,
2024 index;
2025
2026 // Determine which button to start the search from.
2027 index = buttons.indexOf(startAt);
2028 if (index < -1) {
2029 index = 0;
62467795
AN
2030 }
2031
b9d065ed 2032 // Try to find the next.
af31595b 2033 while (checkCount < buttons.size()) {
62467795
AN
2034 index += direction;
2035 if (index < 0) {
2036 index = buttons.size() - 1;
2037 } else if (index >= buttons.size()) {
2038 // Handle wrapping.
2039 index = 0;
2040 }
af31595b
FM
2041
2042 candidate = buttons.item(index);
adca7326 2043
62467795
AN
2044 // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item.
2045 checkCount++;
af31595b 2046
62467795 2047 // Loop while:
af31595b
FM
2048 // * we haven't checked every button;
2049 // * the button is hidden or disabled;
2050 // * the group is hidden.
2051 if (candidate.hasAttribute('hidden') || candidate.hasAttribute('disabled')) {
2052 continue;
2053 }
2054 group = candidate.ancestor('.atto_group');
2055 if (group.hasAttribute('hidden')) {
2056 continue;
2057 }
2058
2059 button = candidate;
2060 break;
62467795 2061 }
af31595b 2062
b9d065ed
FM
2063 return button;
2064 },
2065
2066 /**
2067 * Check the tab focus.
2068 *
2069 * When we disable or hide a button, we should call this method to ensure that the
2070 * focus is not currently set on an inaccessible button, otherwise tabbing to the toolbar
2071 * would be impossible.
2072 *
2073 * @method checkTabFocus
2074 * @chainable
2075 */
2076 checkTabFocus: function() {
2077 if (this._tabFocus) {
2078 if (this._tabFocus.hasAttribute('disabled') || this._tabFocus.hasAttribute('hidden')
2079 || this._tabFocus.ancestor('.atto_group').hasAttribute('hidden')) {
2080 // Find first available button.
557f44d9 2081 var button = this._findFirstFocusable(this.toolbar.all('button'), this._tabFocus, -1);
b9d065ed
FM
2082 if (button) {
2083 if (this._tabFocus.compareTo(document.activeElement)) {
2084 // We should also move the focus, because the inaccessible button also has the focus.
2085 button.focus();
2086 }
2087 this._setTabFocus(button);
2088 }
2089 }
62467795 2090 }
b9d065ed 2091 return this;
62467795 2092 },
d088a835 2093
62467795
AN
2094 /**
2095 * Sets tab focus for the toolbar to the specified Node.
2096 *
2097 * @method _setTabFocus
2098 * @param {Node} button The node that focus should now be set to
2099 * @chainable
2100 * @private
2101 */
2102 _setTabFocus: function(button) {
2103 if (this._tabFocus) {
2104 // Unset the previous entry.
2105 this._tabFocus.setAttribute('tabindex', '-1');
2106 }
26f8822d 2107
62467795
AN
2108 // Set up the new entry.
2109 this._tabFocus = button;
2110 this._tabFocus.setAttribute('tabindex', 0);
4c37c1f4 2111
62467795
AN
2112 // And update the activedescendant to point at the currently selected button.
2113 this.toolbar.setAttribute('aria-activedescendant', this._tabFocus.generateID());
3ee53a42 2114
62467795
AN
2115 return this;
2116 }
2117};
67d3fe45 2118
62467795
AN
2119Y.Base.mix(Y.M.editor_atto.Editor, [EditorToolbarNav]);
2120// This file is part of Moodle - http://moodle.org/
2121//
2122// Moodle is free software: you can redistribute it and/or modify
2123// it under the terms of the GNU General Public License as published by
2124// the Free Software Foundation, either version 3 of the License, or
2125// (at your option) any later version.
2126//
2127// Moodle is distributed in the hope that it will be useful,
2128// but WITHOUT ANY WARRANTY; without even the implied warranty of
2129// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2130// GNU General Public License for more details.
2131//
2132// You should have received a copy of the GNU General Public License
2133// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
ad3f8cd1 2134/* global rangy */
adca7326 2135
62467795
AN
2136/**
2137 * @module moodle-editor_atto-editor
2138 * @submodule selection
2139 */
adca7326 2140
62467795
AN
2141/**
2142 * Selection functions for the Atto editor.
2143 *
2144 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
2145 *
2146 * @namespace M.editor_atto
2147 * @class EditorSelection
2148 */
fcb5b5c4 2149
62467795 2150function EditorSelection() {}
fcb5b5c4 2151
3a0bc0fd 2152EditorSelection.ATTRS = {
62467795 2153};
3ee53a42 2154
62467795 2155EditorSelection.prototype = {
3ee53a42
DW
2156
2157 /**
62467795
AN
2158 * List of saved selections per editor instance.
2159 *
2160 * @property _selections
2161 * @private
3ee53a42 2162 */
62467795 2163 _selections: null,
adca7326
DW
2164
2165 /**
62467795
AN
2166 * A unique identifier for the last selection recorded.
2167 *
2168 * @property _lastSelection
2169 * @param lastselection
2170 * @type string
2171 * @private
adca7326 2172 */
62467795 2173 _lastSelection: null,
adca7326
DW
2174
2175 /**
62467795
AN
2176 * Whether focus came from a click event.
2177 *
2178 * This is used to determine whether to restore the selection or not.
2179 *
2180 * @property _focusFromClick
2181 * @type Boolean
2182 * @default false
2183 * @private
adca7326 2184 */
62467795 2185 _focusFromClick: false,
adca7326 2186
90902953
DM
2187 /**
2188 * Whether if the last gesturemovestart event target was contained in this editor or not.
2189 *
2190 * @property _gesturestartededitor
2191 * @type Boolean
2192 * @default false
2193 * @private
2194 */
2195 _gesturestartededitor: false,
2196
adca7326 2197 /**
62467795
AN
2198 * Set up the watchers for selection save and restoration.
2199 *
2200 * @method setupSelectionWatchers
2201 * @chainable
adca7326 2202 */
62467795
AN
2203 setupSelectionWatchers: function() {
2204 // Save the selection when a change was made.
2205 this.on('atto:selectionchanged', this.saveSelection, this);
adca7326 2206
62467795 2207 this.editor.on('focus', this.restoreSelection, this);
adca7326 2208
62467795
AN
2209 // Do not restore selection when focus is from a click event.
2210 this.editor.on('mousedown', function() {
2211 this._focusFromClick = true;
2212 }, this);
adca7326 2213
62467795
AN
2214 // Copy the current value back to the textarea when focus leaves us and save the current selection.
2215 this.editor.on('blur', function() {
2216 // Clear the _focusFromClick value.
2217 this._focusFromClick = false;
adca7326 2218
62467795
AN
2219 // Update the original text area.
2220 this.updateOriginal();
2221 }, this);
adca7326 2222
8bca3609 2223 this.editor.on(['keyup', 'focus'], function(e) {
ee376395 2224 Y.soon(Y.bind(this._hasSelectionChanged, this, e));
8bca3609 2225 }, this);
e3050641 2226
90902953
DM
2227 Y.one(document.body).on('gesturemovestart', function(e) {
2228 if (this._wrapper.contains(e.target._node)) {
2229 this._gesturestartededitor = true;
2230 } else {
2231 this._gesturestartededitor = false;
2232 }
2233 }, null, this);
923589d7 2234
90902953
DM
2235 Y.one(document.body).on('gesturemoveend', function(e) {
2236 if (!this._gesturestartededitor) {
2237 // Ignore the event if movestart target was not contained in the editor.
923589d7
DM
2238 return;
2239 }
2240 Y.soon(Y.bind(this._hasSelectionChanged, this, e));
1f0d77f7
EM
2241 }, {
2242 // Standalone will make sure all editors receive the end event.
2243 standAlone: true
2244 }, this);
e3050641 2245
62467795 2246 return this;
b269f635
DW
2247 },
2248
adca7326 2249 /**
62467795
AN
2250 * Work out if the cursor is in the editable area for this editor instance.
2251 *
2252 * @method isActive
2253 * @return {boolean}
adca7326 2254 */
62467795
AN
2255 isActive: function() {
2256 var range = rangy.createRange(),
2257 selection = rangy.getSelection();
adca7326 2258
62467795
AN
2259 if (!selection.rangeCount) {
2260 // If there was no range count, then there is no selection.
2261 return false;
2262 }
adca7326 2263
1ce04361 2264 // We can't be active if the editor doesn't have focus at the moment.
9b9a3abf
DT
2265 if (!document.activeElement ||
2266 !(this.editor.compareTo(document.activeElement) || this.editor.contains(document.activeElement))) {
1ce04361
AN
2267 return false;
2268 }
2269
62467795
AN
2270 // Check whether the range intersects the editor selection.
2271 range.selectNode(this.editor.getDOMNode());
2272 return range.intersectsRange(selection.getRangeAt(0));
adca7326
DW
2273 },
2274
2275 /**
62467795
AN
2276 * Create a cross browser selection object that represents a YUI node.
2277 *
2278 * @method getSelectionFromNode
2279 * @param {Node} YUI Node to base the selection upon.
2280 * @return {[rangy.Range]}
adca7326 2281 */
62467795 2282 getSelectionFromNode: function(node) {
d321f68b
DW
2283 var range = rangy.createRange();
2284 range.selectNode(node.getDOMNode());
2285 return [range];
adca7326
DW
2286 },
2287
26f8822d 2288 /**
62467795
AN
2289 * Save the current selection to an internal property.
2290 *
2291 * This allows more reliable return focus, helping improve keyboard navigation.
2292 *
2293 * Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/restoreSelection"}}{{/crossLink}}.
2294 *
2295 * @method saveSelection
26f8822d 2296 */
62467795 2297 saveSelection: function() {
36265972
AN
2298 if (this.isActive()) {
2299 this._selections = this.getSelection();
2300 }
26f8822d
DW
2301 },
2302
2303 /**
62467795
AN
2304 * Restore any stored selection when the editor gets focus again.
2305 *
2306 * Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/saveSelection"}}{{/crossLink}}.
2307 *
2308 * @method restoreSelection
26f8822d 2309 */
62467795
AN
2310 restoreSelection: function() {
2311 if (!this._focusFromClick) {
2312 if (this._selections) {
2313 this.setSelection(this._selections);
26f8822d
DW
2314 }
2315 }
62467795 2316 this._focusFromClick = false;
26f8822d
DW
2317 },
2318
adca7326 2319 /**
62467795
AN
2320 * Get the selection object that can be passed back to setSelection.
2321 *
2322 * @method getSelection
2323 * @return {array} An array of rangy ranges.
adca7326 2324 */
62467795 2325 getSelection: function() {
d321f68b 2326 return rangy.getSelection().getAllRanges();
adca7326
DW
2327 },
2328
2329 /**
62467795
AN
2330 * Check that a YUI node it at least partly contained by the current selection.
2331 *
2332 * @method selectionContainsNode
2333 * @param {Node} The node to check.
2334 * @return {boolean}
adca7326 2335 */
62467795 2336 selectionContainsNode: function(node) {
d321f68b 2337 return rangy.getSelection().containsNode(node.getDOMNode(), true);
adca7326
DW
2338 },
2339
3ee53a42 2340 /**
62467795
AN
2341 * Runs a filter on each node in the selection, and report whether the
2342 * supplied selector(s) were found in the supplied Nodes.
3ee53a42 2343 *
62467795
AN
2344 * By default, all specified nodes must match the selection, but this
2345 * can be controlled with the requireall property.
2346 *
2347 * @method selectionFilterMatches
3ee53a42 2348 * @param {String} selector
62467795
AN
2349 * @param {NodeList} [selectednodes] For performance this should be passed. If not passed, this will be looked up each time.
2350 * @param {Boolean} [requireall=true] Used to specify that "any" match is good enough.
3ee53a42
DW
2351 * @return {Boolean}
2352 */
62467795
AN
2353 selectionFilterMatches: function(selector, selectednodes, requireall) {
2354 if (typeof requireall === 'undefined') {
d321f68b
DW
2355 requireall = true;
2356 }
3ee53a42
DW
2357 if (!selectednodes) {
2358 // Find this because it was not passed as a param.
62467795 2359 selectednodes = this.getSelectedNodes();
3ee53a42 2360 }
62467795
AN
2361 var allmatch = selectednodes.size() > 0,
2362 anymatch = false;
2363
2364 var editor = this.editor,
2365 stopFn = function(node) {
e76ae807
FM
2366 // The function getSelectedNodes only returns nodes within the editor, so this test is safe.
2367 return node === editor;
62467795
AN
2368 };
2369
e76ae807
FM
2370 // If we do not find at least one match in the editor, no point trying to find them in the selection.
2371 if (!editor.one(selector)) {
2372 return false;
2373 }
2374
3a0bc0fd 2375 selectednodes.each(function(node) {
67d3fe45 2376 // Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing.
d321f68b
DW
2377 if (requireall) {
2378 // Check for at least one failure.
62467795 2379 if (!allmatch || !node.ancestor(selector, true, stopFn)) {
d321f68b
DW
2380 allmatch = false;
2381 }
2382 } else {
2383 // Check for at least one match.
62467795 2384 if (!anymatch && node.ancestor(selector, true, stopFn)) {
d321f68b
DW
2385 anymatch = true;
2386 }
3ee53a42 2387 }
67d3fe45 2388 }, this);
d321f68b
DW
2389 if (requireall) {
2390 return allmatch;
2391 } else {
2392 return anymatch;
2393 }
3ee53a42
DW
2394 },
2395
2396 /**
67d3fe45
SH
2397 * Get the deepest possible list of nodes in the current selection.
2398 *
62467795
AN
2399 * @method getSelectedNodes
2400 * @return {NodeList}
3ee53a42 2401 */
62467795 2402 getSelectedNodes: function() {
d321f68b
DW
2403 var results = new Y.NodeList(),
2404 nodes,
2405 selection,
67d3fe45 2406 range,
d321f68b
DW
2407 node,
2408 i;
2409
2410 selection = rangy.getSelection();
2411
2412 if (selection.rangeCount) {
2413 range = selection.getRangeAt(0);
2414 } else {
2415 // Empty range.
2416 range = rangy.createRange();
67d3fe45 2417 }
d321f68b
DW
2418
2419 if (range.collapsed) {
e9883836
FM
2420 // We do not want to select all the nodes in the editor if we managed to
2421 // have a collapsed selection directly in the editor.
17f253fa
AN
2422 // It's also possible for the commonAncestorContainer to be the document, which selectNode does not handle
2423 // so we must filter that out here too.
2424 if (range.commonAncestorContainer !== this.editor.getDOMNode()
2425 && range.commonAncestorContainer !== Y.config.doc) {
e9883836
FM
2426 range = range.cloneRange();
2427 range.selectNode(range.commonAncestorContainer);
2428 }
d321f68b
DW
2429 }
2430
2431 nodes = range.getNodes();
2432
2433 for (i = 0; i < nodes.length; i++) {
2434 node = Y.one(nodes[i]);
62467795 2435 if (this.editor.contains(node)) {
d321f68b 2436 results.push(node);
67d3fe45
SH
2437 }
2438 }
2439 return results;
3ee53a42
DW
2440 },
2441
2442 /**
62467795 2443 * Check whether the current selection has changed since this method was last called.
67d3fe45 2444 *
62467795 2445 * If the selection has changed, the atto:selectionchanged event is also fired.
67d3fe45 2446 *
62467795 2447 * @method _hasSelectionChanged
67d3fe45
SH
2448 * @private
2449 * @param {EventFacade} e
62467795 2450 * @return {Boolean}
3ee53a42 2451 */
62467795 2452 _hasSelectionChanged: function(e) {
d321f68b 2453 var selection = rangy.getSelection(),
67d3fe45 2454 range,
d321f68b
DW
2455 changed = false;
2456
2457 if (selection.rangeCount) {
2458 range = selection.getRangeAt(0);
2459 } else {
2460 // Empty range.
2461 range = rangy.createRange();
67d3fe45 2462 }
d321f68b 2463
62467795
AN
2464 if (this._lastSelection) {
2465 if (!this._lastSelection.equals(range)) {
d321f68b 2466 changed = true;
62467795 2467 return this._fireSelectionChanged(e);
d321f68b 2468 }
67d3fe45 2469 }
62467795 2470 this._lastSelection = range;
d321f68b 2471 return changed;
67d3fe45 2472 },
3ee53a42 2473
67d3fe45
SH
2474 /**
2475 * Fires the atto:selectionchanged event.
2476 *
62467795 2477 * When the selectionchanged event is fired, the following arguments are provided:
67d3fe45 2478 * - event : the original event that lead to this event being fired.
67d3fe45
SH
2479 * - selectednodes : an array containing nodes that are entirely selected of contain partially selected content.
2480 *
62467795 2481 * @method _fireSelectionChanged
67d3fe45
SH
2482 * @private
2483 * @param {EventFacade} e
2484 */
62467795 2485 _fireSelectionChanged: function(e) {
67d3fe45 2486 this.fire('atto:selectionchanged', {
62467795
AN
2487 event: e,
2488 selectedNodes: this.getSelectedNodes()
67d3fe45
SH
2489 });
2490 },
2491
adca7326 2492 /**
62467795
AN
2493 * Get the DOM node representing the common anscestor of the selection nodes.
2494 *
2495 * @method getSelectionParentNode
2496 * @return {Element|boolean} The DOM Node for this parent, or false if no seletion was made.
adca7326 2497 */
62467795 2498 getSelectionParentNode: function() {
d321f68b
DW
2499 var selection = rangy.getSelection();
2500 if (selection.rangeCount) {
2501 return selection.getRangeAt(0).commonAncestorContainer;
34f5867a 2502 }
34f5867a 2503 return false;
adca7326
DW
2504 },
2505
adca7326
DW
2506 /**
2507 * Set the current selection. Used to restore a selection.
62467795
AN
2508 *
2509 * @method selection
2510 * @param {array} ranges A list of rangy.range objects in the selection.
adca7326 2511 */
62467795 2512 setSelection: function(ranges) {
d321f68b
DW
2513 var selection = rangy.getSelection();
2514 selection.setRanges(ranges);
34f5867a
DW
2515 },
2516
62467795
AN
2517 /**
2518 * Inserts the given HTML into the editable content at the currently focused point.
2519 *
2520 * @method insertContentAtFocusPoint
2521 * @param {String} html
39c6f62d 2522 * @return {Node} The YUI Node object added to the DOM.
62467795
AN
2523 */
2524 insertContentAtFocusPoint: function(html) {
2525 var selection = rangy.getSelection(),
2526 range,
2527 node = Y.Node.create(html);
2528 if (selection.rangeCount) {
2529 range = selection.getRangeAt(0);
2530 }
2531 if (range) {
2532 range.deleteContents();
2533 range.insertNode(node.getDOMNode());
2534 }
39c6f62d 2535 return node;
62467795
AN
2536 }
2537
2538};
2539
2540Y.Base.mix(Y.M.editor_atto.Editor, [EditorSelection]);
2541// This file is part of Moodle - http://moodle.org/
2542//
2543// Moodle is free software: you can redistribute it and/or modify
2544// it under the terms of the GNU General Public License as published by
2545// the Free Software Foundation, either version 3 of the License, or
2546// (at your option) any later version.
2547//
2548// Moodle is distributed in the hope that it will be useful,
2549// but WITHOUT ANY WARRANTY; without even the implied warranty of
2550// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2551// GNU General Public License for more details.
2552//
2553// You should have received a copy of the GNU General Public License
2554// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
ad3f8cd1 2555/* global rangy */
62467795
AN
2556
2557/**
2558 * @module moodle-editor_atto-editor
2559 * @submodule styling
2560 */
2561
2562/**
2563 * Editor styling functions for the Atto editor.
2564 *
2565 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
2566 *
2567 * @namespace M.editor_atto
2568 * @class EditorStyling
2569 */
2570
2571function EditorStyling() {}
2572
3a0bc0fd 2573EditorStyling.ATTRS = {
62467795
AN
2574};
2575
2576EditorStyling.prototype = {
f6bef145
FM
2577 /**
2578 * Disable CSS styling.
2579 *
62467795 2580 * @method disableCssStyling
f6bef145 2581 */
62467795 2582 disableCssStyling: function() {
f6bef145
FM
2583 try {
2584 document.execCommand("styleWithCSS", 0, false);
2585 } catch (e1) {
2586 try {
2587 document.execCommand("useCSS", 0, true);
2588 } catch (e2) {
2589 try {
2590 document.execCommand('styleWithCSS', false, false);
2591 } catch (e3) {
2592 // We did our best.
2593 }
2594 }
2595 }
2596 },
2597
2598 /**
2599 * Enable CSS styling.
2600 *
62467795 2601 * @method enableCssStyling
f6bef145 2602 */
62467795 2603 enableCssStyling: function() {
f6bef145
FM
2604 try {
2605 document.execCommand("styleWithCSS", 0, true);
2606 } catch (e1) {
2607 try {
2608 document.execCommand("useCSS", 0, false);
2609 } catch (e2) {
2610 try {
2611 document.execCommand('styleWithCSS', false, true);
2612 } catch (e3) {
2613 // We did our best.
2614 }
2615 }
2616 }
2faf4c45
SH
2617 },
2618
bed1abbc
AD
2619 /**
2620 * Change the formatting for the current selection.
62467795
AN
2621 *
2622 * This will wrap the selection in span tags, adding the provided classes.
bed1abbc
AD
2623 *
2624 * If the selection covers multiple block elements, multiple spans will be inserted to preserve the original structure.
2625 *
62467795 2626 * @method toggleInlineSelectionClass
bed1abbc
AD
2627 * @param {Array} toggleclasses - Class names to be toggled on or off.
2628 */
62467795 2629 toggleInlineSelectionClass: function(toggleclasses) {
af6a2e94 2630 var classname = toggleclasses.join(" ");
b3c10f9f 2631 var cssApplier = rangy.createClassApplier(classname, {normalize: true});
af6a2e94
DW
2632
2633 cssApplier.toggleSelection();
af6a2e94
DW
2634 },
2635
2636 /**
2637 * Change the formatting for the current selection.
2638 *
2639 * This will set inline styles on the current selection.
2640 *
7bb2069b 2641 * @method formatSelectionInlineStyle
af6a2e94
DW
2642 * @param {Array} styles - Style attributes to set on the nodes.
2643 */
2644 formatSelectionInlineStyle: function(styles) {
2645 var classname = this.PLACEHOLDER_CLASS;
b3c10f9f 2646 var cssApplier = rangy.createClassApplier(classname, {normalize: true});
af6a2e94
DW
2647
2648 cssApplier.applyToSelection();
2649
3a0bc0fd 2650 this.editor.all('.' + classname).each(function(node) {
af6a2e94
DW
2651 node.removeClass(classname).setStyles(styles);
2652 }, this);
2653
af6a2e94
DW
2654 },
2655
2656 /**
2657 * Change the formatting for the current selection.
2658 *
2659 * Also changes the selection to the newly formatted block (allows applying multiple styles to a block).
2660 *
2661 * @method formatSelectionBlock
2662 * @param {String} [blocktag] Change the block level tag to this. Empty string, means do not change the tag.
2663 * @param {Object} [attributes] The keys and values for attributes to be added/changed in the block tag.
2664 * @return {Node|boolean} The Node that was formatted if a change was made, otherwise false.
2665 */
2666 formatSelectionBlock: function(blocktag, attributes) {
2667 // First find the nearest ancestor of the selection that is a block level element.
62467795 2668 var selectionparentnode = this.getSelectionParentNode(),
af6a2e94
DW
2669 boundary,
2670 cell,
2671 nearestblock,
2672 newcontent,
2673 match,
2674 replacement;
bed1abbc
AD
2675
2676 if (!selectionparentnode) {
2677 // No selection, nothing to format.
af6a2e94 2678 return false;
bed1abbc
AD
2679 }
2680
af6a2e94 2681 boundary = this.editor;
bed1abbc 2682
af6a2e94
DW
2683 selectionparentnode = Y.one(selectionparentnode);
2684
2685 // If there is a table cell in between the selectionparentnode and the boundary,
2686 // move the boundary to the table cell.
2687 // This is because we might have a table in a div, and we select some text in a cell,
2688 // want to limit the change in style to the table cell, not the entire table (via the outer div).
3a0bc0fd 2689 cell = selectionparentnode.ancestor(function(node) {
af6a2e94
DW
2690 var tagname = node.get('tagName');
2691 if (tagname) {
2692 tagname = tagname.toLowerCase();
bed1abbc 2693 }
af6a2e94
DW
2694 return (node === boundary) ||
2695 (tagname === 'td') ||
2696 (tagname === 'th');
2697 }, true);
bed1abbc 2698
af6a2e94
DW
2699 if (cell) {
2700 // Limit the scope to the table cell.
2701 boundary = cell;
2702 }
adca7326 2703
af6a2e94
DW
2704 nearestblock = selectionparentnode.ancestor(this.BLOCK_TAGS.join(', '), true);
2705 if (nearestblock) {
2706 // Check that the block is contained by the boundary.
3a0bc0fd 2707 match = nearestblock.ancestor(function(node) {
af6a2e94
DW
2708 return node === boundary;
2709 }, false);
bed1abbc 2710
af6a2e94
DW
2711 if (!match) {
2712 nearestblock = false;
bed1abbc 2713 }
af6a2e94 2714 }
bed1abbc 2715
af6a2e94
DW
2716 // No valid block element - make one.
2717 if (!nearestblock) {
2718 // There is no block node in the content, wrap the content in a p and use that.
2719 newcontent = Y.Node.create('<p></p>');
3a0bc0fd 2720 boundary.get('childNodes').each(function(child) {
af6a2e94
DW
2721 newcontent.append(child.remove());
2722 });
2723 boundary.append(newcontent);
2724 nearestblock = newcontent;
2725 }
2726
2727 // Guaranteed to have a valid block level element contained in the contenteditable region.
2728 // Change the tag to the new block level tag.
2729 if (blocktag && blocktag !== '') {
2730 // Change the block level node for a new one.
2731 replacement = Y.Node.create('<' + blocktag + '></' + blocktag + '>');
2732 // Copy all attributes.
2733 replacement.setAttrs(nearestblock.getAttrs());
2734 // Copy all children.
3a0bc0fd 2735 nearestblock.get('childNodes').each(function(child) {
af6a2e94
DW
2736 child.remove();
2737 replacement.append(child);
2738 });
2739
2740 nearestblock.replace(replacement);
2741 nearestblock = replacement;
bed1abbc 2742 }
af6a2e94
DW
2743
2744 // Set the attributes on the block level tag.
2745 if (attributes) {
2746 nearestblock.setAttrs(attributes);
2747 }
2748
2749 // Change the selection to the modified block. This makes sense when we might apply multiple styles
2750 // to the block.
2751 var selection = this.getSelectionFromNode(nearestblock);
2752 this.setSelection(selection);
2753
2754 return nearestblock;
bed1abbc 2755 }
af6a2e94 2756
adca7326 2757};
67d3fe45 2758
62467795
AN
2759Y.Base.mix(Y.M.editor_atto.Editor, [EditorStyling]);
2760// This file is part of Moodle - http://moodle.org/
2761//
2762// Moodle is free software: you can redistribute it and/or modify
2763// it under the terms of the GNU General Public License as published by
2764// the Free Software Foundation, either version 3 of the License, or
2765// (at your option) any later version.
2766//
2767// Moodle is distributed in the hope that it will be useful,
2768// but WITHOUT ANY WARRANTY; without even the implied warranty of
2769// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2770// GNU General Public License for more details.
2771//
2772// You should have received a copy of the GNU General Public License
2773// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
2774
2775/**
2776 * @module moodle-editor_atto-editor
2777 * @submodule filepicker
2778 */
adca7326
DW
2779
2780/**
62467795 2781 * Filepicker options for the Atto editor.
adca7326 2782 *
62467795
AN
2783 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
2784 *
2785 * @namespace M.editor_atto
2786 * @class EditorFilepicker
adca7326 2787 */
adca7326 2788
62467795 2789function EditorFilepicker() {}
adca7326 2790
3a0bc0fd 2791EditorFilepicker.ATTRS = {
adca7326 2792 /**
62467795 2793 * The options for the filepicker.
adca7326 2794 *
62467795
AN
2795 * @attribute filepickeroptions
2796 * @type object