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