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