%PDF- %PDF-
Direktori : /home/vacivi36/ava/lib/editor/atto/yui/src/editor/js/ |
Current File : /home/vacivi36/ava/lib/editor/atto/yui/src/editor/js/editor.js |
// 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. Y.log('Text area not found - unable to setup editor for ' + this.get('elementid'), 'error', LOGNAME); 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") { Y.log("Plugin '" + plugin.name + "' could not be found - skipping initialisation", "warn", LOGNAME); 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); };