[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 /* eslint-disable no-unused-vars */ 16 17 /** 18 * The Atto WYSIWG pluggable editor, written for Moodle. 19 * 20 * @module moodle-editor_atto-editor 21 * @package editor_atto 22 * @copyright 2013 Damyon Wiese <damyon@moodle.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 * @main moodle-editor_atto-editor 25 */ 26 27 /** 28 * @module moodle-editor_atto-editor 29 * @submodule editor-base 30 */ 31 32 var LOGNAME = 'moodle-editor_atto-editor'; 33 var CSS = { 34 CONTENT: 'editor_atto_content', 35 CONTENTWRAPPER: 'editor_atto_content_wrap', 36 TOOLBAR: 'editor_atto_toolbar', 37 WRAPPER: 'editor_atto', 38 HIGHLIGHT: 'highlight' 39 }, 40 rangy = window.rangy; 41 42 /** 43 * The Atto editor for Moodle. 44 * 45 * @namespace M.editor_atto 46 * @class Editor 47 * @constructor 48 * @uses M.editor_atto.EditorClean 49 * @uses M.editor_atto.EditorFilepicker 50 * @uses M.editor_atto.EditorSelection 51 * @uses M.editor_atto.EditorStyling 52 * @uses M.editor_atto.EditorTextArea 53 * @uses M.editor_atto.EditorToolbar 54 * @uses M.editor_atto.EditorToolbarNav 55 */ 56 57 function Editor() { 58 Editor.superclass.constructor.apply(this, arguments); 59 } 60 61 Y.extend(Editor, Y.Base, { 62 63 /** 64 * List of known block level tags. 65 * Taken from "https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements". 66 * 67 * @property BLOCK_TAGS 68 * @type {Array} 69 */ 70 BLOCK_TAGS: [ 71 'address', 72 'article', 73 'aside', 74 'audio', 75 'blockquote', 76 'canvas', 77 'dd', 78 'div', 79 'dl', 80 'fieldset', 81 'figcaption', 82 'figure', 83 'footer', 84 'form', 85 'h1', 86 'h2', 87 'h3', 88 'h4', 89 'h5', 90 'h6', 91 'header', 92 'hgroup', 93 'hr', 94 'noscript', 95 'ol', 96 'output', 97 'p', 98 'pre', 99 'section', 100 'table', 101 'tfoot', 102 'ul', 103 'video' 104 ], 105 106 PLACEHOLDER_CLASS: 'atto-tmp-class', 107 ALL_NODES_SELECTOR: '[style],font[face]', 108 FONT_FAMILY: 'fontFamily', 109 110 /** 111 * The wrapper containing the editor. 112 * 113 * @property _wrapper 114 * @type Node 115 * @private 116 */ 117 _wrapper: null, 118 119 /** 120 * A reference to the content editable Node. 121 * 122 * @property editor 123 * @type Node 124 */ 125 editor: null, 126 127 /** 128 * A reference to the original text area. 129 * 130 * @property textarea 131 * @type Node 132 */ 133 textarea: null, 134 135 /** 136 * A reference to the label associated with the original text area. 137 * 138 * @property textareaLabel 139 * @type Node 140 */ 141 textareaLabel: null, 142 143 /** 144 * A reference to the list of plugins. 145 * 146 * @property plugins 147 * @type object 148 */ 149 plugins: null, 150 151 /** 152 * Event Handles to clear on editor destruction. 153 * 154 * @property _eventHandles 155 * @private 156 */ 157 _eventHandles: null, 158 159 initializer: function() { 160 var template; 161 162 // Note - it is not safe to use a CSS selector like '#' + elementid because the id 163 // may have colons in it - e.g. quiz. 164 this.textarea = Y.one(document.getElementById(this.get('elementid'))); 165 166 if (!this.textarea) { 167 // No text area found. 168 Y.log('Text area not found - unable to setup editor for ' + this.get('elementid'), 169 'error', LOGNAME); 170 return; 171 } 172 173 this._eventHandles = []; 174 175 this._wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />'); 176 template = Y.Handlebars.compile('<div id="{{elementid}}editable" ' + 177 'contenteditable="true" ' + 178 'role="textbox" ' + 179 'spellcheck="true" ' + 180 'aria-live="off" ' + 181 'class="{{CSS.CONTENT}}" ' + 182 '/>'); 183 this.editor = Y.Node.create(template({ 184 elementid: this.get('elementid'), 185 CSS: CSS 186 })); 187 188 // Add a labelled-by attribute to the contenteditable. 189 this.textareaLabel = Y.one('[for="' + this.get('elementid') + '"]'); 190 if (this.textareaLabel) { 191 this.textareaLabel.generateID(); 192 this.editor.setAttribute('aria-labelledby', this.textareaLabel.get("id")); 193 } 194 195 // Add everything to the wrapper. 196 this.setupToolbar(); 197 198 // Editable content wrapper. 199 var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />'); 200 content.appendChild(this.editor); 201 this._wrapper.appendChild(content); 202 203 // Style the editor. According to the styles.css: 20 is the line-height, 8 is padding-top + padding-bottom. 204 this.editor.setStyle('minHeight', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px'); 205 206 if (Y.UA.ie === 0) { 207 // We set a height here to force the overflow because decent browsers allow the CSS property resize. 208 this.editor.setStyle('height', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px'); 209 } 210 211 // Disable odd inline CSS styles. 212 this.disableCssStyling(); 213 214 // Use paragraphs not divs. 215 if (document.queryCommandSupported('DefaultParagraphSeparator')) { 216 document.execCommand('DefaultParagraphSeparator', false, 'p'); 217 } 218 219 // Add the toolbar and editable zone to the page. 220 this.textarea.get('parentNode').insert(this._wrapper, this.textarea). 221 setAttribute('class', 'editor_atto_wrap'); 222 223 // Hide the old textarea. 224 this.textarea.hide(); 225 226 // Copy the text to the contenteditable div. 227 this.updateFromTextArea(); 228 229 // Publish the events that are defined by this editor. 230 this.publishEvents(); 231 232 // Add handling for saving and restoring selections on cursor/focus changes. 233 this.setupSelectionWatchers(); 234 235 // Add polling to update the textarea periodically when typing long content. 236 this.setupAutomaticPolling(); 237 238 // Setup plugins. 239 this.setupPlugins(); 240 241 // Initialize the auto-save timer. 242 this.setupAutosave(); 243 // Preload the icons for the notifications. 244 this.setupNotifications(); 245 }, 246 247 /** 248 * Focus on the editable area for this editor. 249 * 250 * @method focus 251 * @chainable 252 */ 253 focus: function() { 254 this.editor.focus(); 255 256 return this; 257 }, 258 259 /** 260 * Publish events for this editor instance. 261 * 262 * @method publishEvents 263 * @private 264 * @chainable 265 */ 266 publishEvents: function() { 267 /** 268 * Fired when changes are made within the editor. 269 * 270 * @event change 271 */ 272 this.publish('change', { 273 broadcast: true, 274 preventable: true 275 }); 276 277 /** 278 * Fired when all plugins have completed loading. 279 * 280 * @event pluginsloaded 281 */ 282 this.publish('pluginsloaded', { 283 fireOnce: true 284 }); 285 286 this.publish('atto:selectionchanged', { 287 prefix: 'atto' 288 }); 289 290 return this; 291 }, 292 293 /** 294 * Set up automated polling of the text area to update the textarea. 295 * 296 * @method setupAutomaticPolling 297 * @chainable 298 */ 299 setupAutomaticPolling: function() { 300 this._registerEventHandle(this.editor.on(['keyup', 'cut'], this.updateOriginal, this)); 301 this._registerEventHandle(this.editor.on('paste', this.pasteCleanup, this)); 302 303 // Call this.updateOriginal after dropped content has been processed. 304 this._registerEventHandle(this.editor.on('drop', this.updateOriginalDelayed, this)); 305 306 return this; 307 }, 308 309 /** 310 * Calls updateOriginal on a short timer to allow native event handlers to run first. 311 * 312 * @method updateOriginalDelayed 313 * @chainable 314 */ 315 updateOriginalDelayed: function() { 316 Y.soon(Y.bind(this.updateOriginal, this)); 317 318 return this; 319 }, 320 321 setupPlugins: function() { 322 // Clear the list of plugins. 323 this.plugins = {}; 324 325 var plugins = this.get('plugins'); 326 327 var groupIndex, 328 group, 329 pluginIndex, 330 plugin, 331 pluginConfig; 332 333 for (groupIndex in plugins) { 334 group = plugins[groupIndex]; 335 if (!group.plugins) { 336 // No plugins in this group - skip it. 337 continue; 338 } 339 for (pluginIndex in group.plugins) { 340 plugin = group.plugins[pluginIndex]; 341 342 pluginConfig = Y.mix({ 343 name: plugin.name, 344 group: group.group, 345 editor: this.editor, 346 toolbar: this.toolbar, 347 host: this 348 }, plugin); 349 350 // Add a reference to the current editor. 351 if (typeof Y.M['atto_' + plugin.name] === "undefined") { 352 Y.log("Plugin '" + plugin.name + "' could not be found - skipping initialisation", "warn", LOGNAME); 353 continue; 354 } 355 this.plugins[plugin.name] = new Y.M['atto_' + plugin.name].Button(pluginConfig); 356 } 357 } 358 359 // Some plugins need to perform actions once all plugins have loaded. 360 this.fire('pluginsloaded'); 361 362 return this; 363 }, 364 365 enablePlugins: function(plugin) { 366 this._setPluginState(true, plugin); 367 }, 368 369 disablePlugins: function(plugin) { 370 this._setPluginState(false, plugin); 371 }, 372 373 _setPluginState: function(enable, plugin) { 374 var target = 'disableButtons'; 375 if (enable) { 376 target = 'enableButtons'; 377 } 378 379 if (plugin) { 380 this.plugins[plugin][target](); 381 } else { 382 Y.Object.each(this.plugins, function(currentPlugin) { 383 currentPlugin[target](); 384 }, this); 385 } 386 }, 387 388 /** 389 * Register an event handle for disposal in the destructor. 390 * 391 * @method _registerEventHandle 392 * @param {EventHandle} The Event Handle as returned by Y.on, and Y.delegate. 393 * @private 394 */ 395 _registerEventHandle: function(handle) { 396 this._eventHandles.push(handle); 397 } 398 399 }, { 400 NS: 'editor_atto', 401 ATTRS: { 402 /** 403 * The unique identifier for the form element representing the editor. 404 * 405 * @attribute elementid 406 * @type String 407 * @writeOnce 408 */ 409 elementid: { 410 value: null, 411 writeOnce: true 412 }, 413 414 /** 415 * The contextid of the form. 416 * 417 * @attribute contextid 418 * @type Integer 419 * @writeOnce 420 */ 421 contextid: { 422 value: null, 423 writeOnce: true 424 }, 425 426 /** 427 * Plugins with their configuration. 428 * 429 * The plugins structure is: 430 * 431 * [ 432 * { 433 * "group": "groupName", 434 * "plugins": [ 435 * "pluginName": { 436 * "configKey": "configValue" 437 * }, 438 * "pluginName": { 439 * "configKey": "configValue" 440 * } 441 * ] 442 * }, 443 * { 444 * "group": "groupName", 445 * "plugins": [ 446 * "pluginName": { 447 * "configKey": "configValue" 448 * } 449 * ] 450 * } 451 * ] 452 * 453 * @attribute plugins 454 * @type Object 455 * @writeOnce 456 */ 457 plugins: { 458 value: {}, 459 writeOnce: true 460 } 461 } 462 }); 463 464 // The Editor publishes custom events that can be subscribed to. 465 Y.augment(Editor, Y.EventTarget); 466 467 Y.namespace('M.editor_atto').Editor = Editor; 468 469 // Function for Moodle's initialisation. 470 Y.namespace('M.editor_atto.Editor').init = function(config) { 471 return new Y.M.editor_atto.Editor(config); 472 };
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |