MDL-44834 editor_atto: Delegate change event to one editor
[moodle.git] / lib / editor / atto / yui / src / editor / js / editor.js
CommitLineData
adca7326
DW
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/>.
15
16/**
62467795 17 * The Atto WYSIWG pluggable editor, written for Moodle.
adca7326 18 *
62467795 19 * @module moodle-editor_atto-editor
adca7326
DW
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
62467795 23 * @main moodle-editor_atto-editor
adca7326
DW
24 */
25
26/**
62467795
AN
27 * @module moodle-editor_atto-editor
28 * @submodule editor-base
adca7326 29 */
62467795
AN
30
31var LOGNAME = 'moodle-editor_atto-editor';
32var 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 };
adca7326
DW
39
40/**
62467795 41 * The Atto editor for Moodle.
adca7326 42 *
62467795
AN
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
adca7326 53 */
62467795
AN
54
55function Editor() {
56 Editor.superclass.constructor.apply(this, arguments);
57}
58
59Y.extend(Editor, Y.Base, {
adca7326 60
34f5867a
DW
61 /**
62 * List of known block level tags.
63 * Taken from "https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements".
64 *
62467795 65 * @property BLOCK_TAGS
34f5867a
DW
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',
62467795
AN
101 'video'
102 ],
d321f68b 103
bed1abbc
AD
104 PLACEHOLDER_FONTNAME: 'yui-tmp',
105 ALL_NODES_SELECTOR: '[style],font[face]',
106 FONT_FAMILY: 'fontFamily',
34f5867a 107
adca7326 108 /**
62467795 109 * The wrapper containing the editor.
67d3fe45 110 *
62467795
AN
111 * @property _wrapper
112 * @type Node
113 * @private
adca7326 114 */
62467795 115 _wrapper: null,
adca7326 116
48bdf86f 117 /**
62467795 118 * A reference to the content editable Node.
48bdf86f 119 *
62467795
AN
120 * @property editor
121 * @type Node
48bdf86f 122 */
62467795 123 editor: null,
48bdf86f
DW
124
125 /**
62467795 126 * A reference to the original text area.
48bdf86f 127 *
62467795
AN
128 * @property textarea
129 * @type Node
48bdf86f 130 */
62467795 131 textarea: null,
48bdf86f
DW
132
133 /**
62467795 134 * A reference to the label associated with the original text area.
48bdf86f 135 *
62467795
AN
136 * @property textareaLabel
137 * @type Node
3ee53a42 138 */
62467795 139 textareaLabel: null,
3ee53a42
DW
140
141 /**
62467795 142 * A reference to the list of plugins.
8951d614 143 *
62467795
AN
144 * @property plugins
145 * @type object
adca7326 146 */
62467795 147 plugins: null,
fe0d2477 148
62467795
AN
149 initializer: function() {
150 var template;
adca7326 151
62467795
AN
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')));
adca7326 155
62467795
AN
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;
adca7326
DW
161 }
162
62467795
AN
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 }));
34f5867a 175
62467795
AN
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"));
86a83e3a 181 }
adca7326 182
62467795
AN
183 // Add everything to the wrapper.
184 this.setupToolbar();
adca7326
DW
185
186 // Editable content wrapper.
187 var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />');
62467795
AN
188 content.appendChild(this.editor);
189 this._wrapper.appendChild(content);
adca7326 190
4c37c1f4 191 // Style the editor.
62467795
AN
192 this.editor.setStyle('minHeight', (1.2 * (this.textarea.getAttribute('rows'))) + 'em');
193 // Disable odd inline CSS styles.
194 this.disableCssStyling();
d088a835 195
adca7326 196 // Add the toolbar and editable zone to the page.
62467795 197 this.textarea.get('parentNode').insert(this._wrapper, this.textarea);
4c37c1f4 198
adca7326 199 // Hide the old textarea.
62467795 200 this.textarea.hide();
3ee53a42 201
86a83e3a 202 // Copy the text to the contenteditable div.
62467795 203 this.updateFromTextArea();
adca7326 204
62467795
AN
205 // Publish the events that are defined by this editor.
206 this.publishEvents();
55c0403c 207
62467795
AN
208 // Add handling for saving and restoring selections on cursor/focus changes.
209 this.setupSelectionWatchers();
3ee53a42 210
62467795
AN
211 // Setup plugins.
212 this.setupPlugins();
3ee53a42
DW
213 },
214
215 /**
62467795 216 * Focus on the editable area for this editor.
67d3fe45 217 *
62467795
AN
218 * @method focus
219 * @chainable
3ee53a42 220 */
62467795
AN
221 focus: function() {
222 this.editor.focus();
d321f68b 223
62467795 224 return this;
3ee53a42
DW
225 },
226
227 /**
62467795 228 * Publish events for this editor instance.
67d3fe45 229 *
62467795 230 * @method publishEvents
67d3fe45 231 * @private
62467795
AN
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 });
d321f68b 244
62467795
AN
245 /**
246 * Fired when all plugins have completed loading.
247 *
248 * @event pluginsloaded
249 */
250 this.publish('pluginsloaded', {
251 fireOnce: true
252 });
3ee53a42 253
62467795
AN
254 this.publish('atto:selectionchanged', {
255 prefix: 'atto'
67d3fe45 256 });
67d3fe45 257
5ce4583a 258 Y.delegate(['mouseup', 'keyup', 'focus'], this._hasSelectionChanged, document.body, '#' + this.editor.get('id'), this);
adca7326 259
62467795 260 return this;
34f5867a
DW
261 },
262
62467795
AN
263 setupPlugins: function() {
264 // Clear the list of plugins.
265 this.plugins = {};
34f5867a 266
62467795 267 var plugins = this.get('plugins');
34f5867a 268
62467795
AN
269 var groupIndex,
270 group,
271 pluginIndex,
272 plugin,
273 pluginConfig;
34f5867a 274
62467795
AN
275 for (groupIndex in plugins) {
276 group = plugins[groupIndex];
277 if (!group.plugins) {
278 // No plugins in this group - skip it.
279 continue;
34f5867a 280 }
62467795
AN
281 for (pluginIndex in group.plugins) {
282 plugin = group.plugins[pluginIndex];
283
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);
291
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);
34f5867a
DW
298 }
299 }
300
62467795
AN
301 // Some plugins need to perform actions once all plugins have loaded.
302 this.fire('pluginsloaded');
34f5867a 303
62467795 304 return this;
f6bef145
FM
305 },
306
62467795
AN
307 enablePlugins: function(plugin) {
308 this._setPluginState(true, plugin);
f6bef145
FM
309 },
310
62467795
AN
311 disablePlugins: function(plugin) {
312 this._setPluginState(false, plugin);
2faf4c45
SH
313 },
314
62467795
AN
315 _setPluginState: function(enable, plugin) {
316 var target = 'disableButtons';
317 if (enable) {
318 target = 'enableButtons';
2faf4c45 319 }
bed1abbc 320
62467795
AN
321 if (plugin) {
322 this.plugins[plugin][target]();
323 } else {
324 Y.Object.each(this.plugins, function(currentPlugin) {
325 currentPlugin[target]();
326 }, this);
bed1abbc 327 }
62467795 328 }
bed1abbc 329
62467795
AN
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 },
344
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});
adca7326 382
62467795
AN
383// The Editor publishes custom events that can be subscribed to.
384Y.augment(Editor, Y.EventTarget);
bed1abbc 385
62467795 386Y.namespace('M.editor_atto').Editor = Editor;
bed1abbc 387
62467795
AN
388// Function for Moodle's initialisation.
389Y.namespace('M.editor_atto.Editor').init = function(config) {
390 return new Y.M.editor_atto.Editor(config);
adca7326 391};