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