Autoría | Ultima modificación | Ver Log |
YUI.add('moodle-editor_atto-editor', function (Y, NAME) {// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./* eslint-disable no-unused-vars *//*** The Atto WYSIWG pluggable editor, written for Moodle.** @module moodle-editor_atto-editor* @package editor_atto* @copyright 2013 Damyon Wiese <damyon@moodle.com>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later* @main moodle-editor_atto-editor*//*** @module moodle-editor_atto-editor* @submodule editor-base*/var LOGNAME = 'moodle-editor_atto-editor';var CSS = {CONTENT: 'editor_atto_content',CONTENTWRAPPER: 'editor_atto_content_wrap',TOOLBAR: 'editor_atto_toolbar',WRAPPER: 'editor_atto',HIGHLIGHT: 'highlight'},rangy = window.rangy;/*** The Atto editor for Moodle.** @namespace M.editor_atto* @class Editor* @constructor* @uses M.editor_atto.EditorClean* @uses M.editor_atto.EditorFilepicker* @uses M.editor_atto.EditorSelection* @uses M.editor_atto.EditorStyling* @uses M.editor_atto.EditorTextArea* @uses M.editor_atto.EditorToolbar* @uses M.editor_atto.EditorToolbarNav*/function Editor() {Editor.superclass.constructor.apply(this, arguments);}Y.extend(Editor, Y.Base, {/*** List of known block level tags.* Taken from "https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements".** @property BLOCK_TAGS* @type {Array}*/BLOCK_TAGS: ['address','article','aside','audio','blockquote','canvas','dd','div','dl','fieldset','figcaption','figure','footer','form','h1','h2','h3','h4','h5','h6','header','hgroup','hr','noscript','ol','output','p','pre','section','table','tfoot','ul','video'],PLACEHOLDER_CLASS: 'atto-tmp-class',ALL_NODES_SELECTOR: '[style],font[face]',FONT_FAMILY: 'fontFamily',/*** The wrapper containing the editor.** @property _wrapper* @type Node* @private*/_wrapper: null,/*** A reference to the content editable Node.** @property editor* @type Node*/editor: null,/*** A reference to the original text area.** @property textarea* @type Node*/textarea: null,/*** A reference to the label associated with the original text area.** @property textareaLabel* @type Node*/textareaLabel: null,/*** A reference to the list of plugins.** @property plugins* @type object*/plugins: null,/*** An indicator of the current input direction.** @property coreDirection* @type string*/coreDirection: null,/*** Enable/disable the empty placeholder content.** @property enableAppropriateEmptyContent* @type Boolean*/enableAppropriateEmptyContent: null,/*** Event Handles to clear on editor destruction.** @property _eventHandles* @private*/_eventHandles: null,initializer: function() {var template;// Note - it is not safe to use a CSS selector like '#' + elementid because the id// may have colons in it - e.g. quiz.this.textarea = Y.one(document.getElementById(this.get('elementid')));if (!this.textarea) {// No text area found.return;}var extraclasses = this.textarea.getAttribute('class');this._eventHandles = [];var description = Y.Node.create('<div class="sr-only">' + M.util.get_string('richtexteditor', 'editor_atto') + '</div>');this._wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" role="application" />');this._wrapper.appendChild(description);this._wrapper.setAttribute('aria-describedby', description.generateID());template = Y.Handlebars.compile('<div id="{{elementid}}editable" ' +'contenteditable="true" ' +'role="textbox" ' +'spellcheck="true" ' +'aria-live="off" ' +'class="{{CSS.CONTENT}} ' + extraclasses + '" ' +'/>');this.editor = Y.Node.create(template({elementid: this.get('elementid'),CSS: CSS}));// Add a labelled-by attribute to the contenteditable.this.textareaLabel = Y.one('[for="' + this.get('elementid') + '"]');if (this.textareaLabel) {this.textareaLabel.generateID();this.editor.setAttribute('aria-labelledby', this.textareaLabel.get("id"));}// Set diretcion according to current page language.this.coreDirection = Y.one('body').hasClass('dir-rtl') ? 'rtl' : 'ltr';// Enable the placeholder for empty content.this.enablePlaceholderForEmptyContent();// Add everything to the wrapper.this.setupToolbar();// Editable content wrapper.var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />');content.appendChild(this.editor);this._wrapper.appendChild(content);// Style the editor. According to the styles.css: 20 is the line-height, 8 is padding-top + padding-bottom.this.editor.setStyle('minHeight', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px');if (Y.UA.ie === 0) {// We set a height here to force the overflow because decent browsers allow the CSS property resize.this.editor.setStyle('height', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px');}// Disable odd inline CSS styles.this.disableCssStyling();// Use paragraphs not divs.if (document.queryCommandSupported('DefaultParagraphSeparator')) {document.execCommand('DefaultParagraphSeparator', false, 'p');}// Add the toolbar and editable zone to the page.this.textarea.get('parentNode').insert(this._wrapper, this.textarea).setAttribute('class', 'editor_atto_wrap');// Hide the old textarea.this.textarea.hide();// Set up custom event for editor updated.Y.mix(Y.Node.DOM_EVENTS, {'form:editorUpdated': true});this.textarea.on('form:editorUpdated', function() {this.updateEditorState();}, this);// Copy the text to the contenteditable div.this.updateFromTextArea();// Publish the events that are defined by this editor.this.publishEvents();// Add handling for saving and restoring selections on cursor/focus changes.this.setupSelectionWatchers();// Add polling to update the textarea periodically when typing long content.this.setupAutomaticPolling();// Setup plugins.this.setupPlugins();// Initialize the auto-save timer.this.setupAutosave();// Preload the icons for the notifications.this.setupNotifications();},/*** Focus on the editable area for this editor.** @method focus* @chainable*/focus: function() {this.editor.focus();return this;},/*** Publish events for this editor instance.** @method publishEvents* @private* @chainable*/publishEvents: function() {/*** Fired when changes are made within the editor.** @event change*/this.publish('change', {broadcast: true,preventable: true});/*** Fired when all plugins have completed loading.** @event pluginsloaded*/this.publish('pluginsloaded', {fireOnce: true});this.publish('atto:selectionchanged', {prefix: 'atto'});return this;},/*** Set up automated polling of the text area to update the textarea.** @method setupAutomaticPolling* @chainable*/setupAutomaticPolling: function() {this._registerEventHandle(this.editor.on(['keyup', 'cut'], this.updateOriginal, this));this._registerEventHandle(this.editor.on('paste', this.pasteCleanup, this));// Call this.updateOriginal after dropped content has been processed.this._registerEventHandle(this.editor.on('drop', this.updateOriginalDelayed, this));return this;},/*** Calls updateOriginal on a short timer to allow native event handlers to run first.** @method updateOriginalDelayed* @chainable*/updateOriginalDelayed: function() {Y.soon(Y.bind(this.updateOriginal, this));return this;},setupPlugins: function() {// Clear the list of plugins.this.plugins = {};var plugins = this.get('plugins');var groupIndex,group,pluginIndex,plugin,pluginConfig;for (groupIndex in plugins) {group = plugins[groupIndex];if (!group.plugins) {// No plugins in this group - skip it.continue;}for (pluginIndex in group.plugins) {plugin = group.plugins[pluginIndex];pluginConfig = Y.mix({name: plugin.name,group: group.group,editor: this.editor,toolbar: this.toolbar,host: this}, plugin);// Add a reference to the current editor.if (typeof Y.M['atto_' + plugin.name] === "undefined") {continue;}this.plugins[plugin.name] = new Y.M['atto_' + plugin.name].Button(pluginConfig);}}// Some plugins need to perform actions once all plugins have loaded.this.fire('pluginsloaded');return this;},enablePlugins: function(plugin) {this._setPluginState(true, plugin);},disablePlugins: function(plugin) {this._setPluginState(false, plugin);},_setPluginState: function(enable, plugin) {var target = 'disableButtons';if (enable) {target = 'enableButtons';}if (plugin) {this.plugins[plugin][target]();} else {Y.Object.each(this.plugins, function(currentPlugin) {currentPlugin[target]();}, this);}},/*** Update the state of the editor.*/updateEditorState: function() {var disabled = this.textarea.hasAttribute('readonly'),editorfield = Y.one('#' + this.get('elementid') + 'editable');// Enable/Disable all plugins.this._setPluginState(!disabled);// Enable/Disable content of editor.if (editorfield) {editorfield.setAttribute('contenteditable', !disabled);}},/*** Enable the empty placeholder for empty content.*/enablePlaceholderForEmptyContent: function() {this.enableAppropriateEmptyContent = true;},/*** Disable the empty placeholder for empty content.*/disablePlaceholderForEmptyContent: function() {this.enableAppropriateEmptyContent = false;},/*** Register an event handle for disposal in the destructor.** @method _registerEventHandle* @param {EventHandle} The Event Handle as returned by Y.on, and Y.delegate.* @private*/_registerEventHandle: function(handle) {this._eventHandles.push(handle);}}, {NS: 'editor_atto',ATTRS: {/*** The unique identifier for the form element representing the editor.** @attribute elementid* @type String* @writeOnce*/elementid: {value: null,writeOnce: true},/*** The contextid of the form.** @attribute contextid* @type Integer* @writeOnce*/contextid: {value: null,writeOnce: true},/*** Plugins with their configuration.** The plugins structure is:** [* {* "group": "groupName",* "plugins": [* "pluginName": {* "configKey": "configValue"* },* "pluginName": {* "configKey": "configValue"* }* ]* },* {* "group": "groupName",* "plugins": [* "pluginName": {* "configKey": "configValue"* }* ]* }* ]** @attribute plugins* @type Object* @writeOnce*/plugins: {value: {},writeOnce: true}}});// The Editor publishes custom events that can be subscribed to.Y.augment(Editor, Y.EventTarget);Y.namespace('M.editor_atto').Editor = Editor;// Function for Moodle's initialisation.Y.namespace('M.editor_atto.Editor').init = function(config) {return new Y.M.editor_atto.Editor(config);};// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** A notify function for the Atto editor.** @module moodle-editor_atto-notify* @submodule notify* @package editor_atto* @copyright 2014 Damyon Wiese* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/var LOGNAME_NOTIFY = 'moodle-editor_atto-editor-notify',NOTIFY_INFO = 'info',NOTIFY_WARNING = 'warning';function EditorNotify() {}EditorNotify.ATTRS = {};EditorNotify.prototype = {/*** A single Y.Node for this editor. There is only ever one - it is replaced if a new message comes in.** @property messageOverlay* @type {Node}*/messageOverlay: null,/*** A single timer object that can be used to cancel the hiding behaviour.** @property hideTimer* @type {timer}*/hideTimer: null,/*** Initialize the notifications.** @method setupNotifications* @chainable*/setupNotifications: function() {var preload1 = new Image(),preload2 = new Image();preload1.src = M.util.image_url('i/warning', 'moodle');preload2.src = M.util.image_url('i/info', 'moodle');return this;},/*** Show a notification in a floaty overlay somewhere in the atto editor text area.** @method showMessage* @param {String} message The translated message (use get_string)* @param {String} type Must be either "info" or "warning"* @param {Number} timeout Time in milliseconds to show this message for.* @chainable*/showMessage: function(message, type, timeout) {var messageTypeIcon = '',intTimeout,bodyContent;if (this.messageOverlay === null) {this.messageOverlay = Y.Node.create('<div class="editor_atto_notification"></div>');this.messageOverlay.hide(true);this.textarea.get('parentNode').append(this.messageOverlay);this.messageOverlay.on('click', function() {this.messageOverlay.hide(true);}, this);}if (this.hideTimer !== null) {this.hideTimer.cancel();}if (type === NOTIFY_WARNING) {messageTypeIcon = '<img src="' +M.util.image_url('i/warning', 'moodle') +'" alt="' + M.util.get_string('warning', 'moodle') + '"/>';} else if (type === NOTIFY_INFO) {messageTypeIcon = '<img src="' +M.util.image_url('i/info', 'moodle') +'" alt="' + M.util.get_string('info', 'moodle') + '"/>';} else {}// Parse the timeout value.intTimeout = parseInt(timeout, 10);if (intTimeout <= 0) {intTimeout = 60000;}// Convert class to atto_info (for example).type = 'atto_' + type;bodyContent = Y.Node.create('<div class="' + type + '" role="alert" aria-live="assertive">' +messageTypeIcon + ' ' +Y.Escape.html(message) +'</div>');this.messageOverlay.empty();this.messageOverlay.append(bodyContent);this.messageOverlay.show(true);this.hideTimer = Y.later(intTimeout, this, function() {this.hideTimer = null;if (this.messageOverlay.inDoc()) {this.messageOverlay.hide(true);}});return this;}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorNotify]);// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** @module moodle-editor_atto-editor* @submodule textarea*//*** Textarea functions for the Atto editor.** See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.** @namespace M.editor_atto* @class EditorTextArea*/function EditorTextArea() {}EditorTextArea.ATTRS = {};EditorTextArea.prototype = {/*** Return the appropriate empty content value for the current browser.** Different browsers use a different content when they are empty and* we must set this reliable across the board.** @method _getEmptyContent* @return String The content to use representing no user-provided content* @private*/_getEmptyContent: function() {if (!this.enableAppropriateEmptyContent) {// Return the empty string if we do not enable the empty placeholder. Ex: HTML mode.return '';}var alignment;if (this.coreDirection === 'rtl') {alignment = 'style="text-align: right;"';} else {alignment = 'style="text-align: left;"';}if (Y.UA.ie && Y.UA.ie < 10) {return '<p dir="' + this.coreDirection + '" ' + alignment + '></p>';} else {return '<p dir="' + this.coreDirection + '" ' + alignment + '><br></p>';}},/*** Copy and clean the text from the textarea into the contenteditable div.** If the text is empty, provide a default paragraph tag to hold the content.** @method updateFromTextArea* @chainable*/updateFromTextArea: function() {// Clear it first.this.editor.setHTML('');// Copy cleaned HTML to editable div.this.editor.append(this._cleanHTML(this.textarea.get('value'), true));// Insert a paragraph in the empty contenteditable div.if (this.editor.getHTML() === '') {this.editor.setHTML(this._getEmptyContent());}return this;},/*** Copy the text from the contenteditable to the textarea which it replaced.** @method updateOriginal* @chainable*/updateOriginal: function() {// Get the previous and current value to compare them.var oldValue = this.textarea.get('value'),newValue = this.getCleanHTML();if (newValue === "" && this.isActive()) {// The content was entirely empty so get the empty content placeholder.newValue = this._getEmptyContent();}// Only call this when there has been an actual change to reduce processing.if (oldValue !== newValue) {// Insert the cleaned content.this.textarea.set('value', newValue);// Trigger the onchange callback on the textarea, essentially to notify the formchangechecker module.this.textarea.simulate('change');// Trigger handlers for this action.this.fire('change');}return this;}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorTextArea]);// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./* eslint-disable no-unused-vars *//*** A autosave function for the Atto editor.** @module moodle-editor_atto-autosave* @submodule autosave-base* @package editor_atto* @copyright 2014 Damyon Wiese* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/var SUCCESS_MESSAGE_TIMEOUT = 5000,RECOVER_MESSAGE_TIMEOUT = 60000,LOGNAME_AUTOSAVE = 'moodle-editor_atto-editor-autosave';function EditorAutosave() {}EditorAutosave.ATTRS = {/*** Enable/Disable auto save for this instance.** @attribute autosaveEnabled* @type Boolean* @writeOnce*/autosaveEnabled: {value: true,writeOnce: true},/*** The time between autosaves (in seconds).** @attribute autosaveFrequency* @type Number* @default 60* @writeOnce*/autosaveFrequency: {value: 60,writeOnce: true},/*** Unique hash for this page instance. Calculated from $PAGE->url in php.** @attribute pageHash* @type String* @writeOnce*/pageHash: {value: '',writeOnce: true}};EditorAutosave.prototype = {/*** The text that was auto saved in the last request.** @property lastText* @type string*/lastText: "",/*** Autosave instance.** @property autosaveInstance* @type string*/autosaveInstance: null,/*** Autosave Timer.** @property autosaveTimer* @type object*/autosaveTimer: null,/*** Initialize the autosave process** @method setupAutosave* @chainable*/setupAutosave: function() {var draftid = -1,form,optiontype = null,options = this.get('filepickeroptions'),params;if (!this.get('autosaveEnabled')) {// Autosave disabled for this instance.return;}this.autosaveInstance = Y.stamp(this);for (optiontype in options) {if (typeof options[optiontype].itemid !== "undefined") {draftid = options[optiontype].itemid;}}// First see if there are any saved drafts.// Make an ajax request.params = {contextid: this.get('contextid'),action: 'resume',draftid: draftid,elementid: this.get('elementid'),pageinstance: this.autosaveInstance,pagehash: this.get('pageHash')};this.autosaveIo(params, this, {success: function(response) {if (response === null) {// This can happen when there is nothing to resume from.return;} else if (!response) {return;}// Revert untouched editor contents to an empty string.var emptyContents = [// For FF and Chrome.'<p></p>','<p><br></p>','<br>','<p dir="rtl" style="text-align: right;"></p>','<p dir="rtl" style="text-align: right;"><br></p>','<p dir="ltr" style="text-align: left;"></p>','<p dir="ltr" style="text-align: left;"><br></p>',// For IE 9 and 10.'<p> </p>','<p><br> </p>','<p dir="rtl" style="text-align: right;"> </p>','<p dir="rtl" style="text-align: right;"><br> </p>','<p dir="ltr" style="text-align: left;"> </p>','<p dir="ltr" style="text-align: left;"><br> </p>'];if (emptyContents.includes(response.result)) {response.result = '';}if (response.error || typeof response.result === 'undefined') {this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);} else if (response.result !== this.textarea.get('value') &&response.result !== '') {this.recoverText(response.result);}this._fireSelectionChanged();},failure: function() {this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);}});// Now setup the timer for periodic saves.var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000;this.autosaveTimer = Y.later(delay, this, this.saveDraft, false, true);// Now setup the listener for form submission.form = this.textarea.ancestor('form');if (form) {this.autosaveIoOnSubmit(form, {action: 'reset',contextid: this.get('contextid'),elementid: this.get('elementid'),pageinstance: this.autosaveInstance,pagehash: this.get('pageHash')});}return this;},/*** Recover a previous version of this text and show a message.** @method recoverText* @param {String} text* @chainable*/recoverText: function(text) {this.editor.setHTML(text);this.saveSelection();this.updateOriginal();this.lastText = text;this.showMessage(M.util.get_string('textrecovered', 'editor_atto'),NOTIFY_INFO, RECOVER_MESSAGE_TIMEOUT);// Fire an event that the editor content has changed.require(['core_editor/events'], function(editorEvents) {editorEvents.notifyEditorContentRestored(this.editor.getDOMNode());}.bind(this));return this;},/*** Save a single draft via ajax.** @method saveDraft* @chainable*/saveDraft: function() {var url, params;if (!this.editor.getDOMNode()) {// Stop autosaving if the editor was removed from the page.this.autosaveTimer.cancel();return;}// Only copy the text from the div to the textarea if the textarea is not currently visible.if (!this.editor.get('hidden')) {this.updateOriginal();}var newText = this.textarea.get('value');if (newText !== this.lastText) {// Make an ajax request.url = M.cfg.wwwroot + this.get('autosaveAjaxScript');params = {sesskey: M.cfg.sesskey,contextid: this.get('contextid'),action: 'save',drafttext: newText,elementid: this.get('elementid'),pagehash: this.get('pageHash'),pageinstance: this.autosaveInstance};// Reusable error handler - must be passed the correct context.var ajaxErrorFunction = function(response) {var errorDuration = parseInt(this.get('autosaveFrequency'), 10) * 1000;this.showMessage(M.util.get_string('autosavefailed', 'editor_atto'), NOTIFY_WARNING, errorDuration);};this.autosaveIo(params, this, {failure: ajaxErrorFunction,success: function(response) {if (response && response.error) {Y.soon(Y.bind(ajaxErrorFunction, this, [response]));} else {// All working.this.lastText = newText;this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);}}});}return this;}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosave]);// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** A autosave function for the Atto editor.** @module moodle-editor_atto-autosave-io* @submodule autosave-io* @package editor_atto* @copyright 2016 Frédéric Massart* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/var EditorAutosaveIoDispatcherInstance = null;function EditorAutosaveIoDispatcher() {EditorAutosaveIoDispatcher.superclass.constructor.apply(this, arguments);this._submitEvents = {};this._queue = [];this._throttle = null;}EditorAutosaveIoDispatcher.NAME = 'EditorAutosaveIoDispatcher';EditorAutosaveIoDispatcher.ATTRS = {/*** The relative path to the ajax script.** @attribute autosaveAjaxScript* @type String* @default '/lib/editor/atto/autosave-ajax.php'* @readOnly*/autosaveAjaxScript: {value: '/lib/editor/atto/autosave-ajax.php',readOnly: true},/*** The time buffer for the throttled requested.** @attribute delay* @type Number* @default 50* @readOnly*/delay: {value: 50,readOnly: true}};Y.extend(EditorAutosaveIoDispatcher, Y.Base, {/*** Dispatch an IO request.** This method will put the requests in a queue in order to attempt to bulk them.** @param {Object} params The parameters of the request.* @param {Object} context The context in which the callbacks are called.* @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as* optional keys defining the callbacks to call. Success and Complete* functions will receive the response as parameter. Success and Complete* may receive an object containing the error key, use this to confirm* that no errors occured.* @return {Void}*/dispatch: function(params, context, callbacks) {if (this._throttle) {this._throttle.cancel();}this._throttle = Y.later(this.get('delay'), this, this._processDispatchQueue);this._queue.push([params, context, callbacks]);},/*** Dispatches the requests in the queue.** @return {Void}*/_processDispatchQueue: function() {var queue = this._queue,data = {};this._queue = [];if (queue.length < 1) {return;}Y.Array.each(queue, function(item, index) {data[index] = item[0];});Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {method: 'POST',data: Y.QueryString.stringify({actions: data,sesskey: M.cfg.sesskey}),on: {start: this._makeIoEventCallback('start', queue),complete: this._makeIoEventCallback('complete', queue),failure: this._makeIoEventCallback('failure', queue),end: this._makeIoEventCallback('end', queue),success: this._makeIoEventCallback('success', queue)}});},/*** Creates a function that dispatches an IO response to callbacks.** @param {String} event The type of event.* @param {Array} queue The queue.* @return {Function}*/_makeIoEventCallback: function(event, queue) {var noop = function() {};return function() {var response = arguments[1],parsed = {};if ((event == 'complete' || event == 'success') && (typeof response !== 'undefined'&& typeof response.responseText !== 'undefined' && response.responseText !== '')) {// Success and complete events need to parse the response.parsed = JSON.parse(response.responseText) || {};}Y.Array.each(queue, function(item, index) {var context = item[1],cb = (item[2] && item[2][event]) || noop,arg;if (parsed && parsed.error) {// The response is an error, we send it to everyone.arg = parsed;} else if (parsed) {// The response was parsed, we only communicate the relevant portion of the response.arg = parsed[index];}cb.apply(context, [arg]);});};},/*** Form submit handler.** @param {EventFacade} e The event.* @return {Void}*/_onSubmit: function(e) {var data = {},id = e.currentTarget.generateID(),params = this._submitEvents[id];if (!params || params.ios.length < 1) {return;}Y.Array.each(params.ios, function(param, index) {data[index] = param;});Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {method: 'POST',data: Y.QueryString.stringify({actions: data,sesskey: M.cfg.sesskey}),sync: true});},/*** Registers a request to be made on form submission.** @param {Node} node The forum node we will listen to.* @param {Object} params Parameters for the IO request.* @return {Void}*/whenSubmit: function(node, params) {if (typeof this._submitEvents[node.generateID()] === 'undefined') {this._submitEvents[node.generateID()] = {event: node.on('submit', this._onSubmit, this),ajaxEvent: node.on(M.core.event.FORM_SUBMIT_AJAX, this._onSubmit, this),ios: []};}this._submitEvents[node.get('id')].ios.push([params]);}});EditorAutosaveIoDispatcherInstance = new EditorAutosaveIoDispatcher();function EditorAutosaveIo() {}EditorAutosaveIo.prototype = {/*** Dispatch an IO request.** This method will put the requests in a queue in order to attempt to bulk them.** @param {Object} params The parameters of the request.* @param {Object} context The context in which the callbacks are called.* @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as* optional keys defining the callbacks to call. Success and Complete* functions will receive the response as parameter. Success and Complete* may receive an object containing the error key, use this to confirm* that no errors occured.* @return {Void}*/autosaveIo: function(params, context, callbacks) {EditorAutosaveIoDispatcherInstance.dispatch(params, context, callbacks);},/*** Registers a request to be made on form submission.** @param {Node} form The forum node we will listen to.* @param {Object} params Parameters for the IO request.* @return {Void}*/autosaveIoOnSubmit: function(form, params) {EditorAutosaveIoDispatcherInstance.whenSubmit(form, params);}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosaveIo]);// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** @module moodle-editor_atto-editor* @submodule clean*//*** Functions for the Atto editor to clean the generated content.** See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.** @namespace M.editor_atto* @class EditorClean*/function EditorClean() {}EditorClean.ATTRS = {};EditorClean.prototype = {/*** Clean the generated HTML content without modifying the editor content.** This includes removes all YUI ids from the generated content.** @return {string} The cleaned HTML content.*/getCleanHTML: function() {// Clone the editor so that we don't actually modify the real content.var editorClone = this.editor.cloneNode(true),html;// Remove all YUI IDs.Y.each(editorClone.all('[id^="yui"]'), function(node) {node.removeAttribute('id');});editorClone.all('.atto_control').remove(true);html = editorClone.get('innerHTML');// Revert untouched editor contents to an empty string.var emptyContents = [// For FF and Chrome.'<p></p>','<p><br></p>','<br>','<p dir="rtl" style="text-align: right;"></p>','<p dir="rtl" style="text-align: right;"><br></p>','<p dir="ltr" style="text-align: left;"></p>','<p dir="ltr" style="text-align: left;"><br></p>',// For IE 9 and 10.'<p> </p>','<p><br> </p>','<p dir="rtl" style="text-align: right;"> </p>','<p dir="rtl" style="text-align: right;"><br> </p>','<p dir="ltr" style="text-align: left;"> </p>','<p dir="ltr" style="text-align: left;"><br> </p>'];if (emptyContents.includes(html)) {return '';}// Remove any and all nasties from source.return this._cleanHTML(html);},/*** Clean the HTML content of the editor.** @method cleanEditorHTML* @chainable*/cleanEditorHTML: function() {var startValue = this.editor.get('innerHTML');this.editor.set('innerHTML', this._cleanHTML(startValue));return this;},/*** Clean the specified HTML content and remove any content which could cause issues.** @method _cleanHTML* @private* @param {String} content The content to clean* @param {Boolean} deepClean If true, do a more in depth (and resource intensive) cleaning of the HTML.* @return {String} The cleaned HTML*/_cleanHTML: function(content, deepClean) {// Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc.var rules = [// Remove any style blocks. Some browsers do not work well with them in a contenteditable.// Plus style blocks are not allowed in body html, except with "scoped", which most browsers don't support as of 2015.// Reference: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work"{regex: /<style[^>]*>[\s\S]*?<\/style>/gi, replace: ""},// Remove any open HTML comment opens that are not followed by a close. This can completely break page layout.{regex: /<!--(?![\s\S]*?-->)/gi, replace: ""},// Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"// Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link.{regex: /<\/?(?:title|meta|style|st\d|head\b|font|html|body|link)[^>]*?>/gi, replace: ""}];content = this._filterContentWithRules(content, rules);if (deepClean) {content = this._cleanHTMLLists(content);}return content;},/*** Take the supplied content and run on the supplied regex rules.** @method _filterContentWithRules* @private* @param {String} content The content to clean* @param {Array} rules An array of structures: [ {regex: /something/, replace: "something"}, {...}, ...]* @return {String} The cleaned content*/_filterContentWithRules: function(content, rules) {var i = 0;for (i = 0; i < rules.length; i++) {content = content.replace(rules[i].regex, rules[i].replace);}return content;},/*** Intercept and clean html paste events.** @method pasteCleanup* @param {Object} sourceEvent The YUI EventFacade object* @return {Boolean} True if the passed event should continue, false if not.*/pasteCleanup: function(sourceEvent) {// We only expect paste events, but we will check anyways.if (sourceEvent.type === 'paste') {// Register the delayed paste cleanup. We will cancel it if we register the fallback cleanup.var delayedCleanup = this.postPasteCleanupDelayed();// The YUI event wrapper doesn't provide paste event info, so we need the underlying event.var event = sourceEvent._event;// Check if we have a valid clipboardData object in the event.// IE has a clipboard object at window.clipboardData, but as of IE 11, it does not provide HTML content access.if (event && event.clipboardData && event.clipboardData.getData && event.clipboardData.types) {// Check if there is HTML type to be pasted, if we can get it, we want to scrub before insert.var types = event.clipboardData.types;var isHTML = false;// Different browsers use different containers to hold the types, so test various functions.if (typeof types.contains === 'function') {isHTML = types.contains('text/html');} else if (typeof types.indexOf === 'function') {isHTML = (types.indexOf('text/html') > -1);}var content;if (isHTML) {// Get the clipboard content.try {content = event.clipboardData.getData('text/html');} catch (error) {// Something went wrong. Fallback.delayedCleanup.cancel();this.fallbackPasteCleanupDelayed();return true;}// Stop the original paste.sourceEvent.preventDefault();// Scrub the paste content.content = this._cleanPasteHTML(content);// Insert the content.this.insertContentAtFocusPoint(content);// Update the text area.this.updateOriginal();return false;} else {try {// Plaintext clipboard content can only be retrieved this way.content = event.clipboardData.getData('text');} catch (error) {// Something went wrong. Fallback.// Due to poor cross browser clipboard compatibility, the failure to find html doesn't mean it isn't there.// Wait for the clipboard event to finish then fallback clean the entire editor.delayedCleanup.cancel();this.fallbackPasteCleanupDelayed();return true;}}} else {// If we reached a here, this probably means the browser has limited (or no) clipboard support.// Wait for the clipboard event to finish then fallback clean the entire editor.this.fallbackPasteCleanupDelayed();return true;}}// We should never get here - we must have received a non-paste event for some reason.// Um, just call updateOriginalDelayed() - it's safe.this.updateOriginalDelayed();return true;},/*** Calls postPasteCleanup on a short timer to allow the paste event handlers to complete, then deep clean the content.** @method postPasteCleanupDelayed* @return {object}* @chainable*/postPasteCleanupDelayed: function() {Y.soon(Y.bind(this.postPasteCleanup, this));return this;},/*** Do additional cleanup after the paste is complete.** @method postPasteCleanup* @return {object}* @chainable*/postPasteCleanup: function() {// Save the current selection (cursor position).var selection = window.rangy.saveSelection();// Get, clean, and replace the content in the editable.var content = this.editor.get('innerHTML');this.editor.set('innerHTML', this._cleanHTML(content, true));// Update the textarea.this.updateOriginal();// Restore the selection (cursor position).window.rangy.restoreSelection(selection);return this;},/*** Cleanup code after a paste event if we couldn't intercept the paste content.** @method fallbackPasteCleanup* @return {object}* @chainable*/fallbackPasteCleanup: function() {// Save the current selection (cursor position).var selection = window.rangy.saveSelection();// Get, clean, and replace the content in the editable.var content = this.editor.get('innerHTML');this.editor.set('innerHTML', this._cleanHTML(this._cleanPasteHTML(content), true));// Update the textarea.this.updateOriginal();// Restore the selection (cursor position).window.rangy.restoreSelection(selection);return this;},/*** Calls fallbackPasteCleanup on a short timer to allow the paste event handlers to complete.** @method fallbackPasteCleanupDelayed* @chainable*/fallbackPasteCleanupDelayed: function() {Y.soon(Y.bind(this.fallbackPasteCleanup, this));return this;},/*** Cleanup html that comes from WYSIWYG paste events. These are more likely to contain messy code that we should strip.** @method _cleanPasteHTML* @private* @param {String} content The html content to clean* @return {String} The cleaned HTML*/_cleanPasteHTML: function(content) {// Return an empty string if passed an invalid or empty object.if (!content || content.length === 0) {return "";}// Rules that get rid of the real-nasties and don't care about normalize code (correct quotes, white spaces, etc).var rules = [// Stuff that is specifically from MS Word and similar office packages.// Remove all garbage after closing html tag.{regex: /<\s*\/html\s*>([\s\S]+)$/gi, replace: ""},// Remove if comment blocks.{regex: /<!--\[if[\s\S]*?endif\]-->/gi, replace: ""},// Remove start and end fragment comment blocks.{regex: /<!--(Start|End)Fragment-->/gi, replace: ""},// Remove any xml blocks.{regex: /<xml[^>]*>[\s\S]*?<\/xml>/gi, replace: ""},// Remove any <?xml><\?xml> blocks.{regex: /<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi, replace: ""},// Remove <o:blah>, <\o:blah>.{regex: /<\/?\w+:[^>]*>/gi, replace: ""}];// Apply the first set of harsher rules.content = this._filterContentWithRules(content, rules);// Apply the standard rules, which mainly cleans things like headers, links, and style blocks.content = this._cleanHTML(content);// Check if the string is empty or only contains whitespace.if (content.length === 0 || !content.match(/\S/)) {return content;}// Now we let the browser normalize the code by loading it into the DOM and then get the html back.// This gives us well quoted, well formatted code to continue our work on. Word may provide very poorly formatted code.var holder = document.createElement('div');holder.innerHTML = content;content = holder.innerHTML;// Free up the DOM memory.holder.innerHTML = "";// Run some more rules that care about quotes and whitespace.rules = [// Get all class attributes so we can work on them.{regex: /(<[^>]*?class\s*?=\s*?")([^>"]*)(")/gi, replace: function(match, group1, group2, group3) {// Remove MSO classes.group2 = group2.replace(/(?:^|[\s])[\s]*MSO[_a-zA-Z0-9\-]*/gi, "");// Remove Apple- classes.group2 = group2.replace(/(?:^|[\s])[\s]*Apple-[_a-zA-Z0-9\-]*/gi, "");return group1 + group2 + group3;}},// Remove OLE_LINK# anchors that may litter the code.{regex: /<a [^>]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi, replace: ""}];// Clean all style attributes from the text.content = this._cleanStyles(content);// Apply the rules.content = this._filterContentWithRules(content, rules);// Reapply the standard cleaner to the content.content = this._cleanHTML(content);// Clean unused spans out of the content.content = this._cleanSpans(content);return content;},/*** Clean all inline styles from pasted text.** This code intentionally doesn't use YUI Nodes. YUI was quite a bit slower at this, so using raw DOM objects instead.** @method _cleanStyles* @private* @param {String} content The content to clean* @return {String} The cleaned HTML*/_cleanStyles: function(content) {var holder = document.createElement('div');holder.innerHTML = content;var elementsWithStyle = holder.querySelectorAll('[style]');var i = 0;for (i = 0; i < elementsWithStyle.length; i++) {elementsWithStyle[i].removeAttribute('style');}var elementsWithClass = holder.querySelectorAll('[class]');for (i = 0; i < elementsWithClass.length; i++) {elementsWithClass[i].removeAttribute('class');}return holder.innerHTML;},/*** Clean empty or un-unused spans from passed HTML.** This code intentionally doesn't use YUI Nodes. YUI was quite a bit slower at this, so using raw DOM objects instead.** @method _cleanSpans* @private* @param {String} content The content to clean* @return {String} The cleaned HTML*/_cleanSpans: function(content) {// Return an empty string if passed an invalid or empty object.if (!content || content.length === 0) {return "";}// Check if the string is empty or only contains whitespace.if (content.length === 0 || !content.match(/\S/)) {return content;}var rules = [// Remove unused class, style, or id attributes. This will make empty tag detection easier later.{regex: /(<[^>]*?)(?:[\s]*(?:class|style|id)\s*?=\s*?"\s*?")+/gi, replace: "$1"}];// Apply the rules.content = this._filterContentWithRules(content, rules);// Reference: "http://stackoverflow.com/questions/8131396/remove-nested-span-without-id"// This is better to run detached from the DOM, so the browser doesn't try to update on each change.var holder = document.createElement('div');holder.innerHTML = content;var spans = holder.getElementsByTagName('span');// Since we will be removing elements from the list, we should copy it to an array, making it static.var spansarr = Array.prototype.slice.call(spans, 0);spansarr.forEach(function(span) {if (!span.hasAttributes()) {// If no attributes (id, class, style, etc), this span is has no effect.// Move each child (if they exist) to the parent in place of this span.while (span.firstChild) {span.parentNode.insertBefore(span.firstChild, span);}// Remove the now empty span.span.parentNode.removeChild(span);}});return holder.innerHTML;},/*** This is a function that searches for, and attempts to correct certain issues with ul/ol html lists.* This is needed because these lists are used heavily in page layout, and content with bad tags can* lead to broke course pages.** The theory of operation here is to linearly process the incoming content, counting the opening and closing* of list tags, and determining when there is a mismatch.** The specific issues this should be able to correct are:* - Orphaned li elements will be wrapped in a set of ul tags.* - li elements inside li elements.* - An extra closing ul, or ol tag will be discarded.* - An extra closing li tag will have an opening tag added if appropriate, or will be discarded.* - If there is an unmatched list open tag, a matching close tag will be inserted.** It does it's best to match the case of corrected tags. Even though not required by html spec,* it seems like the safer route.** A note on parent elements of li. This code assumes that li must have a ol or ul parent.* There are two other potential other parents of li. They are menu and dir. The dir tag was deprecated in* HTML4, and removed in HTML5. The menu tag is experimental as of this writing, and basically doesn't work* in any browsers, even Firefox, which theoretically has limited support for it. If other parents of li* become viable, they will need to be added to this code.** @method _cleanHTMLLists* @private* @param {String} content The content to clean* @return {String} The cleaned content*/_cleanHTMLLists: function(content) {var output = '',toProcess = content,match = null,openTags = [],currentTag = null,previousTag = null;// Use a regular expression to find the next open or close li, ul, or ol tag.// Keep going until there are no more matching tags left.// This expression looks for whole words by employing the word boundary (\b) metacharacter.while ((match = toProcess.match(/<(\/?)(li|ul|ol)\b[^>]*>/i))) {currentTag = {tag: match[2],tagLowerCase: match[2].toLowerCase(),fullTag: match[0],isOpen: (match[1].length == 1) ? false : true};// Get the most recent open tag.previousTag = (openTags.length) ? openTags[openTags.length - 1] : null;// Slice up the content based on the match and add content before the match to output.output += toProcess.slice(0, match.index);toProcess = toProcess.slice(match.index + match[0].length);// Now the full content is in output + currentTag.fullTag + toProcess. When making fixes, it is best to push the fix and// fullTag back onto the front or toProcess, then restart the loop. This allows processing to follow the normal path// most often. But sometimes we will need to modify output to insert or remove tags in the already complete code.if (currentTag.isOpen) {// We are at the opening phase of a tag.// We have to do special processing for list items, as they can only be children of ul and ol tags.if (currentTag.tagLowerCase === 'li') {if (!previousTag) {// This means we have are opening a li, but aren't in a list. This is not allowed!// We are going to check for the count of open and close ol tags ahead to decide what to do.var closeCount = (toProcess.match(/<\/(ol)[ >]/ig) || []).length;var openCount = (toProcess.match(/<(ol)[ >]/ig) || []).length;if (closeCount > openCount) {// There are more close ol's ahead than opens ahead. So open the ol and try again.toProcess = '<ol>' + currentTag.fullTag + toProcess;continue;}// For the other cases, just open a ul and try again. Later the closing ul will get matched if it exists,// or if it doesn't one will automatically get inserted.toProcess = '<ul>' + currentTag.fullTag + toProcess;continue;}if (previousTag.tagLowerCase === 'li') {// You aren't allowed to nest li tags. Close the current one before starting the new one.toProcess = '</' + previousTag.tag + '>' + currentTag.fullTag + toProcess;continue;}// Previous tag must be a list at this point, so we can continue.}// If we made it this far, record the tag to the open tags list.openTags.push({tag: currentTag.tag,tagLowerCase: currentTag.tagLowerCase,position: output.length,length: currentTag.fullTag.length});} else {// We are processing a closing tag.if (openTags.length == 0) {// We are closing a tag that isn't open. That's a problem. Just discarding should be safe.continue;}if (previousTag.tagLowerCase === currentTag.tagLowerCase) {// Closing a tag that matches the open tag. This is the nominal case. Pop it off, and update previousTag.if (currentTag.tag != previousTag.tag) {// This would mean cases don't match between the opening and closing tag.// We are going to swap them to match, even though not required.currentTag.fullTag = currentTag.fullTag.replace(currentTag.tag, previousTag.tag);}openTags.pop();previousTag = (openTags.length) ? openTags[openTags.length - 1] : null;} else {// We are closing a tag that isn't the most recent open one open, so we have a mismatch.if (currentTag.tagLowerCase === 'li' && previousTag.liEnd && (previousTag.liEnd < output.length)) {// We are closing an unopened li, but the parent list has complete li tags more than 0 chars ago.// Assume we are missing an open li at the end of the previous li, and insert there.output = this._insertString(output, '<' + currentTag.tag + '>', previousTag.liEnd);} else if (currentTag.tagLowerCase === 'li' && !previousTag.liEnd &&((previousTag.position + previousTag.length) < output.length)) {// We are closing an unopened li, and the parent has no previous lis in it, but opened more than 0// chars ago. Assume we are missing a starting li, and insert it right after the list opened.output = this._insertString(output, '<' + currentTag.tag + '>', previousTag.position + previousTag.length);} else if (previousTag.tagLowerCase === 'li') {// We must be trying to close a ul/ol while in a li. Just assume we are missing a closing li.toProcess = '</' + previousTag.tag + '>' + currentTag.fullTag + toProcess;continue;} else {// Here we must be trying to close a tag that isn't open, or is open higher up. Just discard.// If there ends up being a missing close tag later on, that will get fixed separately.continue;}}// If we have a valid closing li tag, and a list, record where the li ended.if (currentTag.tagLowerCase === 'li' && previousTag) {previousTag.liEnd = output.length + currentTag.fullTag.length;}}// Now we can add the tag to the output.output += currentTag.fullTag;}// Add anything left in toProcess to the output.output += toProcess;// Anything still in the openTags list are extra and need to be dealt with.if (openTags.length) {// Work on the list in reverse order so positions stay correct.while ((currentTag = openTags.pop())) {if (currentTag.liEnd) {// We have a position for the last list item in this element. Insert the closing it after that.output = this._insertString(output, '</' + currentTag.tag + '>', currentTag.liEnd);} else {// If there weren't any children list items, then we should just remove the tag where it started.// This will also remote an open li tag that runs to the end of the content, since it has no children lis.output = output.slice(0, currentTag.position) + output.slice(currentTag.position + currentTag.length);}}}return output;},/*** Insert a string in the middle of an existing string at the specified location.** @method _insertString* @param {String} content The subject of the insertion.* @param {String} insert The string that will be inserted.* @param {Number} position The location to make the insertion.* @return {String} The string with the new content inserted.*/_insertString: function(content, insert, position) {return content.slice(0, position) + insert + content.slice(position);}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorClean]);// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** @module moodle-editor_atto-editor* @submodule commands*//*** Selection functions for the Atto editor.** See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.** @namespace M.editor_atto* @class EditorCommand*/function EditorCommand() {}EditorCommand.ATTRS = {};EditorCommand.prototype = {/*** Applies a callback method to editor if selection is uncollapsed or waits for input to select first.* @method applyFormat* @param e EventTarget Event to be passed to callback if selection is uncollapsed* @param method callback A callback method which changes editor when text is selected.* @param object context Context to be used for callback method* @param array args Array of arguments to pass to callback*/applyFormat: function(e, callback, context, args) {function handleInsert(e, callback, context, args, anchorNode, anchorOffset) {// After something is inputed, select it and apply the formating function.Y.soon(Y.bind(function(e, callback, context, args, anchorNode, anchorOffset) {var selection = window.rangy.getSelection();// Set the start of the selection to where it was when the method was first called.var range = selection.getRangeAt(0);range.setStart(anchorNode, anchorOffset);selection.setSingleRange(range);// Now apply callback to the new text that is selected.callback.apply(context, [e, args]);// Collapse selection so cursor is at end of inserted material.selection.collapseToEnd();// Save save selection and editor contents.this.saveSelection();this.updateOriginal();}, this, e, callback, context, args, anchorNode, anchorOffset));}// Set default context for the method.context = context || this;// Check whether range is collapsed.var selection = window.rangy.getSelection();if (selection.isCollapsed) {// Selection is collapsed so listen for input into editor.var handle = this.editor.once('input', handleInsert, this, callback, context, args,selection.anchorNode, selection.anchorOffset);// Cancel if selection changes before input.this.editor.onceAfter(['click', 'selectstart'], handle.detach, handle);return;}// The range is not collapsed; so apply callback method immediately.callback.apply(context, [e, args]);// Save save selection and editor contents.this.saveSelection();this.updateOriginal();},/*** Replaces all the tags in a node list with new type.* @method replaceTags* @param NodeList nodelist* @param String tag*/replaceTags: function(nodelist, tag) {// We mark elements in the node list for iterations.nodelist.setAttribute('data-iterate', true);var node = this.editor.one('[data-iterate="true"]');while (node) {var clone = Y.Node.create('<' + tag + ' />').setAttrs(node.getAttrs()).removeAttribute('data-iterate');// Copy class and style if not blank.if (node.getAttribute('style')) {clone.setAttribute('style', node.getAttribute('style'));}if (node.getAttribute('class')) {clone.setAttribute('class', node.getAttribute('class'));}// We use childNodes here because we are interested in both type 1 and 3 child nodes.var children = node.getDOMNode().childNodes;var child;child = children[0];while (typeof child !== "undefined") {clone.append(child);child = children[0];}node.replace(clone);node = this.editor.one('[data-iterate="true"]');}},/*** Change all tags with given type to a span with CSS class attribute.* @method changeToCSS* @param String tag Tag type to be changed to span* @param String markerClass CSS class that corresponds to desired tag*/changeToCSS: function(tag, markerClass) {// Save the selection.var selection = window.rangy.saveSelection();// Remove display:none from rangy markers so browser doesn't delete them.this.editor.all('.rangySelectionBoundary').setStyle('display', null);// Replace tags with CSS classes.this.editor.all(tag).addClass(markerClass);this.replaceTags(this.editor.all('.' + markerClass), 'span');// Restore selection and toggle class.window.rangy.restoreSelection(selection);},/*** Change spans with CSS classes in editor into elements with given tag.* @method changeToCSS* @param String markerClass CSS class that corresponds to desired tag* @param String tag New tag type to be created*/changeToTags: function(markerClass, tag) {// Save the selection.var selection = window.rangy.saveSelection();// Remove display:none from rangy markers so browser doesn't delete them.this.editor.all('.rangySelectionBoundary').setStyle('display', null);// Replace spans with given tag.this.replaceTags(this.editor.all('span[class="' + markerClass + '"]'), tag);this.editor.all(tag + '[class="' + markerClass + '"]').removeAttribute('class');this.editor.all('.' + markerClass).each(function(n) {n.wrap('<' + tag + '/>');n.removeClass(markerClass);});// Remove CSS classes.this.editor.all('[class="' + markerClass + '"]').removeAttribute('class');this.editor.all(tag).removeClass(markerClass);// Restore selection.window.rangy.restoreSelection(selection);}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorCommand]);// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** @module moodle-editor_atto-editor* @submodule toolbar*//*** Toolbar functions for the Atto editor.** See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.** @namespace M.editor_atto* @class EditorToolbar*/function EditorToolbar() {}EditorToolbar.ATTRS = {};EditorToolbar.prototype = {/*** A reference to the toolbar Node.** @property toolbar* @type Node*/toolbar: null,/*** A reference to any currently open menus in the toolbar.** @property openMenus* @type Array*/openMenus: null,/*** Setup the toolbar on the editor.** @method setupToolbar* @chainable*/setupToolbar: function() {this.toolbar = Y.Node.create('<div class="' + CSS.TOOLBAR + '" role="toolbar" aria-live="off"/>');this.openMenus = [];this._wrapper.appendChild(this.toolbar);if (this.textareaLabel) {this.toolbar.setAttribute('aria-labelledby', this.textareaLabel.get("id"));}// Add keyboard navigation for the toolbar.this.setupToolbarNavigation();return this;}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorToolbar]);// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** @module moodle-editor_atto-editor* @submodule toolbarnav*//*** Toolbar Navigation functions for the Atto editor.** See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.** @namespace M.editor_atto* @class EditorToolbarNav*/function EditorToolbarNav() {}EditorToolbarNav.ATTRS = {};EditorToolbarNav.prototype = {/*** The current focal point for tabbing.** @property _tabFocus* @type Node* @default null* @private*/_tabFocus: null,/*** Set up the watchers for toolbar navigation.** @method setupToolbarNavigation* @chainable*/setupToolbarNavigation: function() {// Listen for Arrow left and Arrow right keys.this._wrapper.delegate('key',this.toolbarKeyboardNavigation,'down:37,39','.' + CSS.TOOLBAR,this);this._wrapper.delegate('focus',function(e) {this._setTabFocus(e.currentTarget);}, '.' + CSS.TOOLBAR + ' button', this);return this;},/*** Implement arrow key navigation for the buttons in the toolbar.** @method toolbarKeyboardNavigation* @param {EventFacade} e - the keyboard event.*/toolbarKeyboardNavigation: function(e) {// Prevent the default browser behaviour.e.preventDefault();// On cursor moves we loops through the buttons.var buttons = this.toolbar.all('button'),direction = 1,button,current = e.target.ancestor('button', true),innerButtons = e.target.all('button');// If we are not on a button and the element we are on contains some buttons, then move between the inner buttons.if (!current && innerButtons) {buttons = innerButtons;}if (e.keyCode === 37) {// Moving left so reverse the direction.direction = -1;}button = this._findFirstFocusable(buttons, current, direction);if (button) {button.focus();this._setTabFocus(button);} else {}},/*** Find the first focusable button.** @param {NodeList} buttons A list of nodes.* @param {Node} startAt The node in the list to start the search from.* @param {Number} direction The direction in which to search (1 or -1).* @return {Node | Undefined} The Node or undefined.* @method _findFirstFocusable* @private*/_findFirstFocusable: function(buttons, startAt, direction) {var checkCount = 0,candidate,button,index;// Determine which button to start the search from.index = buttons.indexOf(startAt);if (index < -1) {index = 0;}// Try to find the next.while (checkCount < buttons.size()) {index += direction;if (index < 0) {index = buttons.size() - 1;} else if (index >= buttons.size()) {// Handle wrapping.index = 0;}candidate = buttons.item(index);// Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item.checkCount++;// Loop while:// * we haven't checked every button;// * the button is hidden or disabled;// * the button is inside a hidden wrapper element.if (candidate.hasAttribute('hidden') || candidate.hasAttribute('disabled') || candidate.ancestor('[hidden]')) {continue;}button = candidate;break;}return button;},/*** Check the tab focus.** When we disable or hide a button, we should call this method to ensure that the* focus is not currently set on an inaccessible button, otherwise tabbing to the toolbar* would be impossible.** @method checkTabFocus* @chainable*/checkTabFocus: function() {if (this._tabFocus) {if (this._tabFocus.hasAttribute('disabled') || this._tabFocus.hasAttribute('hidden')|| this._tabFocus.ancestor('.atto_group').hasAttribute('hidden')) {// Find first available button.var button = this._findFirstFocusable(this.toolbar.all('button'), this._tabFocus, -1);if (button) {if (this._tabFocus.compareTo(document.activeElement)) {// We should also move the focus, because the inaccessible button also has the focus.button.focus();}this._setTabFocus(button);}}}return this;},/*** Sets tab focus for the toolbar to the specified Node.** @method _setTabFocus* @param {Node} button The node that focus should now be set to* @chainable* @private*/_setTabFocus: function(button) {if (this._tabFocus) {// Unset the previous entry.this._tabFocus.setAttribute('tabindex', '-1');}// Set up the new entry.this._tabFocus = button;this._tabFocus.setAttribute('tabindex', 0);// And update the activedescendant to point at the currently selected button.this.toolbar.setAttribute('aria-activedescendant', this._tabFocus.generateID());return this;}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorToolbarNav]);// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** @module moodle-editor_atto-editor* @submodule selection*//*** Selection functions for the Atto editor.** See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.** @namespace M.editor_atto* @class EditorSelection*/function EditorSelection() {}EditorSelection.ATTRS = {};EditorSelection.prototype = {/*** List of saved selections per editor instance.** @property _selections* @private*/_selections: null,/*** A unique identifier for the last selection recorded.** @property _lastSelection* @param lastselection* @type string* @private*/_lastSelection: null,/*** Whether focus came from a click event.** This is used to determine whether to restore the selection or not.** @property _focusFromClick* @type Boolean* @default false* @private*/_focusFromClick: false,/*** Whether if the last gesturemovestart event target was contained in this editor or not.** @property _gesturestartededitor* @type Boolean* @default false* @private*/_gesturestartededitor: false,/*** Set up the watchers for selection save and restoration.** @method setupSelectionWatchers* @chainable*/setupSelectionWatchers: function() {// Save the selection when a change was made.this.on('atto:selectionchanged', this.saveSelection, this);this.editor.on('focus', this.restoreSelection, this);// Do not restore selection when focus is from a click event.this.editor.on('mousedown', function() {this._focusFromClick = true;}, this);// Copy the current value back to the textarea when focus leaves us and save the current selection.this.editor.on('blur', function() {// Clear the _focusFromClick value.this._focusFromClick = false;// Update the original text area.this.updateOriginal();}, this);this.editor.on(['keyup', 'focus'], function(e) {Y.soon(Y.bind(this._hasSelectionChanged, this, e));}, this);Y.one(document.body).on('gesturemovestart', function(e) {if (this._wrapper.contains(e.target._node)) {this._gesturestartededitor = true;} else {this._gesturestartededitor = false;}}, null, this);Y.one(document.body).on('gesturemoveend', function(e) {if (!this._gesturestartededitor) {// Ignore the event if movestart target was not contained in the editor.return;}Y.soon(Y.bind(this._hasSelectionChanged, this, e));}, {// Standalone will make sure all editors receive the end event.standAlone: true}, this);return this;},/*** Work out if the cursor is in the editable area for this editor instance.** @method isActive* @return {boolean}*/isActive: function() {var range = rangy.createRange(),selection = rangy.getSelection();if (!selection.rangeCount) {// If there was no range count, then there is no selection.return false;}// We can't be active if the editor doesn't have focus at the moment.if (!document.activeElement ||!(this.editor.compareTo(document.activeElement) || this.editor.contains(document.activeElement))) {return false;}// Check whether the range intersects the editor selection.range.selectNode(this.editor.getDOMNode());return range.intersectsRange(selection.getRangeAt(0));},/*** Create a cross browser selection object that represents a YUI node.** @method getSelectionFromNode* @param {Node} YUI Node to base the selection upon.* @return {[rangy.Range]}*/getSelectionFromNode: function(node) {var range = rangy.createRange();range.selectNode(node.getDOMNode());return [range];},/*** Save the current selection to an internal property.** This allows more reliable return focus, helping improve keyboard navigation.** Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/restoreSelection"}}{{/crossLink}}.** @method saveSelection*/saveSelection: function() {if (this.isActive()) {this._selections = this.getSelection();}},/*** Restore any stored selection when the editor gets focus again.** Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/saveSelection"}}{{/crossLink}}.** @method restoreSelection*/restoreSelection: function() {if (!this._focusFromClick) {if (this._selections) {this.setSelection(this._selections);}}this._focusFromClick = false;},/*** Get the selection object that can be passed back to setSelection.** @method getSelection* @return {array} An array of rangy ranges.*/getSelection: function() {return rangy.getSelection().getAllRanges();},/*** Check that a YUI node it at least partly contained by the current selection.** @method selectionContainsNode* @param {Node} The node to check.* @return {boolean}*/selectionContainsNode: function(node) {return rangy.getSelection().containsNode(node.getDOMNode(), true);},/*** Runs a filter on each node in the selection, and report whether the* supplied selector(s) were found in the supplied Nodes.** By default, all specified nodes must match the selection, but this* can be controlled with the requireall property.** @method selectionFilterMatches* @param {String} selector* @param {NodeList} [selectednodes] For performance this should be passed. If not passed, this will be looked up each time.* @param {Boolean} [requireall=true] Used to specify that "any" match is good enough.* @return {Boolean}*/selectionFilterMatches: function(selector, selectednodes, requireall) {if (typeof requireall === 'undefined') {requireall = true;}if (!selectednodes) {// Find this because it was not passed as a param.selectednodes = this.getSelectedNodes();}var allmatch = selectednodes.size() > 0,anymatch = false;var editor = this.editor,stopFn = function(node) {// The function getSelectedNodes only returns nodes within the editor, so this test is safe.return node === editor;};// If we do not find at least one match in the editor, no point trying to find them in the selection.if (!editor.one(selector)) {return false;}selectednodes.each(function(node) {// Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing.if (requireall) {// Check for at least one failure.if (!allmatch || !node.ancestor(selector, true, stopFn)) {allmatch = false;}} else {// Check for at least one match.if (!anymatch && node.ancestor(selector, true, stopFn)) {anymatch = true;}}}, this);if (requireall) {return allmatch;} else {return anymatch;}},/*** Get the deepest possible list of nodes in the current selection.** @method getSelectedNodes* @return {NodeList}*/getSelectedNodes: function() {var results = new Y.NodeList(),nodes,selection,range,node,i;selection = rangy.getSelection();if (selection.rangeCount) {range = selection.getRangeAt(0);} else {// Empty range.range = rangy.createRange();}if (range.collapsed) {// We do not want to select all the nodes in the editor if we managed to// have a collapsed selection directly in the editor.// It's also possible for the commonAncestorContainer to be the document, which selectNode does not handle// so we must filter that out here too.if (range.commonAncestorContainer !== this.editor.getDOMNode()&& range.commonAncestorContainer !== Y.config.doc) {range = range.cloneRange();range.selectNode(range.commonAncestorContainer);}}nodes = range.getNodes();for (i = 0; i < nodes.length; i++) {node = Y.one(nodes[i]);if (this.editor.contains(node)) {results.push(node);}}return results;},/*** Check whether the current selection has changed since this method was last called.** If the selection has changed, the atto:selectionchanged event is also fired.** @method _hasSelectionChanged* @private* @param {EventFacade} e* @return {Boolean}*/_hasSelectionChanged: function(e) {var selection = rangy.getSelection(),range,changed = false;if (selection.rangeCount) {range = selection.getRangeAt(0);} else {// Empty range.range = rangy.createRange();}if (this._lastSelection) {if (!this._lastSelection.equals(range)) {changed = true;return this._fireSelectionChanged(e);}}this._lastSelection = range;return changed;},/*** Fires the atto:selectionchanged event.** When the selectionchanged event is fired, the following arguments are provided:* - event : the original event that lead to this event being fired.* - selectednodes : an array containing nodes that are entirely selected of contain partially selected content.** @method _fireSelectionChanged* @private* @param {EventFacade} e*/_fireSelectionChanged: function(e) {this.fire('atto:selectionchanged', {event: e,selectedNodes: this.getSelectedNodes()});},/*** Get the DOM node representing the common anscestor of the selection nodes.** @method getSelectionParentNode* @return {Element|boolean} The DOM Node for this parent, or false if no seletion was made.*/getSelectionParentNode: function() {var selection = rangy.getSelection();if (selection.rangeCount) {return selection.getRangeAt(0).commonAncestorContainer;}return false;},/*** Set the current selection. Used to restore a selection.** @method selection* @param {array} ranges A list of rangy.range objects in the selection.*/setSelection: function(ranges) {var selection = rangy.getSelection();selection.setRanges(ranges);},/*** Inserts the given HTML into the editable content at the currently focused point.** @method insertContentAtFocusPoint* @param {String} html* @return {Node} The YUI Node object added to the DOM.*/insertContentAtFocusPoint: function(html) {var selection = rangy.getSelection(),range,node = Y.Node.create(html);if (selection.rangeCount) {range = selection.getRangeAt(0);}if (range) {range.deleteContents();range.collapse(false);var currentnode = node.getDOMNode(),last = currentnode.lastChild || currentnode;range.insertNode(currentnode);range.collapseAfter(last);selection.setSingleRange(range);}return node;}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorSelection]);// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** @module moodle-editor_atto-editor* @submodule styling*//*** Editor styling functions for the Atto editor.** See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.** @namespace M.editor_atto* @class EditorStyling*/function EditorStyling() {}EditorStyling.ATTRS = {};EditorStyling.prototype = {/*** Disable CSS styling.** @method disableCssStyling*/disableCssStyling: function() {try {document.execCommand("styleWithCSS", 0, false);} catch (e1) {try {document.execCommand("useCSS", 0, true);} catch (e2) {try {document.execCommand('styleWithCSS', false, false);} catch (e3) {// We did our best.}}}},/*** Enable CSS styling.** @method enableCssStyling*/enableCssStyling: function() {try {document.execCommand("styleWithCSS", 0, true);} catch (e1) {try {document.execCommand("useCSS", 0, false);} catch (e2) {try {document.execCommand('styleWithCSS', false, true);} catch (e3) {// We did our best.}}}},/*** Change the formatting for the current selection.** This will wrap the selection in span tags, adding the provided classes.** If the selection covers multiple block elements, multiple spans will be inserted to preserve the original structure.** @method toggleInlineSelectionClass* @param {Array} toggleclasses - Class names to be toggled on or off.*/toggleInlineSelectionClass: function(toggleclasses) {var classname = toggleclasses.join(" ");var cssApplier = rangy.createClassApplier(classname, {normalize: true});cssApplier.toggleSelection();},/*** Change the formatting for the current selection.** This will set inline styles on the current selection.** @method formatSelectionInlineStyle* @param {Array} styles - Style attributes to set on the nodes.*/formatSelectionInlineStyle: function(styles) {var classname = this.PLACEHOLDER_CLASS;var cssApplier = rangy.createClassApplier(classname, {normalize: true});cssApplier.applyToSelection();this.editor.all('.' + classname).each(function(node) {node.removeClass(classname).setStyles(styles);}, this);},/*** Change the formatting for the current selection.** Also changes the selection to the newly formatted block (allows applying multiple styles to a block).** @method formatSelectionBlock* @param {String} [blocktag] Change the block level tag to this. Empty string, means do not change the tag.* @param {Object} [attributes] The keys and values for attributes to be added/changed in the block tag.* @return {Node|boolean} The Node that was formatted if a change was made, otherwise false.*/formatSelectionBlock: function(blocktag, attributes) {// First find the nearest ancestor of the selection that is a block level element.var selectionparentnode = this.getSelectionParentNode(),boundary,cell,nearestblock,newcontent,match,replacement;if (!selectionparentnode) {// No selection, nothing to format.return false;}boundary = this.editor;selectionparentnode = Y.one(selectionparentnode);// If there is a table cell in between the selectionparentnode and the boundary,// move the boundary to the table cell.// This is because we might have a table in a div, and we select some text in a cell,// want to limit the change in style to the table cell, not the entire table (via the outer div).cell = selectionparentnode.ancestor(function(node) {var tagname = node.get('tagName');if (tagname) {tagname = tagname.toLowerCase();}return (node === boundary) ||(tagname === 'td') ||(tagname === 'th');}, true);if (cell) {// Limit the scope to the table cell.boundary = cell;}nearestblock = selectionparentnode.ancestor(this.BLOCK_TAGS.join(', '), true);if (nearestblock) {// Check that the block is contained by the boundary.match = nearestblock.ancestor(function(node) {return node === boundary;}, false);if (!match) {nearestblock = false;}}// No valid block element - make one.if (!nearestblock) {var alignment;if (this.coreDirection === 'rtl') {alignment = 'style="text-align: right;"';} else {alignment = 'style="text-align: left;"';}// There is no block node in the content, wrap the content in a p and use that.newcontent = Y.Node.create('<p dir="' + this.coreDirection + '" ' + alignment + '></p>');boundary.get('childNodes').each(function(child) {newcontent.append(child.remove());});boundary.append(newcontent);nearestblock = newcontent;}// Guaranteed to have a valid block level element contained in the contenteditable region.// Change the tag to the new block level tag.if (blocktag && blocktag !== '') {// Change the block level node for a new one.replacement = Y.Node.create('<' + blocktag + '></' + blocktag + '>');// Copy all attributes.replacement.setAttrs(nearestblock.getAttrs());// Copy all children.nearestblock.get('childNodes').each(function(child) {child.remove();replacement.append(child);});nearestblock.replace(replacement);nearestblock = replacement;}// Set the attributes on the block level tag.if (attributes) {nearestblock.setAttrs(attributes);}// Change the selection to the modified block. This makes sense when we might apply multiple styles// to the block.var selection = this.getSelectionFromNode(nearestblock);this.setSelection(selection);return nearestblock;}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorStyling]);// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** @module moodle-editor_atto-editor* @submodule filepicker*//*** Filepicker options for the Atto editor.** See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.** @namespace M.editor_atto* @class EditorFilepicker*/function EditorFilepicker() {}EditorFilepicker.ATTRS = {/*** The options for the filepicker.** @attribute filepickeroptions* @type object* @default {}*/filepickeroptions: {value: {}}};EditorFilepicker.prototype = {/*** Should we show the filepicker for this filetype?** @method canShowFilepicker* @param string type The media type for the file picker.* @return {boolean}*/canShowFilepicker: function(type) {return (typeof this.get('filepickeroptions')[type] !== 'undefined');},/*** Show the filepicker.** This depends on core_filepicker, and then call that modules show function.** @method showFilepicker* @param {string} type The media type for the file picker.* @param {function} callback The callback to use when selecting an item of media.* @param {object} [context] The context from which to call the callback.*/showFilepicker: function(type, callback, context) {var self = this;Y.use('core_filepicker', function(Y) {var options = Y.clone(self.get('filepickeroptions')[type], true);options.formcallback = callback;if (context) {options.magicscope = context;}M.core_filepicker.show(Y, options);});}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorFilepicker]);}, '@VERSION@', {"requires": ["node","transition","io","overlay","escape","event","event-simulate","event-custom","node-event-html5","node-event-simulate","yui-throttle","moodle-core-notification-dialogue","moodle-editor_atto-rangy","handlebars","timers","querystring-stringify"]});