1 YUI.add('moodle-editor_atto-editor', function (Y, NAME) {
3 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
19 * The Atto WYSIWG pluggable editor, written for Moodle.
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
29 * @module moodle-editor_atto-editor
30 * @submodule editor-base
33 var LOGNAME = 'moodle-editor_atto-editor';
35 CONTENT: 'editor_atto_content',
36 CONTENTWRAPPER: 'editor_atto_content_wrap',
37 TOOLBAR: 'editor_atto_toolbar',
38 WRAPPER: 'editor_atto',
39 HIGHLIGHT: 'highlight'
43 * The Atto editor for Moodle.
45 * @namespace M.editor_atto
48 * @uses M.editor_atto.EditorClean
49 * @uses M.editor_atto.EditorFilepicker
50 * @uses M.editor_atto.EditorSelection
51 * @uses M.editor_atto.EditorStyling
52 * @uses M.editor_atto.EditorTextArea
53 * @uses M.editor_atto.EditorToolbar
54 * @uses M.editor_atto.EditorToolbarNav
58 Editor.superclass.constructor.apply(this, arguments);
61 Y.extend(Editor, Y.Base, {
64 * List of known block level tags.
65 * Taken from "https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements".
67 * @property BLOCK_TAGS
106 PLACEHOLDER_CLASS: 'atto-tmp-class',
107 ALL_NODES_SELECTOR: '[style],font[face]',
108 FONT_FAMILY: 'fontFamily',
111 * The wrapper containing the editor.
120 * A reference to the content editable Node.
128 * A reference to the original text area.
136 * A reference to the label associated with the original text area.
138 * @property textareaLabel
144 * A reference to the list of plugins.
152 * Event Handles to clear on editor destruction.
154 * @property _eventHandles
159 initializer: function() {
162 // Note - it is not safe to use a CSS selector like '#' + elementid because the id
163 // may have colons in it - e.g. quiz.
164 this.textarea = Y.one(document.getElementById(this.get('elementid')));
166 if (!this.textarea) {
167 // No text area found.
168 Y.log('Text area not found - unable to setup editor for ' + this.get('elementid'),
173 this._eventHandles = [];
175 this._wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />');
176 template = Y.Handlebars.compile('<div id="{{elementid}}editable" ' +
177 'contenteditable="true" ' +
179 'spellcheck="true" ' +
181 'class="{{CSS.CONTENT}}" ' +
183 this.editor = Y.Node.create(template({
184 elementid: this.get('elementid'),
188 // Add a labelled-by attribute to the contenteditable.
189 this.textareaLabel = Y.one('[for="' + this.get('elementid') + '"]');
190 if (this.textareaLabel) {
191 this.textareaLabel.generateID();
192 this.editor.setAttribute('aria-labelledby', this.textareaLabel.get("id"));
195 // Add everything to the wrapper.
198 // Editable content wrapper.
199 var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />');
200 content.appendChild(this.editor);
201 this._wrapper.appendChild(content);
203 // Style the editor. According to the styles.css: 20 is the line-height, 8 is padding-top + padding-bottom.
204 this.editor.setStyle('minHeight', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px');
207 // We set a height here to force the overflow because decent browsers allow the CSS property resize.
208 this.editor.setStyle('height', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px');
211 // Disable odd inline CSS styles.
212 this.disableCssStyling();
214 // Use paragraphs not divs.
215 if (document.queryCommandSupported('DefaultParagraphSeparator')) {
216 document.execCommand('DefaultParagraphSeparator', false, 'p');
219 // Add the toolbar and editable zone to the page.
220 this.textarea.get('parentNode').insert(this._wrapper, this.textarea).
221 setAttribute('class', 'editor_atto_wrap');
223 // Hide the old textarea.
224 this.textarea.hide();
226 // Copy the text to the contenteditable div.
227 this.updateFromTextArea();
229 // Publish the events that are defined by this editor.
230 this.publishEvents();
232 // Add handling for saving and restoring selections on cursor/focus changes.
233 this.setupSelectionWatchers();
235 // Add polling to update the textarea periodically when typing long content.
236 this.setupAutomaticPolling();
241 // Initialize the auto-save timer.
242 this.setupAutosave();
243 // Preload the icons for the notifications.
244 this.setupNotifications();
248 * Focus on the editable area for this editor.
260 * Publish events for this editor instance.
262 * @method publishEvents
266 publishEvents: function() {
268 * Fired when changes are made within the editor.
272 this.publish('change', {
278 * Fired when all plugins have completed loading.
280 * @event pluginsloaded
282 this.publish('pluginsloaded', {
286 this.publish('atto:selectionchanged', {
294 * Set up automated polling of the text area to update the textarea.
296 * @method setupAutomaticPolling
299 setupAutomaticPolling: function() {
300 this._registerEventHandle(this.editor.on(['keyup', 'cut'], this.updateOriginal, this));
301 this._registerEventHandle(this.editor.on('paste', this.pasteCleanup, this));
303 // Call this.updateOriginal after dropped content has been processed.
304 this._registerEventHandle(this.editor.on('drop', this.updateOriginalDelayed, this));
310 * Calls updateOriginal on a short timer to allow native event handlers to run first.
312 * @method updateOriginalDelayed
315 updateOriginalDelayed: function() {
316 Y.soon(Y.bind(this.updateOriginal, this));
321 setupPlugins: function() {
322 // Clear the list of plugins.
325 var plugins = this.get('plugins');
333 for (groupIndex in plugins) {
334 group = plugins[groupIndex];
335 if (!group.plugins) {
336 // No plugins in this group - skip it.
339 for (pluginIndex in group.plugins) {
340 plugin = group.plugins[pluginIndex];
342 pluginConfig = Y.mix({
346 toolbar: this.toolbar,
350 // Add a reference to the current editor.
351 if (typeof Y.M['atto_' + plugin.name] === "undefined") {
352 Y.log("Plugin '" + plugin.name + "' could not be found - skipping initialisation", "warn", LOGNAME);
355 this.plugins[plugin.name] = new Y.M['atto_' + plugin.name].Button(pluginConfig);
359 // Some plugins need to perform actions once all plugins have loaded.
360 this.fire('pluginsloaded');
365 enablePlugins: function(plugin) {
366 this._setPluginState(true, plugin);
369 disablePlugins: function(plugin) {
370 this._setPluginState(false, plugin);
373 _setPluginState: function(enable, plugin) {
374 var target = 'disableButtons';
376 target = 'enableButtons';
380 this.plugins[plugin][target]();
382 Y.Object.each(this.plugins, function(currentPlugin) {
383 currentPlugin[target]();
389 * Register an event handle for disposal in the destructor.
391 * @method _registerEventHandle
392 * @param {EventHandle} The Event Handle as returned by Y.on, and Y.delegate.
395 _registerEventHandle: function(handle) {
396 this._eventHandles.push(handle);
403 * The unique identifier for the form element representing the editor.
405 * @attribute elementid
415 * The contextid of the form.
417 * @attribute contextid
427 * Plugins with their configuration.
429 * The plugins structure is:
433 * "group": "groupName",
436 * "configKey": "configValue"
439 * "configKey": "configValue"
444 * "group": "groupName",
447 * "configKey": "configValue"
464 // The Editor publishes custom events that can be subscribed to.
465 Y.augment(Editor, Y.EventTarget);
467 Y.namespace('M.editor_atto').Editor = Editor;
469 // Function for Moodle's initialisation.
470 Y.namespace('M.editor_atto.Editor').init = function(config) {
471 return new Y.M.editor_atto.Editor(config);
473 // This file is part of Moodle - http://moodle.org/
475 // Moodle is free software: you can redistribute it and/or modify
476 // it under the terms of the GNU General Public License as published by
477 // the Free Software Foundation, either version 3 of the License, or
478 // (at your option) any later version.
480 // Moodle is distributed in the hope that it will be useful,
481 // but WITHOUT ANY WARRANTY; without even the implied warranty of
482 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
483 // GNU General Public License for more details.
485 // You should have received a copy of the GNU General Public License
486 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
489 * A notify function for the Atto editor.
491 * @module moodle-editor_atto-notify
493 * @package editor_atto
494 * @copyright 2014 Damyon Wiese
495 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
498 var LOGNAME_NOTIFY = 'moodle-editor_atto-editor-notify',
499 NOTIFY_INFO = 'info',
500 NOTIFY_WARNING = 'warning';
502 function EditorNotify() {}
504 EditorNotify.ATTRS= {
507 EditorNotify.prototype = {
510 * A single Y.Node for this editor. There is only ever one - it is replaced if a new message comes in.
512 * @property messageOverlay
515 messageOverlay: null,
518 * A single timer object that can be used to cancel the hiding behaviour.
520 * @property hideTimer
526 * Initialize the notifications.
528 * @method setupNotifications
531 setupNotifications: function() {
532 var preload1 = new Image(),
533 preload2 = new Image();
535 preload1.src = M.util.image_url('i/warning', 'moodle');
536 preload2.src = M.util.image_url('i/info', 'moodle');
542 * Show a notification in a floaty overlay somewhere in the atto editor text area.
544 * @method showMessage
545 * @param {String} message The translated message (use get_string)
546 * @param {String} type Must be either "info" or "warning"
547 * @param {Number} timeout Time in milliseconds to show this message for.
550 showMessage: function(message, type, timeout) {
551 var messageTypeIcon = '',
555 if (this.messageOverlay === null) {
556 this.messageOverlay = Y.Node.create('<div class="editor_atto_notification"></div>');
558 this.messageOverlay.hide(true);
559 this.textarea.get('parentNode').append(this.messageOverlay);
561 this.messageOverlay.on('click', function() {
562 this.messageOverlay.hide(true);
566 if (this.hideTimer !== null) {
567 this.hideTimer.cancel();
570 if (type === NOTIFY_WARNING) {
571 messageTypeIcon = '<img src="' +
572 M.util.image_url('i/warning', 'moodle') +
573 '" alt="' + M.util.get_string('warning', 'moodle') + '"/>';
574 } else if (type === NOTIFY_INFO) {
575 messageTypeIcon = '<img src="' +
576 M.util.image_url('i/info', 'moodle') +
577 '" alt="' + M.util.get_string('info', 'moodle') + '"/>';
579 Y.log('Invalid message type specified: ' + type + '. Must be either "info" or "warning".', 'debug', LOGNAME_NOTIFY);
582 // Parse the timeout value.
583 intTimeout = parseInt(timeout, 10);
584 if (intTimeout <= 0) {
588 // Convert class to atto_info (for example).
589 type = 'atto_' + type;
591 bodyContent = Y.Node.create('<div class="' + type + '" role="alert" aria-live="assertive">' +
592 messageTypeIcon + ' ' +
593 Y.Escape.html(message) +
595 this.messageOverlay.empty();
596 this.messageOverlay.append(bodyContent);
597 this.messageOverlay.show(true);
599 this.hideTimer = Y.later(intTimeout, this, function() {
600 Y.log('Hide Atto notification.', 'debug', LOGNAME_NOTIFY);
601 this.hideTimer = null;
602 this.messageOverlay.hide(true);
610 Y.Base.mix(Y.M.editor_atto.Editor, [EditorNotify]);
611 // This file is part of Moodle - http://moodle.org/
613 // Moodle is free software: you can redistribute it and/or modify
614 // it under the terms of the GNU General Public License as published by
615 // the Free Software Foundation, either version 3 of the License, or
616 // (at your option) any later version.
618 // Moodle is distributed in the hope that it will be useful,
619 // but WITHOUT ANY WARRANTY; without even the implied warranty of
620 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
621 // GNU General Public License for more details.
623 // You should have received a copy of the GNU General Public License
624 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
627 * @module moodle-editor_atto-editor
628 * @submodule textarea
632 * Textarea functions for the Atto editor.
634 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
636 * @namespace M.editor_atto
637 * @class EditorTextArea
640 function EditorTextArea() {}
642 EditorTextArea.ATTRS= {
645 EditorTextArea.prototype = {
648 * Return the appropriate empty content value for the current browser.
650 * Different browsers use a different content when they are empty and
651 * we must set this reliable across the board.
653 * @method _getEmptyContent
654 * @return String The content to use representing no user-provided content
657 _getEmptyContent: function() {
658 if (Y.UA.ie && Y.UA.ie < 10) {
661 return '<p><br></p>';
666 * Copy and clean the text from the textarea into the contenteditable div.
668 * If the text is empty, provide a default paragraph tag to hold the content.
670 * @method updateFromTextArea
673 updateFromTextArea: function() {
675 this.editor.setHTML('');
677 // Copy text to editable div.
678 this.editor.append(this.textarea.get('value'));
681 this.cleanEditorHTML();
683 // Insert a paragraph in the empty contenteditable div.
684 if (this.editor.getHTML() === '') {
685 this.editor.setHTML(this._getEmptyContent());
690 * Copy the text from the contenteditable to the textarea which it replaced.
692 * @method updateOriginal
695 updateOriginal : function() {
696 // Get the previous and current value to compare them.
697 var oldValue = this.textarea.get('value'),
698 newValue = this.getCleanHTML();
700 if (newValue === "" && this.isActive()) {
701 // The content was entirely empty so get the empty content placeholder.
702 newValue = this._getEmptyContent();
705 // Only call this when there has been an actual change to reduce processing.
706 if (oldValue !== newValue) {
707 // Insert the cleaned content.
708 this.textarea.set('value', newValue);
710 // Trigger the onchange callback on the textarea, essentially to notify moodle-core-formchangechecker.
711 this.textarea.simulate('change');
713 // Trigger handlers for this action.
721 Y.Base.mix(Y.M.editor_atto.Editor, [EditorTextArea]);
722 // This file is part of Moodle - http://moodle.org/
724 // Moodle is free software: you can redistribute it and/or modify
725 // it under the terms of the GNU General Public License as published by
726 // the Free Software Foundation, either version 3 of the License, or
727 // (at your option) any later version.
729 // Moodle is distributed in the hope that it will be useful,
730 // but WITHOUT ANY WARRANTY; without even the implied warranty of
731 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
732 // GNU General Public License for more details.
734 // You should have received a copy of the GNU General Public License
735 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
738 * A autosave function for the Atto editor.
740 * @module moodle-editor_atto-autosave
741 * @submodule autosave-base
742 * @package editor_atto
743 * @copyright 2014 Damyon Wiese
744 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
747 var SUCCESS_MESSAGE_TIMEOUT = 5000,
748 RECOVER_MESSAGE_TIMEOUT = 60000,
749 LOGNAME_AUTOSAVE = 'moodle-editor_atto-editor-autosave';
751 function EditorAutosave() {}
753 EditorAutosave.ATTRS= {
755 * Enable/Disable auto save for this instance.
757 * @attribute autosaveEnabled
767 * The time between autosaves (in seconds).
769 * @attribute autosaveFrequency
780 * Unique hash for this page instance. Calculated from $PAGE->url in php.
782 * @attribute pageHash
792 * The relative path to the ajax script.
794 * @attribute autosaveAjaxScript
796 * @default '/lib/editor/atto/autosave-ajax.php'
799 autosaveAjaxScript: {
800 value: '/lib/editor/atto/autosave-ajax.php',
805 EditorAutosave.prototype = {
808 * The text that was auto saved in the last request.
818 * @property autosaveInstance
821 autosaveInstance: null,
824 * Initialize the autosave process
826 * @method setupAutosave
829 setupAutosave: function() {
833 options = this.get('filepickeroptions');
835 if (!this.get('autosaveEnabled')) {
836 // Autosave disabled for this instance.
840 this.autosaveInstance = Y.stamp(this);
841 for (optiontype in options) {
842 if (typeof options[optiontype].itemid !== "undefined") {
843 draftid = options[optiontype].itemid;
847 // First see if there are any saved drafts.
848 // Make an ajax request.
849 url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
851 sesskey: M.cfg.sesskey,
852 contextid: this.get('contextid'),
856 elementid: this.get('elementid'),
857 pageinstance: this.autosaveInstance,
858 pagehash: this.get('pageHash')
866 success: function(id,o) {
867 if (typeof o.responseText !== "undefined" && o.responseText !== "") {
868 response_json = JSON.parse(o.responseText);
870 // Revert untouched editor contents to an empty string.
871 // Check for FF and Chrome.
872 if (response_json.result === '<p></p>' || response_json.result === '<p><br></p>' ||
873 response_json.result === '<br>') {
874 response_json.result = '';
877 // Check for IE 9 and 10.
878 if (response_json.result === '<p> </p>' || response_json.result === '<p><br> </p>') {
879 response_json.result = '';
882 if (response_json.error || typeof response_json.result === 'undefined') {
883 Y.log('Error occurred recovering draft text: ' + response_json.error, 'debug', LOGNAME_AUTOSAVE);
884 this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'), NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
885 } else if (response_json.result !== this.textarea.get('value') &&
886 response_json.result !== '') {
887 Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
888 this.recoverText(response_json.result);
890 this._fireSelectionChanged();
893 failure: function() {
894 this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'), NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
899 // Now setup the timer for periodic saves.
901 var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000;
902 Y.later(delay, this, this.saveDraft, false, true);
904 // Now setup the listener for form submission.
905 form = this.textarea.ancestor('form');
907 form.on('submit', this.resetAutosave, this);
913 * Clear the autosave text because the form was submitted normally.
915 * @method resetAutosave
918 resetAutosave: function() {
919 // Make an ajax request to reset the autosaved text.
920 url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
922 sesskey: M.cfg.sesskey,
923 contextid: this.get('contextid'),
925 elementid: this.get('elementid'),
926 pageinstance: this.autosaveInstance,
927 pagehash: this.get('pageHash')
940 * Recover a previous version of this text and show a message.
942 * @method recoverText
943 * @param {String} text
946 recoverText: function(text) {
947 this.editor.setHTML(text);
948 this.saveSelection();
949 this.updateOriginal();
950 this.lastText = text;
952 this.showMessage(M.util.get_string('textrecovered', 'editor_atto'), NOTIFY_INFO, RECOVER_MESSAGE_TIMEOUT);
958 * Save a single draft via ajax.
963 saveDraft: function() {
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();
968 var newText = this.textarea.get('value');
970 if (newText !== this.lastText) {
971 Y.log('Autosave text', 'debug', LOGNAME_AUTOSAVE);
973 // Make an ajax request.
974 url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
976 sesskey: M.cfg.sesskey,
977 contextid: this.get('contextid'),
980 elementid: this.get('elementid'),
981 pagehash: this.get('pageHash'),
982 pageinstance: this.autosaveInstance
985 // Reusable error handler - must be passed the correct context.
986 var ajaxErrorFunction = function(code, response) {
987 var errorDuration = parseInt(this.get('autosaveFrequency'), 10) * 1000;
988 Y.log('Error while autosaving text:' + code, 'warn', LOGNAME_AUTOSAVE);
989 Y.log(response, 'warn', LOGNAME_AUTOSAVE);
990 this.showMessage(M.util.get_string('autosavefailed', 'editor_atto'), NOTIFY_WARNING, errorDuration);
998 error: ajaxErrorFunction,
999 failure: ajaxErrorFunction,
1000 success: function(code, response) {
1001 if (response.responseText !== "") {
1002 Y.soon(Y.bind(ajaxErrorFunction, this, [code, response]));
1005 this.lastText = newText;
1006 this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'), NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
1016 Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosave]);
1017 // This file is part of Moodle - http://moodle.org/
1019 // Moodle is free software: you can redistribute it and/or modify
1020 // it under the terms of the GNU General Public License as published by
1021 // the Free Software Foundation, either version 3 of the License, or
1022 // (at your option) any later version.
1024 // Moodle is distributed in the hope that it will be useful,
1025 // but WITHOUT ANY WARRANTY; without even the implied warranty of
1026 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1027 // GNU General Public License for more details.
1029 // You should have received a copy of the GNU General Public License
1030 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1033 * @module moodle-editor_atto-editor
1038 * Functions for the Atto editor to clean the generated content.
1040 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1042 * @namespace M.editor_atto
1043 * @class EditorClean
1046 function EditorClean() {}
1048 EditorClean.ATTRS= {
1051 EditorClean.prototype = {
1053 * Clean the generated HTML content without modifying the editor content.
1055 * This includes removes all YUI ids from the generated content.
1057 * @return {string} The cleaned HTML content.
1059 getCleanHTML: function() {
1060 // Clone the editor so that we don't actually modify the real content.
1061 var editorClone = this.editor.cloneNode(true),
1064 // Remove all YUI IDs.
1065 Y.each(editorClone.all('[id^="yui"]'), function(node) {
1066 node.removeAttribute('id');
1069 editorClone.all('.atto_control').remove(true);
1070 html = editorClone.get('innerHTML');
1072 // Revert untouched editor contents to an empty string.
1073 if (html === '<p></p>' || html === '<p><br></p>') {
1077 // Remove any and all nasties from source.
1078 return this._cleanHTML(html);
1082 * Clean the HTML content of the editor.
1084 * @method cleanEditorHTML
1087 cleanEditorHTML: function() {
1088 var startValue = this.editor.get('innerHTML');
1089 this.editor.set('innerHTML', this._cleanHTML(startValue));
1095 * Clean the specified HTML content and remove any content which could cause issues.
1097 * @method _cleanHTML
1099 * @param {String} content The content to clean
1100 * @return {String} The cleaned HTML
1102 _cleanHTML: function(content) {
1103 // Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc.
1106 // Remove any style blocks. Some browsers do not work well with them in a contenteditable.
1107 // Plus style blocks are not allowed in body html, except with "scoped", which most browsers don't support as of 2015.
1108 // Reference: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work"
1109 {regex: /<style[^>]*>[\s\S]*?<\/style>/gi, replace: ""},
1111 // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout.
1112 {regex: /<!--(?![\s\S]*?-->)/gi, replace: ""},
1114 // Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"
1115 // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link.
1116 {regex: /<\/?(?:title|meta|style|st\d|head|font|html|body|link)[^>]*?>/gi, replace: ""}
1119 return this._filterContentWithRules(content, rules);
1123 * Take the supplied content and run on the supplied regex rules.
1125 * @method _filterContentWithRules
1127 * @param {String} content The content to clean
1128 * @param {Array} rules An array of structures: [ {regex: /something/, replace: "something"}, {...}, ...]
1129 * @return {String} The cleaned content
1131 _filterContentWithRules: function(content, rules) {
1133 for (i = 0; i < rules.length; i++) {
1134 content = content.replace(rules[i].regex, rules[i].replace);
1141 * Intercept and clean html paste events.
1143 * @method pasteCleanup
1144 * @param {Object} sourceEvent The YUI EventFacade object
1145 * @return {Boolean} True if the passed event should continue, false if not.
1147 pasteCleanup: function(sourceEvent) {
1148 // We only expect paste events, but we will check anyways.
1149 if (sourceEvent.type === 'paste') {
1150 // The YUI event wrapper doesn't provide paste event info, so we need the underlying event.
1151 var event = sourceEvent._event;
1152 // Check if we have a valid clipboardData object in the event.
1153 // IE has a clipboard object at window.clipboardData, but as of IE 11, it does not provide HTML content access.
1154 if (event && event.clipboardData && event.clipboardData.getData) {
1155 // Check if there is HTML type to be pasted, this is all we care about.
1156 var types = event.clipboardData.types;
1158 // Different browsers use different things to hold the types, so test various functions.
1161 } else if (typeof types.contains === 'function') {
1162 isHTML = types.contains('text/html');
1163 } else if (typeof types.indexOf === 'function') {
1164 isHTML = (types.indexOf('text/html') > -1);
1166 if ((types.indexOf('com.apple.webarchive') > -1) || (types.indexOf('com.apple.iWork.TSPNativeData') > -1)) {
1167 // This is going to be a specialized Apple paste paste. We cannot capture this, so clean everything.
1168 this.fallbackPasteCleanupDelayed();
1173 // We don't know how to handle the clipboard info, so wait for the clipboard event to finish then fallback.
1174 this.fallbackPasteCleanupDelayed();
1179 // Get the clipboard content.
1182 content = event.clipboardData.getData('text/html');
1184 // Something went wrong. Fallback.
1185 this.fallbackPasteCleanupDelayed();
1189 // Stop the original paste.
1190 sourceEvent.preventDefault();
1192 // Scrub the paste content.
1193 content = this._cleanPasteHTML(content);
1195 // Save the current selection.
1196 // Using saveSelection as it produces a more consistent experience.
1197 var selection = window.rangy.saveSelection();
1199 // Insert the content.
1200 this.insertContentAtFocusPoint(content);
1202 // Restore the selection, and collapse to end.
1203 window.rangy.restoreSelection(selection);
1204 window.rangy.getSelection().collapseToEnd();
1206 // Update the text area.
1207 this.updateOriginal();
1210 // This is a non-html paste event, we can just let this continue on and call updateOriginalDelayed.
1211 this.updateOriginalDelayed();
1215 // If we reached a here, this probably means the browser has limited (or no) clipboard support.
1216 // Wait for the clipboard event to finish then fallback.
1217 this.fallbackPasteCleanupDelayed();
1222 // We should never get here - we must have received a non-paste event for some reason.
1223 // Um, just call updateOriginalDelayed() - it's safe.
1224 this.updateOriginalDelayed();
1229 * Cleanup code after a paste event if we couldn't intercept the paste content.
1231 * @method fallbackPasteCleanup
1234 fallbackPasteCleanup: function() {
1235 Y.log('Using fallbackPasteCleanup for atto cleanup', 'debug', LOGNAME);
1237 // Save the current selection (cursor position).
1238 var selection = window.rangy.saveSelection();
1240 // Get, clean, and replace the content in the editable.
1241 var content = this.editor.get('innerHTML');
1242 this.editor.set('innerHTML', this._cleanPasteHTML(content));
1244 // Update the textarea.
1245 this.updateOriginal();
1247 // Restore the selection (cursor position).
1248 window.rangy.restoreSelection(selection);
1254 * Calls fallbackPasteCleanup on a short timer to allow the paste event handlers to complete.
1256 * @method fallbackPasteCleanupDelayed
1259 fallbackPasteCleanupDelayed: function() {
1260 Y.soon(Y.bind(this.fallbackPasteCleanup, this));
1266 * Cleanup html that comes from WYSIWYG paste events. These are more likely to contain messy code that we should strip.
1268 * @method _cleanPasteHTML
1270 * @param {String} content The html content to clean
1271 * @return {String} The cleaned HTML
1273 _cleanPasteHTML: function(content) {
1274 // Return an empty string if passed an invalid or empty object.
1275 if (!content || content.length === 0) {
1279 // Rules that get rid of the real-nasties and don't care about normalize code (correct quotes, white spaces, etc).
1281 // Stuff that is specifically from MS Word and similar office packages.
1282 // Remove all garbage after closing html tag.
1283 {regex: /<\s*\/html\s*>([\s\S]+)$/gi, replace: ""},
1284 // Remove if comment blocks.
1285 {regex: /<!--\[if[\s\S]*?endif\]-->/gi, replace: ""},
1286 // Remove start and end fragment comment blocks.
1287 {regex: /<!--(Start|End)Fragment-->/gi, replace: ""},
1288 // Remove any xml blocks.
1289 {regex: /<xml[^>]*>[\s\S]*?<\/xml>/gi, replace: ""},
1290 // Remove any <?xml><\?xml> blocks.
1291 {regex: /<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi, replace: ""},
1292 // Remove <o:blah>, <\o:blah>.
1293 {regex: /<\/?\w+:[^>]*>/gi, replace: ""}
1296 // Apply the first set of harsher rules.
1297 content = this._filterContentWithRules(content, rules);
1299 // Apply the standard rules, which mainly cleans things like headers, links, and style blocks.
1300 content = this._cleanHTML(content);
1302 // Check if the string is empty or only contains whitespace.
1303 if (content.length === 0 || !content.match(/\S/)) {
1307 // Now we let the browser normalize the code by loading it into the DOM and then get the html back.
1308 // This gives us well quoted, well formatted code to continue our work on. Word may provide very poorly formatted code.
1309 var holder = document.createElement('div');
1310 holder.innerHTML = content;
1311 content = holder.innerHTML;
1312 // Free up the DOM memory.
1313 holder.innerHTML = "";
1315 // Run some more rules that care about quotes and whitespace.
1317 // Remove MSO-blah, MSO:blah in style attributes. Only removes one or more that appear in succession.
1318 {regex: /(<[^>]*?style\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[-:][^>;"]*;?)+/gi, replace: "$1"},
1319 // Remove MSO classes in class attributes. Only removes one or more that appear in succession.
1320 {regex: /(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*MSO[_a-zA-Z0-9\-]*)+/gi, replace: "$1"},
1321 // Remove Apple- classes in class attributes. Only removes one or more that appear in succession.
1322 {regex: /(<[^>]*?class\s*?=\s*?"[^>"]*?)(?:[\s]*Apple-[_a-zA-Z0-9\-]*)+/gi, replace: "$1"},
1323 // Remove OLE_LINK# anchors that may litter the code.
1324 {regex: /<a [^>]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi, replace: ""},
1325 // Remove empty spans, but not ones from Rangy.
1326 {regex: /<span(?![^>]*?rangySelectionBoundary[^>]*?)[^>]*>( |\s)*<\/span>/gi, replace: ""}
1330 content = this._filterContentWithRules(content, rules);
1332 // Reapply the standard cleaner to the content.
1333 content = this._cleanHTML(content);
1339 Y.Base.mix(Y.M.editor_atto.Editor, [EditorClean]);
1340 // This file is part of Moodle - http://moodle.org/
1342 // Moodle is free software: you can redistribute it and/or modify
1343 // it under the terms of the GNU General Public License as published by
1344 // the Free Software Foundation, either version 3 of the License, or
1345 // (at your option) any later version.
1347 // Moodle is distributed in the hope that it will be useful,
1348 // but WITHOUT ANY WARRANTY; without even the implied warranty of
1349 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1350 // GNU General Public License for more details.
1352 // You should have received a copy of the GNU General Public License
1353 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1356 * @module moodle-editor_atto-editor
1357 * @submodule toolbar
1361 * Toolbar functions for the Atto editor.
1363 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1365 * @namespace M.editor_atto
1366 * @class EditorToolbar
1369 function EditorToolbar() {}
1371 EditorToolbar.ATTRS= {
1374 EditorToolbar.prototype = {
1376 * A reference to the toolbar Node.
1384 * A reference to any currently open menus in the toolbar.
1386 * @property openMenus
1392 * Setup the toolbar on the editor.
1394 * @method setupToolbar
1397 setupToolbar: function() {
1398 this.toolbar = Y.Node.create('<div class="' + CSS.TOOLBAR + '" role="toolbar" aria-live="off"/>');
1399 this.openMenus = [];
1400 this._wrapper.appendChild(this.toolbar);
1402 if (this.textareaLabel) {
1403 this.toolbar.setAttribute('aria-labelledby', this.textareaLabel.get("id"));
1406 // Add keyboard navigation for the toolbar.
1407 this.setupToolbarNavigation();
1413 Y.Base.mix(Y.M.editor_atto.Editor, [EditorToolbar]);
1414 // This file is part of Moodle - http://moodle.org/
1416 // Moodle is free software: you can redistribute it and/or modify
1417 // it under the terms of the GNU General Public License as published by
1418 // the Free Software Foundation, either version 3 of the License, or
1419 // (at your option) any later version.
1421 // Moodle is distributed in the hope that it will be useful,
1422 // but WITHOUT ANY WARRANTY; without even the implied warranty of
1423 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1424 // GNU General Public License for more details.
1426 // You should have received a copy of the GNU General Public License
1427 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1430 * @module moodle-editor_atto-editor
1431 * @submodule toolbarnav
1435 * Toolbar Navigation functions for the Atto editor.
1437 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1439 * @namespace M.editor_atto
1440 * @class EditorToolbarNav
1443 function EditorToolbarNav() {}
1445 EditorToolbarNav.ATTRS= {
1448 EditorToolbarNav.prototype = {
1450 * The current focal point for tabbing.
1452 * @property _tabFocus
1460 * Set up the watchers for toolbar navigation.
1462 * @method setupToolbarNavigation
1465 setupToolbarNavigation: function() {
1466 // Listen for Arrow left and Arrow right keys.
1467 this._wrapper.delegate('key',
1468 this.toolbarKeyboardNavigation,
1472 this._wrapper.delegate('focus',
1474 this._setTabFocus(e.currentTarget);
1475 }, '.' + CSS.TOOLBAR + ' button', this);
1481 * Implement arrow key navigation for the buttons in the toolbar.
1483 * @method toolbarKeyboardNavigation
1484 * @param {EventFacade} e - the keyboard event.
1486 toolbarKeyboardNavigation: function(e) {
1487 // Prevent the default browser behaviour.
1490 // On cursor moves we loops through the buttons.
1491 var buttons = this.toolbar.all('button'),
1494 current = e.target.ancestor('button', true);
1496 if (e.keyCode === 37) {
1497 // Moving left so reverse the direction.
1501 button = this._findFirstFocusable(buttons, current, direction);
1504 this._setTabFocus(button);
1506 Y.log("Unable to find a button to focus on", 'debug', LOGNAME);
1511 * Find the first focusable button.
1513 * @param {NodeList} buttons A list of nodes.
1514 * @param {Node} startAt The node in the list to start the search from.
1515 * @param {Number} direction The direction in which to search (1 or -1).
1516 * @return {Node | Undefined} The Node or undefined.
1517 * @method _findFirstFocusable
1520 _findFirstFocusable: function(buttons, startAt, direction) {
1527 // Determine which button to start the search from.
1528 index = buttons.indexOf(startAt);
1530 Y.log("Unable to find the button in the list of buttons", 'debug', LOGNAME);
1534 // Try to find the next.
1535 while (checkCount < buttons.size()) {
1538 index = buttons.size() - 1;
1539 } else if (index >= buttons.size()) {
1544 candidate = buttons.item(index);
1546 // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item.
1550 // * we haven't checked every button;
1551 // * the button is hidden or disabled;
1552 // * the group is hidden.
1553 if (candidate.hasAttribute('hidden') || candidate.hasAttribute('disabled')) {
1556 group = candidate.ancestor('.atto_group');
1557 if (group.hasAttribute('hidden')) {
1569 * Check the tab focus.
1571 * When we disable or hide a button, we should call this method to ensure that the
1572 * focus is not currently set on an inaccessible button, otherwise tabbing to the toolbar
1573 * would be impossible.
1575 * @method checkTabFocus
1578 checkTabFocus: function() {
1579 if (this._tabFocus) {
1580 if (this._tabFocus.hasAttribute('disabled') || this._tabFocus.hasAttribute('hidden')
1581 || this._tabFocus.ancestor('.atto_group').hasAttribute('hidden')) {
1582 // Find first available button.
1583 button = this._findFirstFocusable(this.toolbar.all('button'), this._tabFocus, -1);
1585 if (this._tabFocus.compareTo(document.activeElement)) {
1586 // We should also move the focus, because the inaccessible button also has the focus.
1589 this._setTabFocus(button);
1597 * Sets tab focus for the toolbar to the specified Node.
1599 * @method _setTabFocus
1600 * @param {Node} button The node that focus should now be set to
1604 _setTabFocus: function(button) {
1605 if (this._tabFocus) {
1606 // Unset the previous entry.
1607 this._tabFocus.setAttribute('tabindex', '-1');
1610 // Set up the new entry.
1611 this._tabFocus = button;
1612 this._tabFocus.setAttribute('tabindex', 0);
1614 // And update the activedescendant to point at the currently selected button.
1615 this.toolbar.setAttribute('aria-activedescendant', this._tabFocus.generateID());
1621 Y.Base.mix(Y.M.editor_atto.Editor, [EditorToolbarNav]);
1622 // This file is part of Moodle - http://moodle.org/
1624 // Moodle is free software: you can redistribute it and/or modify
1625 // it under the terms of the GNU General Public License as published by
1626 // the Free Software Foundation, either version 3 of the License, or
1627 // (at your option) any later version.
1629 // Moodle is distributed in the hope that it will be useful,
1630 // but WITHOUT ANY WARRANTY; without even the implied warranty of
1631 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1632 // GNU General Public License for more details.
1634 // You should have received a copy of the GNU General Public License
1635 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1638 * @module moodle-editor_atto-editor
1639 * @submodule selection
1643 * Selection functions for the Atto editor.
1645 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1647 * @namespace M.editor_atto
1648 * @class EditorSelection
1651 function EditorSelection() {}
1653 EditorSelection.ATTRS= {
1656 EditorSelection.prototype = {
1659 * List of saved selections per editor instance.
1661 * @property _selections
1667 * A unique identifier for the last selection recorded.
1669 * @property _lastSelection
1670 * @param lastselection
1674 _lastSelection: null,
1677 * Whether focus came from a click event.
1679 * This is used to determine whether to restore the selection or not.
1681 * @property _focusFromClick
1686 _focusFromClick: false,
1689 * Set up the watchers for selection save and restoration.
1691 * @method setupSelectionWatchers
1694 setupSelectionWatchers: function() {
1695 // Save the selection when a change was made.
1696 this.on('atto:selectionchanged', this.saveSelection, this);
1698 this.editor.on('focus', this.restoreSelection, this);
1700 // Do not restore selection when focus is from a click event.
1701 this.editor.on('mousedown', function() {
1702 this._focusFromClick = true;
1705 // Copy the current value back to the textarea when focus leaves us and save the current selection.
1706 this.editor.on('blur', function() {
1707 // Clear the _focusFromClick value.
1708 this._focusFromClick = false;
1710 // Update the original text area.
1711 this.updateOriginal();
1714 this.editor.on(['keyup', 'focus'], function(e) {
1715 Y.soon(Y.bind(this._hasSelectionChanged, this, e));
1718 // To capture both mouseup and touchend events, we need to track the gesturemoveend event in standAlone mode. Without
1719 // standAlone, it will only fire if we listened to a gesturemovestart too.
1720 this.editor.on('gesturemoveend', function(e) {
1721 Y.soon(Y.bind(this._hasSelectionChanged, this, e));
1730 * Work out if the cursor is in the editable area for this editor instance.
1735 isActive: function() {
1736 var range = rangy.createRange(),
1737 selection = rangy.getSelection();
1739 if (!selection.rangeCount) {
1740 // If there was no range count, then there is no selection.
1744 // We can't be active if the editor doesn't have focus at the moment.
1745 if (!document.activeElement ||
1746 !(this.editor.compareTo(document.activeElement) || this.editor.contains(document.activeElement))) {
1750 // Check whether the range intersects the editor selection.
1751 range.selectNode(this.editor.getDOMNode());
1752 return range.intersectsRange(selection.getRangeAt(0));
1756 * Create a cross browser selection object that represents a YUI node.
1758 * @method getSelectionFromNode
1759 * @param {Node} YUI Node to base the selection upon.
1760 * @return {[rangy.Range]}
1762 getSelectionFromNode: function(node) {
1763 var range = rangy.createRange();
1764 range.selectNode(node.getDOMNode());
1769 * Save the current selection to an internal property.
1771 * This allows more reliable return focus, helping improve keyboard navigation.
1773 * Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/restoreSelection"}}{{/crossLink}}.
1775 * @method saveSelection
1777 saveSelection: function() {
1778 if (this.isActive()) {
1779 this._selections = this.getSelection();
1784 * Restore any stored selection when the editor gets focus again.
1786 * Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/saveSelection"}}{{/crossLink}}.
1788 * @method restoreSelection
1790 restoreSelection: function() {
1791 if (!this._focusFromClick) {
1792 if (this._selections) {
1793 this.setSelection(this._selections);
1796 this._focusFromClick = false;
1800 * Get the selection object that can be passed back to setSelection.
1802 * @method getSelection
1803 * @return {array} An array of rangy ranges.
1805 getSelection: function() {
1806 return rangy.getSelection().getAllRanges();
1810 * Check that a YUI node it at least partly contained by the current selection.
1812 * @method selectionContainsNode
1813 * @param {Node} The node to check.
1816 selectionContainsNode: function(node) {
1817 return rangy.getSelection().containsNode(node.getDOMNode(), true);
1821 * Runs a filter on each node in the selection, and report whether the
1822 * supplied selector(s) were found in the supplied Nodes.
1824 * By default, all specified nodes must match the selection, but this
1825 * can be controlled with the requireall property.
1827 * @method selectionFilterMatches
1828 * @param {String} selector
1829 * @param {NodeList} [selectednodes] For performance this should be passed. If not passed, this will be looked up each time.
1830 * @param {Boolean} [requireall=true] Used to specify that "any" match is good enough.
1833 selectionFilterMatches: function(selector, selectednodes, requireall) {
1834 if (typeof requireall === 'undefined') {
1837 if (!selectednodes) {
1838 // Find this because it was not passed as a param.
1839 selectednodes = this.getSelectedNodes();
1841 var allmatch = selectednodes.size() > 0,
1844 var editor = this.editor,
1845 stopFn = function(node) {
1846 // The function getSelectedNodes only returns nodes within the editor, so this test is safe.
1847 return node === editor;
1850 // If we do not find at least one match in the editor, no point trying to find them in the selection.
1851 if (!editor.one(selector)) {
1855 selectednodes.each(function(node){
1856 // Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing.
1858 // Check for at least one failure.
1859 if (!allmatch || !node.ancestor(selector, true, stopFn)) {
1863 // Check for at least one match.
1864 if (!anymatch && node.ancestor(selector, true, stopFn)) {
1877 * Get the deepest possible list of nodes in the current selection.
1879 * @method getSelectedNodes
1880 * @return {NodeList}
1882 getSelectedNodes: function() {
1883 var results = new Y.NodeList(),
1890 selection = rangy.getSelection();
1892 if (selection.rangeCount) {
1893 range = selection.getRangeAt(0);
1896 range = rangy.createRange();
1899 if (range.collapsed) {
1900 // We do not want to select all the nodes in the editor if we managed to
1901 // have a collapsed selection directly in the editor.
1902 // It's also possible for the commonAncestorContainer to be the document, which selectNode does not handle
1903 // so we must filter that out here too.
1904 if (range.commonAncestorContainer !== this.editor.getDOMNode()
1905 && range.commonAncestorContainer !== Y.config.doc) {
1906 range = range.cloneRange();
1907 range.selectNode(range.commonAncestorContainer);
1911 nodes = range.getNodes();
1913 for (i = 0; i < nodes.length; i++) {
1914 node = Y.one(nodes[i]);
1915 if (this.editor.contains(node)) {
1923 * Check whether the current selection has changed since this method was last called.
1925 * If the selection has changed, the atto:selectionchanged event is also fired.
1927 * @method _hasSelectionChanged
1929 * @param {EventFacade} e
1932 _hasSelectionChanged: function(e) {
1933 var selection = rangy.getSelection(),
1937 if (selection.rangeCount) {
1938 range = selection.getRangeAt(0);
1941 range = rangy.createRange();
1944 if (this._lastSelection) {
1945 if (!this._lastSelection.equals(range)) {
1947 return this._fireSelectionChanged(e);
1950 this._lastSelection = range;
1955 * Fires the atto:selectionchanged event.
1957 * When the selectionchanged event is fired, the following arguments are provided:
1958 * - event : the original event that lead to this event being fired.
1959 * - selectednodes : an array containing nodes that are entirely selected of contain partially selected content.
1961 * @method _fireSelectionChanged
1963 * @param {EventFacade} e
1965 _fireSelectionChanged: function(e) {
1966 this.fire('atto:selectionchanged', {
1968 selectedNodes: this.getSelectedNodes()
1973 * Get the DOM node representing the common anscestor of the selection nodes.
1975 * @method getSelectionParentNode
1976 * @return {Element|boolean} The DOM Node for this parent, or false if no seletion was made.
1978 getSelectionParentNode: function() {
1979 var selection = rangy.getSelection();
1980 if (selection.rangeCount) {
1981 return selection.getRangeAt(0).commonAncestorContainer;
1987 * Set the current selection. Used to restore a selection.
1990 * @param {array} ranges A list of rangy.range objects in the selection.
1992 setSelection: function(ranges) {
1993 var selection = rangy.getSelection();
1994 selection.setRanges(ranges);
1998 * Inserts the given HTML into the editable content at the currently focused point.
2000 * @method insertContentAtFocusPoint
2001 * @param {String} html
2002 * @return {Node} The YUI Node object added to the DOM.
2004 insertContentAtFocusPoint: function(html) {
2005 var selection = rangy.getSelection(),
2007 node = Y.Node.create(html);
2008 if (selection.rangeCount) {
2009 range = selection.getRangeAt(0);
2012 range.deleteContents();
2013 range.insertNode(node.getDOMNode());
2020 Y.Base.mix(Y.M.editor_atto.Editor, [EditorSelection]);
2021 // This file is part of Moodle - http://moodle.org/
2023 // Moodle is free software: you can redistribute it and/or modify
2024 // it under the terms of the GNU General Public License as published by
2025 // the Free Software Foundation, either version 3 of the License, or
2026 // (at your option) any later version.
2028 // Moodle is distributed in the hope that it will be useful,
2029 // but WITHOUT ANY WARRANTY; without even the implied warranty of
2030 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2031 // GNU General Public License for more details.
2033 // You should have received a copy of the GNU General Public License
2034 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
2037 * @module moodle-editor_atto-editor
2038 * @submodule styling
2042 * Editor styling functions for the Atto editor.
2044 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
2046 * @namespace M.editor_atto
2047 * @class EditorStyling
2050 function EditorStyling() {}
2052 EditorStyling.ATTRS= {
2055 EditorStyling.prototype = {
2057 * Disable CSS styling.
2059 * @method disableCssStyling
2061 disableCssStyling: function() {
2063 document.execCommand("styleWithCSS", 0, false);
2066 document.execCommand("useCSS", 0, true);
2069 document.execCommand('styleWithCSS', false, false);
2078 * Enable CSS styling.
2080 * @method enableCssStyling
2082 enableCssStyling: function() {
2084 document.execCommand("styleWithCSS", 0, true);
2087 document.execCommand("useCSS", 0, false);
2090 document.execCommand('styleWithCSS', false, true);
2099 * Change the formatting for the current selection.
2101 * This will wrap the selection in span tags, adding the provided classes.
2103 * If the selection covers multiple block elements, multiple spans will be inserted to preserve the original structure.
2105 * @method toggleInlineSelectionClass
2106 * @param {Array} toggleclasses - Class names to be toggled on or off.
2108 toggleInlineSelectionClass: function(toggleclasses) {
2109 var classname = toggleclasses.join(" ");
2110 var originalSelection = this.getSelection();
2111 var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
2113 cssApplier.toggleSelection();
2115 this.setSelection(originalSelection);
2119 * Change the formatting for the current selection.
2121 * This will set inline styles on the current selection.
2123 * @method toggleInlineSelectionClass
2124 * @param {Array} styles - Style attributes to set on the nodes.
2126 formatSelectionInlineStyle: function(styles) {
2127 var classname = this.PLACEHOLDER_CLASS;
2128 var originalSelection = this.getSelection();
2129 var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
2131 cssApplier.applyToSelection();
2133 this.editor.all('.' + classname).each(function (node) {
2134 node.removeClass(classname).setStyles(styles);
2137 this.setSelection(originalSelection);
2141 * Change the formatting for the current selection.
2143 * Also changes the selection to the newly formatted block (allows applying multiple styles to a block).
2145 * @method formatSelectionBlock
2146 * @param {String} [blocktag] Change the block level tag to this. Empty string, means do not change the tag.
2147 * @param {Object} [attributes] The keys and values for attributes to be added/changed in the block tag.
2148 * @return {Node|boolean} The Node that was formatted if a change was made, otherwise false.
2150 formatSelectionBlock: function(blocktag, attributes) {
2151 // First find the nearest ancestor of the selection that is a block level element.
2152 var selectionparentnode = this.getSelectionParentNode(),
2160 if (!selectionparentnode) {
2161 // No selection, nothing to format.
2165 boundary = this.editor;
2167 selectionparentnode = Y.one(selectionparentnode);
2169 // If there is a table cell in between the selectionparentnode and the boundary,
2170 // move the boundary to the table cell.
2171 // This is because we might have a table in a div, and we select some text in a cell,
2172 // want to limit the change in style to the table cell, not the entire table (via the outer div).
2173 cell = selectionparentnode.ancestor(function (node) {
2174 var tagname = node.get('tagName');
2176 tagname = tagname.toLowerCase();
2178 return (node === boundary) ||
2179 (tagname === 'td') ||
2184 // Limit the scope to the table cell.
2188 nearestblock = selectionparentnode.ancestor(this.BLOCK_TAGS.join(', '), true);
2190 // Check that the block is contained by the boundary.
2191 match = nearestblock.ancestor(function (node) {
2192 return node === boundary;
2196 nearestblock = false;
2200 // No valid block element - make one.
2201 if (!nearestblock) {
2202 // There is no block node in the content, wrap the content in a p and use that.
2203 newcontent = Y.Node.create('<p></p>');
2204 boundary.get('childNodes').each(function (child) {
2205 newcontent.append(child.remove());
2207 boundary.append(newcontent);
2208 nearestblock = newcontent;
2211 // Guaranteed to have a valid block level element contained in the contenteditable region.
2212 // Change the tag to the new block level tag.
2213 if (blocktag && blocktag !== '') {
2214 // Change the block level node for a new one.
2215 replacement = Y.Node.create('<' + blocktag + '></' + blocktag + '>');
2216 // Copy all attributes.
2217 replacement.setAttrs(nearestblock.getAttrs());
2218 // Copy all children.
2219 nearestblock.get('childNodes').each(function (child) {
2221 replacement.append(child);
2224 nearestblock.replace(replacement);
2225 nearestblock = replacement;
2228 // Set the attributes on the block level tag.
2230 nearestblock.setAttrs(attributes);
2233 // Change the selection to the modified block. This makes sense when we might apply multiple styles
2235 var selection = this.getSelectionFromNode(nearestblock);
2236 this.setSelection(selection);
2238 return nearestblock;
2243 Y.Base.mix(Y.M.editor_atto.Editor, [EditorStyling]);
2244 // This file is part of Moodle - http://moodle.org/
2246 // Moodle is free software: you can redistribute it and/or modify
2247 // it under the terms of the GNU General Public License as published by
2248 // the Free Software Foundation, either version 3 of the License, or
2249 // (at your option) any later version.
2251 // Moodle is distributed in the hope that it will be useful,
2252 // but WITHOUT ANY WARRANTY; without even the implied warranty of
2253 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2254 // GNU General Public License for more details.
2256 // You should have received a copy of the GNU General Public License
2257 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
2260 * @module moodle-editor_atto-editor
2261 * @submodule filepicker
2265 * Filepicker options for the Atto editor.
2267 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
2269 * @namespace M.editor_atto
2270 * @class EditorFilepicker
2273 function EditorFilepicker() {}
2275 EditorFilepicker.ATTRS= {
2277 * The options for the filepicker.
2279 * @attribute filepickeroptions
2283 filepickeroptions: {
2288 EditorFilepicker.prototype = {
2290 * Should we show the filepicker for this filetype?
2292 * @method canShowFilepicker
2293 * @param string type The media type for the file picker.
2296 canShowFilepicker: function(type) {
2297 return (typeof this.get('filepickeroptions')[type] !== 'undefined');
2301 * Show the filepicker.
2303 * This depends on core_filepicker, and then call that modules show function.
2305 * @method showFilepicker
2306 * @param {string} type The media type for the file picker.
2307 * @param {function} callback The callback to use when selecting an item of media.
2308 * @param {object} [context] The context from which to call the callback.
2310 showFilepicker: function(type, callback, context) {
2312 Y.use('core_filepicker', function (Y) {
2313 var options = Y.clone(self.get('filepickeroptions')[type], true);
2314 options.formcallback = callback;
2316 options.magicscope = context;
2319 M.core_filepicker.show(Y, options);
2324 Y.Base.mix(Y.M.editor_atto.Editor, [EditorFilepicker]);
2338 "moodle-core-notification-dialogue",
2339 "moodle-core-notification-confirm",
2340 "moodle-editor_atto-rangy",