Merge branch 'MDL-32729-master' of git://github.com/andrewnicols/moodle
[moodle.git] / lib / editor / atto / yui / src / editor / js / editor.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * The Atto WYSIWG pluggable editor, written for Moodle.
18  *
19  * @module     moodle-editor_atto-editor
20  * @package    editor_atto
21  * @copyright  2013 Damyon Wiese  <damyon@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  * @main       moodle-editor_atto-editor
24  */
26 /**
27  * @module moodle-editor_atto-editor
28  * @submodule editor-base
29  */
31 var LOGNAME = 'moodle-editor_atto-editor';
32 var CSS = {
33         CONTENT: 'editor_atto_content',
34         CONTENTWRAPPER: 'editor_atto_content_wrap',
35         TOOLBAR: 'editor_atto_toolbar',
36         WRAPPER: 'editor_atto',
37         HIGHLIGHT: 'highlight'
38     };
40 /**
41  * The Atto editor for Moodle.
42  *
43  * @namespace M.editor_atto
44  * @class Editor
45  * @constructor
46  * @uses M.editor_atto.EditorClean
47  * @uses M.editor_atto.EditorFilepicker
48  * @uses M.editor_atto.EditorSelection
49  * @uses M.editor_atto.EditorStyling
50  * @uses M.editor_atto.EditorTextArea
51  * @uses M.editor_atto.EditorToolbar
52  * @uses M.editor_atto.EditorToolbarNav
53  */
55 function Editor() {
56     Editor.superclass.constructor.apply(this, arguments);
57 }
59 Y.extend(Editor, Y.Base, {
61     /**
62      * List of known block level tags.
63      * Taken from "https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements".
64      *
65      * @property BLOCK_TAGS
66      * @type {Array}
67      */
68     BLOCK_TAGS : [
69         'address',
70         'article',
71         'aside',
72         'audio',
73         'blockquote',
74         'canvas',
75         'dd',
76         'div',
77         'dl',
78         'fieldset',
79         'figcaption',
80         'figure',
81         'footer',
82         'form',
83         'h1',
84         'h2',
85         'h3',
86         'h4',
87         'h5',
88         'h6',
89         'header',
90         'hgroup',
91         'hr',
92         'noscript',
93         'ol',
94         'output',
95         'p',
96         'pre',
97         'section',
98         'table',
99         'tfoot',
100         'ul',
101         'video'
102     ],
104     PLACEHOLDER_FONTNAME: 'yui-tmp',
105     ALL_NODES_SELECTOR: '[style],font[face]',
106     FONT_FAMILY: 'fontFamily',
108     /**
109      * The wrapper containing the editor.
110      *
111      * @property _wrapper
112      * @type Node
113      * @private
114      */
115     _wrapper: null,
117     /**
118      * A reference to the content editable Node.
119      *
120      * @property editor
121      * @type Node
122      */
123     editor: null,
125     /**
126      * A reference to the original text area.
127      *
128      * @property textarea
129      * @type Node
130      */
131     textarea: null,
133     /**
134      * A reference to the label associated with the original text area.
135      *
136      * @property textareaLabel
137      * @type Node
138      */
139     textareaLabel: null,
141     /**
142      * A reference to the list of plugins.
143      *
144      * @property plugins
145      * @type object
146      */
147     plugins: null,
149     initializer: function() {
150         var template;
152         // Note - it is not safe to use a CSS selector like '#' + elementid because the id
153         // may have colons in it - e.g.  quiz.
154         this.textarea = Y.one(document.getElementById(this.get('elementid')));
156         if (!this.textarea) {
157             // No text area found.
158             Y.log('Text area not found - unable to setup editor for ' + this.get('elementid'),
159                     'error', LOGNAME);
160             return;
161         }
163         this._wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />');
164         template = Y.Handlebars.compile('<div id="{{elementid}}editable" ' +
165                 'contenteditable="true" ' +
166                 'role="textbox" ' +
167                 'spellcheck="true" ' +
168                 'aria-live="off" ' +
169                 'class="{{CSS.CONTENT}}" ' +
170                 '/>');
171         this.editor = Y.Node.create(template({
172             elementid: this.get('elementid'),
173             CSS: CSS
174         }));
176         // Add a labelled-by attribute to the contenteditable.
177         this.textareaLabel = Y.one('[for="' + this.get('elementid') + '"]');
178         if (this.textareaLabel) {
179             this.textareaLabel.generateID();
180             this.editor.setAttribute('aria-labelledby', this.textareaLabel.get("id"));
181         }
183         // Add everything to the wrapper.
184         this.setupToolbar();
186         // Editable content wrapper.
187         var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />');
188         content.appendChild(this.editor);
189         this._wrapper.appendChild(content);
191         // Style the editor.
192         this.editor.setStyle('minHeight', (1.2 * (this.textarea.getAttribute('rows'))) + 'em');
193         // Disable odd inline CSS styles.
194         this.disableCssStyling();
196         // Add the toolbar and editable zone to the page.
197         this.textarea.get('parentNode').insert(this._wrapper, this.textarea);
199         // Hide the old textarea.
200         this.textarea.hide();
202         // Copy the text to the contenteditable div.
203         this.updateFromTextArea();
205         // Publish the events that are defined by this editor.
206         this.publishEvents();
208         // Add handling for saving and restoring selections on cursor/focus changes.
209         this.setupSelectionWatchers();
211         // Setup plugins.
212         this.setupPlugins();
213     },
215     /**
216      * Focus on the editable area for this editor.
217      *
218      * @method focus
219      * @chainable
220      */
221     focus: function() {
222         this.editor.focus();
224         return this;
225     },
227     /**
228      * Publish events for this editor instance.
229      *
230      * @method publishEvents
231      * @private
232      * @chainable
233      */
234     publishEvents: function() {
235         /**
236          * Fired when changes are made within the editor.
237          *
238          * @event change
239          */
240         this.publish('change', {
241             broadcast: true,
242             preventable: true
243         });
245         /**
246          * Fired when all plugins have completed loading.
247          *
248          * @event pluginsloaded
249          */
250         this.publish('pluginsloaded', {
251             fireOnce: true
252         });
254         this.publish('atto:selectionchanged', {
255             prefix: 'atto'
256         });
258         Y.delegate(['mouseup', 'keyup', 'focus'], this._hasSelectionChanged, document.body, '.' + CSS.CONTENT, this);
260         return this;
261     },
263     setupPlugins: function() {
264         // Clear the list of plugins.
265         this.plugins = {};
267         var plugins = this.get('plugins');
269         var groupIndex,
270             group,
271             pluginIndex,
272             plugin,
273             pluginConfig;
275         for (groupIndex in plugins) {
276             group = plugins[groupIndex];
277             if (!group.plugins) {
278                 // No plugins in this group - skip it.
279                 continue;
280             }
281             for (pluginIndex in group.plugins) {
282                 plugin = group.plugins[pluginIndex];
284                 pluginConfig = Y.mix({
285                     name: plugin.name,
286                     group: group.group,
287                     editor: this.editor,
288                     toolbar: this.toolbar,
289                     host: this
290                 }, plugin);
292                 // Add a reference to the current editor.
293                 if (typeof Y.M['atto_' + plugin.name] === "undefined") {
294                     Y.log("Plugin '" + plugin.name + "' could not be found - skipping initialisation", "warn", LOGNAME);
295                     continue;
296                 }
297                 this.plugins[plugin.name] = new Y.M['atto_' + plugin.name].Button(pluginConfig);
298             }
299         }
301         // Some plugins need to perform actions once all plugins have loaded.
302         this.fire('pluginsloaded');
304         return this;
305     },
307     enablePlugins: function(plugin) {
308         this._setPluginState(true, plugin);
309     },
311     disablePlugins: function(plugin) {
312         this._setPluginState(false, plugin);
313     },
315     _setPluginState: function(enable, plugin) {
316         var target = 'disableButtons';
317         if (enable) {
318             target = 'enableButtons';
319         }
321         if (plugin) {
322             this.plugins[plugin][target]();
323         } else {
324             Y.Object.each(this.plugins, function(currentPlugin) {
325                 currentPlugin[target]();
326             }, this);
327         }
328     }
330 }, {
331     NS: 'editor_atto',
332     ATTRS: {
333         /**
334          * The unique identifier for the form element representing the editor.
335          *
336          * @attribute elementid
337          * @type String
338          * @writeOnce
339          */
340         elementid: {
341             value: null,
342             writeOnce: true
343         },
345         /**
346          * Plugins with their configuration.
347          *
348          * The plugins structure is:
349          *
350          *     [
351          *         {
352          *             "group": "groupName",
353          *             "plugins": [
354          *                 "pluginName": {
355          *                     "configKey": "configValue"
356          *                 },
357          *                 "pluginName": {
358          *                     "configKey": "configValue"
359          *                 }
360          *             ]
361          *         },
362          *         {
363          *             "group": "groupName",
364          *             "plugins": [
365          *                 "pluginName": {
366          *                     "configKey": "configValue"
367          *                 }
368          *             ]
369          *         }
370          *     ]
371          *
372          * @attribute plugins
373          * @type Object
374          * @writeOnce
375          */
376         plugins: {
377             value: {},
378             writeOnce: true
379         }
380     }
381 });
383 // The Editor publishes custom events that can be subscribed to.
384 Y.augment(Editor, Y.EventTarget);
386 Y.namespace('M.editor_atto').Editor = Editor;
388 // Function for Moodle's initialisation.
389 Y.namespace('M.editor_atto.Editor').init = function(config) {
390     return new Y.M.editor_atto.Editor(config);
391 };