[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 YUI.add('moodle-editor_atto-plugin', function (Y, NAME) { 2 3 // This file is part of Moodle - http://moodle.org/ 4 // 5 // Moodle is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // Moodle is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU General Public License for more details. 14 // 15 // You should have received a copy of the GNU General Public License 16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 17 18 /** 19 * Atto editor plugin. 20 * 21 * @module moodle-editor_atto-plugin 22 * @submodule plugin-base 23 * @package editor_atto 24 * @copyright 2014 Andrew Nicols 25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 */ 27 28 /** 29 * A Plugin for the Atto Editor used in Moodle. 30 * 31 * This class should not be directly instantiated, and all Editor plugins 32 * should extend this class. 33 * 34 * @namespace M.editor_atto 35 * @class EditorPlugin 36 * @main 37 * @constructor 38 * @uses M.editor_atto.EditorPluginButtons 39 * @uses M.editor_atto.EditorPluginDialogue 40 */ 41 42 function EditorPlugin() { 43 EditorPlugin.superclass.constructor.apply(this, arguments); 44 } 45 46 var GROUPSELECTOR = '.atto_group.', 47 GROUP = '_group'; 48 49 Y.extend(EditorPlugin, Y.Base, { 50 /** 51 * The name of the current plugin. 52 * 53 * @property name 54 * @type string 55 */ 56 name: null, 57 58 /** 59 * A Node reference to the editor. 60 * 61 * @property editor 62 * @type Node 63 */ 64 editor: null, 65 66 /** 67 * A Node reference to the editor toolbar. 68 * 69 * @property toolbar 70 * @type Node 71 */ 72 toolbar: null, 73 74 initializer: function(config) { 75 // Set the references to configuration parameters. 76 this.name = config.name; 77 this.toolbar = config.toolbar; 78 this.editor = config.editor; 79 80 // Set up the prototypal properties. 81 // These must be set up here becuase prototypal arrays and objects are copied across instances. 82 this.buttons = {}; 83 this.buttonNames = []; 84 this.buttonStates = {}; 85 this.menus = {}; 86 this._primaryKeyboardShortcut = []; 87 this._buttonHandlers = []; 88 this._menuHideHandlers = []; 89 this._highlightQueue = {}; 90 }, 91 92 /** 93 * Mark the content ediable content as having been changed. 94 * 95 * This is a convenience function and passes through to 96 * {{#crossLink "M.editor_atto.EditorTextArea/updateOriginal"}}updateOriginal{{/crossLink}}. 97 * 98 * @method markUpdated 99 */ 100 markUpdated: function() { 101 // Save selection after changes to the DOM. If you don't do this here, 102 // subsequent calls to restoreSelection() will fail expecting the 103 // previous DOM state. 104 this.get('host').saveSelection(); 105 106 return this.get('host').updateOriginal(); 107 } 108 }, { 109 NAME: 'editorPlugin', 110 ATTRS: { 111 /** 112 * The editor instance that this plugin was instantiated by. 113 * 114 * @attribute host 115 * @type M.editor_atto.Editor 116 * @writeOnce 117 */ 118 host: { 119 writeOnce: true 120 }, 121 122 /** 123 * The toolbar group that this button belongs to. 124 * 125 * When setting, the name of the group should be specified. 126 * 127 * When retrieving, the Node for the toolbar group is returned. If 128 * the group doesn't exist yet, then it is created first. 129 * 130 * @attribute group 131 * @type Node 132 * @writeOnce 133 */ 134 group: { 135 writeOnce: true, 136 getter: function(groupName) { 137 var group = this.toolbar.one(GROUPSELECTOR + groupName + GROUP); 138 if (!group) { 139 group = Y.Node.create('<div class="atto_group ' + 140 groupName + GROUP + '"></div>'); 141 this.toolbar.append(group); 142 } 143 144 return group; 145 } 146 } 147 } 148 }); 149 150 Y.namespace('M.editor_atto').EditorPlugin = EditorPlugin; 151 // This file is part of Moodle - http://moodle.org/ 152 // 153 // Moodle is free software: you can redistribute it and/or modify 154 // it under the terms of the GNU General Public License as published by 155 // the Free Software Foundation, either version 3 of the License, or 156 // (at your option) any later version. 157 // 158 // Moodle is distributed in the hope that it will be useful, 159 // but WITHOUT ANY WARRANTY; without even the implied warranty of 160 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 161 // GNU General Public License for more details. 162 // 163 // You should have received a copy of the GNU General Public License 164 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 165 /* global YUI */ 166 167 /** 168 * @module moodle-editor_atto-plugin 169 * @submodule buttons 170 */ 171 172 /** 173 * Button functions for an Atto Plugin. 174 * 175 * See {{#crossLink "M.editor_atto.EditorPlugin"}}{{/crossLink}} for details. 176 * 177 * @namespace M.editor_atto 178 * @class EditorPluginButtons 179 */ 180 181 var MENUTEMPLATE = '' + 182 '<button class="{{buttonClass}} atto_hasmenu" ' + 183 'tabindex="-1" ' + 184 'type="button" ' + 185 'title="{{title}}">' + 186 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" ' + 187 'style="background-color:{{config.menuColor}};" src="{{config.iconurl}}" />' + 188 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" ' + 189 'src="{{image_url "t/expanded" "moodle"}}"/>' + 190 '</button>'; 191 192 var DISABLED = 'disabled', 193 HIGHLIGHT = 'highlight', 194 LOGNAME = 'moodle-editor_atto-editor-plugin', 195 CSS = { 196 EDITORWRAPPER: '.editor_atto_content' 197 }; 198 199 function EditorPluginButtons() {} 200 201 EditorPluginButtons.ATTRS = { 202 }; 203 204 EditorPluginButtons.prototype = { 205 /** 206 * All of the buttons that belong to this plugin instance. 207 * 208 * Buttons are stored by button name. 209 * 210 * @property buttons 211 * @type object 212 */ 213 buttons: null, 214 215 /** 216 * A list of each of the button names. 217 * 218 * @property buttonNames 219 * @type array 220 */ 221 buttonNames: null, 222 223 /** 224 * A read-only view of the current state for each button. Mappings are stored by name. 225 * 226 * Possible states are: 227 * <ul> 228 * <li>{{#crossLink "M.editor_atto.EditorPluginButtons/ENABLED:property"}}{{/crossLink}}; and</li> 229 * <li>{{#crossLink "M.editor_atto.EditorPluginButtons/DISABLED:property"}}{{/crossLink}}.</li> 230 * </ul> 231 * 232 * @property buttonStates 233 * @type object 234 */ 235 buttonStates: null, 236 237 /** 238 * The menus belonging to this plugin instance. 239 * 240 * @property menus 241 * @type object 242 */ 243 menus: null, 244 245 /** 246 * The state for a disabled button. 247 * 248 * @property DISABLED 249 * @type Number 250 * @static 251 * @value 0 252 */ 253 DISABLED: 0, 254 255 /** 256 * The state for an enabled button. 257 * 258 * @property ENABLED 259 * @type Number 260 * @static 261 * @value 1 262 */ 263 ENABLED: 1, 264 265 /** 266 * The list of Event Handlers for buttons. 267 * 268 * @property _buttonHandlers 269 * @protected 270 * @type array 271 */ 272 _buttonHandlers: null, 273 274 /** 275 * Hide handlers which are cancelled when the menu is hidden. 276 * 277 * @property _menuHideHandlers 278 * @protected 279 * @type array 280 */ 281 _menuHideHandlers: null, 282 283 /** 284 * A textual description of the primary keyboard shortcut for this 285 * plugin. 286 * 287 * This will be null if no keyboard shortcut has been registered. 288 * 289 * @property _primaryKeyboardShortcut 290 * @protected 291 * @type String 292 * @default null 293 */ 294 _primaryKeyboardShortcut: null, 295 296 /** 297 * An list of objects returned by Y.soon(). 298 * 299 * The keys will be the buttonName of the button, and the value the Y.soon() object. 300 * 301 * @property _highlightQueue 302 * @protected 303 * @type Object 304 * @default null 305 */ 306 _highlightQueue: null, 307 308 /** 309 * Add a button for this plugin to the toolbar. 310 * 311 * @method addButton 312 * @param {object} config The configuration for this button 313 * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. 314 * @param {string} [config.icon] The icon identifier. 315 * @param {string} [config.iconComponent='core'] The icon component. 316 * @param {string} [config.keys] The shortcut key that can call this plugin from the keyboard. 317 * @param {string} [config.keyDescription] An optional description for the keyboard shortcuts. 318 * If not specified, this is automatically generated based on config.keys. 319 * If multiple key bindings are supplied to config.keys, then only the first is used. 320 * If set to false, then no description is added to the title. 321 * @param {string} [config.tags] The tags that trigger this button to be highlighted. 322 * @param {boolean} [config.tagMatchRequiresAll=true] Working in combination with the tags parameter, when true 323 * every tag of the selection has to match. When false, only one match is needed. Only set this to false when 324 * necessary as it is much less efficient. 325 * See {{#crossLink "M.editor_atto.EditorSelection/selectionFilterMatches:method"}}{{/crossLink}} for more information. 326 * @param {string} [config.title=this.name] The string identifier in the plugin's language file. 327 * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if 328 * specified, in the class for the button. 329 * @param {function} config.callback A callback function to call when the button is clicked. 330 * @param {object} [config.callbackArgs] Any arguments to pass to the callback. 331 * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed. 332 * @return {Node} The Node representing the newly created button. 333 */ 334 addButton: function(config) { 335 var group = this.get('group'), 336 pluginname = this.name, 337 buttonClass = 'atto_' + pluginname + '_button', 338 button, 339 host = this.get('host'); 340 341 if (config.exec) { 342 buttonClass = buttonClass + '_' + config.exec; 343 } 344 345 if (!config.buttonName) { 346 // Set a default button name - this is used as an identifier in the button object. 347 config.buttonName = config.exec || pluginname; 348 } else { 349 buttonClass = buttonClass + '_' + config.buttonName; 350 } 351 config.buttonClass = buttonClass; 352 353 // Normalize icon configuration. 354 config = this._normalizeIcon(config); 355 356 if (!config.title) { 357 config.title = 'pluginname'; 358 } 359 var title = M.util.get_string(config.title, 'atto_' + pluginname); 360 361 // Create the actual button. 362 button = Y.Node.create('<button type="button" class="' + buttonClass + '"' + 363 'tabindex="-1">' + 364 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" src="' + 365 config.iconurl + '"/>' + 366 '</button>'); 367 button.setAttribute('title', title); 368 369 // Append it to the group. 370 group.append(button); 371 372 var currentfocus = this.toolbar.getAttribute('aria-activedescendant'); 373 if (!currentfocus) { 374 // Initially set the first button in the toolbar to be the default on keyboard focus. 375 button.setAttribute('tabindex', '0'); 376 this.toolbar.setAttribute('aria-activedescendant', button.generateID()); 377 this.get('host')._tabFocus = button; 378 } 379 380 // Normalize the callback parameters. 381 config = this._normalizeCallback(config); 382 383 // Add the standard click handler to the button. 384 this._buttonHandlers.push( 385 this.toolbar.delegate('click', config.callback, '.' + buttonClass, this) 386 ); 387 388 // Handle button click via shortcut key. 389 if (config.keys) { 390 if (typeof config.keyDescription !== 'undefined') { 391 // A keyboard shortcut description was specified - use it. 392 this._primaryKeyboardShortcut[buttonClass] = config.keyDescription; 393 } 394 this._addKeyboardListener(config.callback, config.keys, buttonClass); 395 396 if (this._primaryKeyboardShortcut[buttonClass]) { 397 // If we have a valid keyboard shortcut description, then set it with the title. 398 button.setAttribute('title', M.util.get_string('plugin_title_shortcut', 'editor_atto', { 399 title: title, 400 shortcut: this._primaryKeyboardShortcut[buttonClass] 401 })); 402 } 403 } 404 405 // Handle highlighting of the button. 406 if (config.tags) { 407 var tagMatchRequiresAll = true; 408 if (typeof config.tagMatchRequiresAll === 'boolean') { 409 tagMatchRequiresAll = config.tagMatchRequiresAll; 410 } 411 this._buttonHandlers.push( 412 host.on(['atto:selectionchanged', 'change'], function(e) { 413 if (typeof this._highlightQueue[config.buttonName] !== 'undefined') { 414 this._highlightQueue[config.buttonName].cancel(); 415 } 416 // Async the highlighting. 417 this._highlightQueue[config.buttonName] = Y.soon(Y.bind(function(e) { 418 if (host.selectionFilterMatches(config.tags, e.selectedNodes, tagMatchRequiresAll)) { 419 this.highlightButtons(config.buttonName); 420 } else { 421 this.unHighlightButtons(config.buttonName); 422 } 423 }, this, e)); 424 }, this) 425 ); 426 } 427 428 // Add the button reference to the buttons array for later reference. 429 this.buttonNames.push(config.buttonName); 430 this.buttons[config.buttonName] = button; 431 this.buttonStates[config.buttonName] = this.ENABLED; 432 return button; 433 }, 434 435 /** 436 * Add a basic button which ties into the execCommand. 437 * 438 * See {{#crossLink "M.editor_atto.EditorPluginButtons/addButton:method"}}addButton{{/crossLink}} 439 * for full details of the optional parameters. 440 * 441 * @method addBasicButton 442 * @param {object} config The button configuration 443 * @param {string} config.exec The execCommand to call on the document. 444 * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. 445 * @param {string} [config.icon] The icon identifier. 446 * @param {string} [config.iconComponent='core'] The icon component. 447 * @param {string} [config.keys] The shortcut key that can call this plugin from the keyboard. 448 * @param {string} [config.tags] The tags that trigger this button to be highlighted. 449 * @param {boolean} [config.tagMatchRequiresAll=false] Working in combination with the tags parameter, highlight 450 * this button when any match is good enough. 451 * 452 * See {{#crossLink "M.editor_atto.EditorSelection/selectionFilterMatches:method"}}{{/crossLink}} for more information. 453 * @param {string} [config.title=this.name] The string identifier in the plugin's language file. 454 * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if 455 * specified, in the class for the button. 456 * @return {Node} The Node representing the newly created button. 457 */ 458 addBasicButton: function(config) { 459 if (!config.exec) { 460 Y.log('No exec command specified. Cannot proceed.', 461 'warn', 'moodle-editor_atto-plugin'); 462 return null; 463 } 464 465 // The default icon - true for most core plugins. 466 if (!config.icon) { 467 config.icon = 'e/' + config.exec; 468 } 469 470 // The default callback. 471 config.callback = function() { 472 document.execCommand(config.exec, false, null); 473 474 // And mark the text area as updated. 475 this.markUpdated(); 476 }; 477 478 // Return the newly created button. 479 return this.addButton(config); 480 }, 481 482 /** 483 * Add a menu for this plugin to the editor toolbar. 484 * 485 * @method addToolbarMenu 486 * @param {object} config The configuration for this button 487 * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. 488 * @param {string} [config.icon] The icon identifier. 489 * @param {string} [config.iconComponent='core'] The icon component. 490 * @param {string} [config.title=this.name] The string identifier in the plugin's language file. 491 * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if 492 * specified, in the class for the button. 493 * @param {function} config.callback A callback function to call when the button is clicked. 494 * @param {object} [config.callbackArgs] Any arguments to pass to the callback. 495 * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed. 496 * @param {array} config.entries List of menu entries with the string (entry.text) and the handlers (entry.handler). 497 * @param {number} [config.overlayWidth=14] The width of the menu. This will be suffixed with the 'em' unit. 498 * @param {string} [config.menuColor] menu icon background color 499 * @return {Node} The Node representing the newly created button. 500 */ 501 addToolbarMenu: function(config) { 502 var group = this.get('group'), 503 pluginname = this.name, 504 buttonClass = 'atto_' + pluginname + '_button', 505 button, 506 currentFocus; 507 508 if (!config.buttonName) { 509 // Set a default button name - this is used as an identifier in the button object. 510 config.buttonName = pluginname; 511 } else { 512 buttonClass = buttonClass + '_' + config.buttonName; 513 } 514 config.buttonClass = buttonClass; 515 516 // Normalize icon configuration. 517 config = this._normalizeIcon(config); 518 519 if (!config.title) { 520 config.title = 'pluginname'; 521 } 522 var title = M.util.get_string(config.title, 'atto_' + pluginname); 523 524 if (!config.menuColor) { 525 config.menuColor = 'transparent'; 526 } 527 528 // Create the actual button. 529 var template = Y.Handlebars.compile(MENUTEMPLATE); 530 button = Y.Node.create(template({ 531 buttonClass: buttonClass, 532 config: config, 533 title: title 534 })); 535 536 // Append it to the group. 537 group.append(button); 538 539 currentFocus = this.toolbar.getAttribute('aria-activedescendant'); 540 if (!currentFocus) { 541 // Initially set the first button in the toolbar to be the default on keyboard focus. 542 button.setAttribute('tabindex', '0'); 543 this.toolbar.setAttribute('aria-activedescendant', button.generateID()); 544 } 545 546 // Add the standard click handler to the menu. 547 this._buttonHandlers.push( 548 this.toolbar.delegate('click', this._showToolbarMenu, '.' + buttonClass, this, config), 549 this.toolbar.delegate('key', this._showToolbarMenuAndFocus, '40, 32, enter', '.' + buttonClass, this, config) 550 ); 551 552 // Add the button reference to the buttons array for later reference. 553 this.buttonNames.push(config.buttonName); 554 this.buttons[config.buttonName] = button; 555 this.buttonStates[config.buttonName] = this.ENABLED; 556 557 return button; 558 }, 559 560 /** 561 * Display a toolbar menu. 562 * 563 * @method _showToolbarMenu 564 * @param {EventFacade} e 565 * @param {object} config The configuration for the whole toolbar. 566 * @param {Number} [config.overlayWidth=14] The width of the menu 567 * @private 568 */ 569 _showToolbarMenu: function(e, config) { 570 // Prevent default primarily to prevent arrow press changes. 571 e.preventDefault(); 572 573 if (!this.isEnabled()) { 574 // Exit early if the plugin is disabled. 575 return; 576 } 577 578 if (e.currentTarget.ancestor('button', true).hasAttribute(DISABLED)) { 579 // Exit early if the clicked button was disabled. 580 return; 581 } 582 583 var menuDialogue; 584 585 if (!this.menus[config.buttonClass]) { 586 if (!config.overlayWidth) { 587 config.overlayWidth = '14'; 588 } 589 590 if (!config.innerOverlayWidth) { 591 config.innerOverlayWidth = parseInt(config.overlayWidth, 10) - 2 + 'em'; 592 } 593 config.overlayWidth = parseInt(config.overlayWidth, 10) + 'em'; 594 595 this.menus[config.buttonClass] = new Y.M.editor_atto.Menu(config); 596 597 this.menus[config.buttonClass].get('contentBox').delegate('click', 598 this._chooseMenuItem, '.atto_menuentry a', this, config); 599 } 600 601 // Clear the focusAfterHide for any other menus which may be open. 602 Y.Array.each(this.get('host').openMenus, function(menu) { 603 menu.set('focusAfterHide', null); 604 }); 605 606 // Ensure that we focus on this button next time. 607 var creatorButton = this.buttons[config.buttonName]; 608 creatorButton.focus(); 609 this.get('host')._setTabFocus(creatorButton); 610 611 // Get a reference to the menu dialogue. 612 menuDialogue = this.menus[config.buttonClass]; 613 614 // Focus on the button by default after hiding this menu. 615 menuDialogue.set('focusAfterHide', creatorButton); 616 617 // Display the menu. 618 menuDialogue.show(); 619 620 // Position it next to the button which opened it. 621 menuDialogue.align(this.buttons[config.buttonName], [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]); 622 623 this.get('host').openMenus = [menuDialogue]; 624 }, 625 626 /** 627 * Display a toolbar menu and focus upon the first item. 628 * 629 * @method _showToolbarMenuAndFocus 630 * @param {EventFacade} e 631 * @param {object} config The configuration for the whole toolbar. 632 * @param {Number} [config.overlayWidth=14] The width of the menu 633 * @private 634 */ 635 _showToolbarMenuAndFocus: function(e, config) { 636 this._showToolbarMenu(e, config); 637 638 // Focus on the first element in the menu. 639 this.menus[config.buttonClass].get('boundingBox').one('a').focus(); 640 }, 641 642 /** 643 * Select a menu item and call the appropriate callbacks. 644 * 645 * @method _chooseMenuItem 646 * @param {EventFacade} e 647 * @param {object} config 648 * @param {M.core.dialogue} menuDialogue The Dialogue to hide. 649 * @private 650 */ 651 _chooseMenuItem: function(e, config, menuDialogue) { 652 // Get the index from the clicked anchor. 653 var index = e.target.ancestor('a', true).getData('index'), 654 655 // And the normalized callback configuration. 656 buttonConfig = this._normalizeCallback(config.items[index], config.globalItemConfig); 657 658 menuDialogue = this.menus[config.buttonClass]; 659 660 // Prevent the dialogue to be closed because of some browser weirdness. 661 menuDialogue.set('preventHideMenu', true); 662 663 // Call the callback for this button. 664 buttonConfig.callback(e, buttonConfig._callback, buttonConfig.callbackArgs); 665 666 // Cancel the hide menu prevention. 667 menuDialogue.set('preventHideMenu', false); 668 669 // Set the focus after hide so that focus is returned to the editor and changes are made correctly. 670 menuDialogue.set('focusAfterHide', this.get('host').editor); 671 menuDialogue.hide(e); 672 }, 673 674 /** 675 * Normalize and sanitize the configuration variables relating to callbacks. 676 * 677 * @method _normalizeCallback 678 * @param {object} config 679 * @param {function} config.callback A callback function to call when the button is clicked. 680 * @param {object} [config.callbackArgs] Any arguments to pass to the callback. 681 * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed. 682 * @param {object} [inheritFrom] A parent configuration that this configuration may inherit from. 683 * @return {object} The normalized configuration 684 * @private 685 */ 686 _normalizeCallback: function(config, inheritFrom) { 687 if (config._callbackNormalized) { 688 // Return early if the callback has already been normalized. 689 return config; 690 } 691 692 if (!inheritFrom) { 693 // Create an empty inheritFrom to make life easier below. 694 inheritFrom = {}; 695 } 696 697 698 // First we wrap the callback in function to handle formating of text inserted into collapsed selection. 699 config.inlineFormat = config.inlineFormat || inheritFrom.inlineFormat; 700 config._inlineCallback = config.callback || inheritFrom.callback; 701 config._callback = config.callback || inheritFrom.callback; 702 if (config.inlineFormat && typeof config._inlineCallback === 'function') { 703 config._callback = function(e, args) { 704 this.get('host').applyFormat(e, config._inlineCallback, this, args); 705 }; 706 } 707 // We wrap the callback in function to prevent the default action, check whether the editor is 708 // active and focus it, and then mark the field as updated. 709 config.callback = Y.rbind(this._callbackWrapper, this, config._callback, config.callbackArgs); 710 711 config._callbackNormalized = true; 712 713 return config; 714 }, 715 716 /** 717 * Normalize and sanitize the configuration variables relating to icons. 718 * 719 * @method _normalizeIcon 720 * @param {object} config 721 * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. 722 * @param {string} [config.icon] The icon identifier. 723 * @param {string} [config.iconComponent='core'] The icon component. 724 * @return {object} The normalized configuration 725 * @private 726 */ 727 _normalizeIcon: function(config) { 728 if (!config.iconurl) { 729 // The default icon component. 730 if (!config.iconComponent) { 731 config.iconComponent = 'core'; 732 } 733 config.iconurl = M.util.image_url(config.icon, config.iconComponent); 734 } 735 736 return config; 737 }, 738 739 /** 740 * A wrapper in which to run the callbacks. 741 * 742 * This handles common functionality such as: 743 * <ul> 744 * <li>preventing the default action; and</li> 745 * <li>focusing the editor if relevant.</li> 746 * </ul> 747 * 748 * @method _callbackWrapper 749 * @param {EventFacade} e 750 * @param {Function} callback The function to call which makes the relevant changes. 751 * @param {Array} [callbackArgs] The arguments passed to this callback. 752 * @return {Mixed} The value returned by the callback. 753 * @private 754 */ 755 _callbackWrapper: function(e, callback, callbackArgs) { 756 e.preventDefault(); 757 758 if (!this.isEnabled()) { 759 // Exit early if the plugin is disabled. 760 return; 761 } 762 763 var creatorButton = e.currentTarget.ancestor('button', true); 764 765 if (creatorButton && creatorButton.hasAttribute(DISABLED)) { 766 // Exit early if the clicked button was disabled. 767 return; 768 } 769 770 if (!(YUI.Env.UA.android || this.get('host').isActive())) { 771 // We must not focus for Android here, even if the editor is not active because the keyboard auto-completion 772 // changes the cursor position. 773 // If we save that change, then when we restore the change later we get put in the wrong place. 774 // Android is fine to save the selection without the editor being in focus. 775 this.get('host').focus(); 776 } 777 778 // Save the selection. 779 this.get('host').saveSelection(); 780 781 // Ensure that we focus on this button next time. 782 if (creatorButton) { 783 this.get('host')._setTabFocus(creatorButton); 784 } 785 786 // Build the arguments list, but remove the callback we're calling. 787 var args = [e, callbackArgs]; 788 789 // Restore selection before making changes. 790 this.get('host').restoreSelection(); 791 792 // Actually call the callback now. 793 return callback.apply(this, args); 794 }, 795 796 /** 797 * Add a keyboard listener to call the callback. 798 * 799 * The keyConfig will take either an array of keyConfigurations, in 800 * which case _addKeyboardListener is called multiple times; an object 801 * containing an optional eventtype, optional container, and a set of 802 * keyCodes, or just a string containing the keyCodes. When keyConfig is 803 * not an object, it is wrapped around a function that ensures that 804 * only the expected key modifiers were used. For instance, it checks 805 * that space+ctrl is not triggered when the user presses ctrl+shift+space. 806 * When using an object, the developer should check that manually. 807 * 808 * @method _addKeyboardListener 809 * @param {function} callback 810 * @param {array|object|string} keyConfig 811 * @param {string} [keyConfig.eventtype=key] The type of event 812 * @param {string} [keyConfig.container=.editor_atto_content] The containing element. 813 * @param {string} keyConfig.keyCodes The keycodes to user for the event. 814 * @private 815 * 816 */ 817 _addKeyboardListener: function(callback, keyConfig, buttonName) { 818 var eventtype = 'key', 819 container = CSS.EDITORWRAPPER, 820 keys, 821 handler, 822 modifier; 823 824 if (Y.Lang.isArray(keyConfig)) { 825 // If an Array was specified, call the add function for each element. 826 Y.Array.each(keyConfig, function(config) { 827 this._addKeyboardListener(callback, config); 828 }, this); 829 830 return this; 831 832 } else if (typeof keyConfig === "object") { 833 if (keyConfig.eventtype) { 834 eventtype = keyConfig.eventtype; 835 } 836 837 if (keyConfig.container) { 838 container = keyConfig.container; 839 } 840 841 // Must be specified. 842 keys = keyConfig.keyCodes; 843 handler = callback; 844 845 } else { 846 modifier = this._getDefaultMetaKey(); 847 keys = this._getKeyEvent() + keyConfig + '+' + modifier; 848 if (typeof this._primaryKeyboardShortcut[buttonName] === 'undefined') { 849 this._primaryKeyboardShortcut[buttonName] = this._getDefaultMetaKeyDescription(keyConfig); 850 } 851 852 // Wrap the callback into a handler to check if it uses the specified modifiers, not more. 853 handler = Y.bind(function(modifiers, e) { 854 if (this._eventUsesExactKeyModifiers(modifiers, e)) { 855 callback.apply(this, [e]); 856 } 857 }, this, [modifier]); 858 } 859 860 this._buttonHandlers.push( 861 this.editor.delegate( 862 eventtype, 863 handler, 864 keys, 865 container, 866 this 867 ) 868 ); 869 870 Y.log('Atto shortcut registered: ' + keys + ' now triggers for ' + buttonName, 871 'debug', LOGNAME); 872 }, 873 874 /** 875 * Checks if a key event was strictly defined for the modifiers passed. 876 * 877 * @method _eventUsesExactKeyModifiers 878 * @param {Array} modifiers List of key modifiers to check for (alt, ctrl, meta or shift). 879 * @param {EventFacade} e The event facade. 880 * @return {Boolean} True if the event was stricly using the modifiers specified. 881 */ 882 _eventUsesExactKeyModifiers: function(modifiers, e) { 883 var exactMatch = true, 884 hasKey; 885 886 if (e.type !== 'key') { 887 return false; 888 } 889 890 hasKey = Y.Array.indexOf(modifiers, 'alt') > -1; 891 exactMatch = exactMatch && ((e.altKey && hasKey) || (!e.altKey && !hasKey)); 892 hasKey = Y.Array.indexOf(modifiers, 'ctrl') > -1; 893 exactMatch = exactMatch && ((e.ctrlKey && hasKey) || (!e.ctrlKey && !hasKey)); 894 hasKey = Y.Array.indexOf(modifiers, 'meta') > -1; 895 exactMatch = exactMatch && ((e.metaKey && hasKey) || (!e.metaKey && !hasKey)); 896 hasKey = Y.Array.indexOf(modifiers, 'shift') > -1; 897 exactMatch = exactMatch && ((e.shiftKey && hasKey) || (!e.shiftKey && !hasKey)); 898 899 return exactMatch; 900 }, 901 902 /** 903 * Determine if this plugin is enabled, based upon the state of it's buttons. 904 * 905 * @method isEnabled 906 * @return {boolean} 907 */ 908 isEnabled: function() { 909 // The first instance of an undisabled button will make this return true. 910 var found = Y.Object.some(this.buttonStates, function(button) { 911 return (button === this.ENABLED); 912 }, this); 913 914 return found; 915 }, 916 917 /** 918 * Enable one button, or all buttons relating to this Plugin. 919 * 920 * If no button is specified, all buttons are disabled. 921 * 922 * @method disableButtons 923 * @param {String} [button] The name of a specific plugin to enable. 924 * @chainable 925 */ 926 disableButtons: function(button) { 927 return this._setButtonState(false, button); 928 }, 929 930 /** 931 * Enable one button, or all buttons relating to this Plugin. 932 * 933 * If no button is specified, all buttons are enabled. 934 * 935 * @method enableButtons 936 * @param {String} [button] The name of a specific plugin to enable. 937 * @chainable 938 */ 939 enableButtons: function(button) { 940 return this._setButtonState(true, button); 941 }, 942 943 /** 944 * Set the button state for one button, or all buttons associated with this plugin. 945 * 946 * @method _setButtonState 947 * @param {Boolean} enable Whether to enable this button. 948 * @param {String} [button] The name of a specific plugin to set state for. 949 * @chainable 950 * @private 951 */ 952 _setButtonState: function(enable, button) { 953 var attributeChange = 'setAttribute'; 954 if (enable) { 955 attributeChange = 'removeAttribute'; 956 } 957 if (button) { 958 if (this.buttons[button]) { 959 this.buttons[button][attributeChange](DISABLED, DISABLED); 960 this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED; 961 } 962 } else { 963 Y.Array.each(this.buttonNames, function(button) { 964 this.buttons[button][attributeChange](DISABLED, DISABLED); 965 this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED; 966 }, this); 967 } 968 969 this.get('host').checkTabFocus(); 970 return this; 971 }, 972 973 /** 974 * Highlight a button, or buttons in the toolbar. 975 * 976 * If no button is specified, all buttons are highlighted. 977 * 978 * @method highlightButtons 979 * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. 980 * @chainable 981 */ 982 highlightButtons: function(button) { 983 return this._changeButtonHighlight(true, button); 984 }, 985 986 /** 987 * Un-highlight a button, or buttons in the toolbar. 988 * 989 * If no button is specified, all buttons are un-highlighted. 990 * 991 * @method unHighlightButtons 992 * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. 993 * @chainable 994 */ 995 unHighlightButtons: function(button) { 996 return this._changeButtonHighlight(false, button); 997 }, 998 999 /** 1000 * Highlight a button, or buttons in the toolbar. 1001 * 1002 * @method _changeButtonHighlight 1003 * @param {boolean} highlight true 1004 * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. 1005 * @protected 1006 * @chainable 1007 */ 1008 _changeButtonHighlight: function(highlight, button) { 1009 var method = 'addClass'; 1010 1011 if (!highlight) { 1012 method = 'removeClass'; 1013 } 1014 if (button) { 1015 if (this.buttons[button]) { 1016 this.buttons[button][method](HIGHLIGHT); 1017 } 1018 } else { 1019 Y.Object.each(this.buttons, function(button) { 1020 button[method](HIGHLIGHT); 1021 }, this); 1022 } 1023 1024 return this; 1025 }, 1026 1027 /** 1028 * Get the default meta key to use with keyboard events. 1029 * 1030 * On a Mac, this will be the 'meta' key for Command; otherwise it will 1031 * be the Control key. 1032 * 1033 * @method _getDefaultMetaKey 1034 * @return {string} 1035 * @private 1036 */ 1037 _getDefaultMetaKey: function() { 1038 if (Y.UA.os === 'macintosh') { 1039 return 'meta'; 1040 } else { 1041 return 'ctrl'; 1042 } 1043 }, 1044 1045 /** 1046 * Get the user-visible description of the meta key to use with keyboard events. 1047 * 1048 * On a Mac, this will be 'Command' ; otherwise it will be 'Control'. 1049 * 1050 * @method _getDefaultMetaKeyDescription 1051 * @return {string} 1052 * @private 1053 */ 1054 _getDefaultMetaKeyDescription: function(keyCode) { 1055 if (Y.UA.os === 'macintosh') { 1056 return M.util.get_string('editor_command_keycode', 'editor_atto', String.fromCharCode(keyCode).toLowerCase()); 1057 } else { 1058 return M.util.get_string('editor_control_keycode', 'editor_atto', String.fromCharCode(keyCode).toLowerCase()); 1059 } 1060 }, 1061 1062 /** 1063 * Get the standard key event to use for keyboard events. 1064 * 1065 * @method _getKeyEvent 1066 * @return {string} 1067 * @private 1068 */ 1069 _getKeyEvent: function() { 1070 return 'down:'; 1071 } 1072 }; 1073 1074 Y.Base.mix(Y.M.editor_atto.EditorPlugin, [EditorPluginButtons]); 1075 // This file is part of Moodle - http://moodle.org/ 1076 // 1077 // Moodle is free software: you can redistribute it and/or modify 1078 // it under the terms of the GNU General Public License as published by 1079 // the Free Software Foundation, either version 3 of the License, or 1080 // (at your option) any later version. 1081 // 1082 // Moodle is distributed in the hope that it will be useful, 1083 // but WITHOUT ANY WARRANTY; without even the implied warranty of 1084 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1085 // GNU General Public License for more details. 1086 // 1087 // You should have received a copy of the GNU General Public License 1088 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 1089 1090 /** 1091 * @module moodle-editor_atto-plugin 1092 * @submodule dialogue 1093 */ 1094 1095 /** 1096 * Dialogue functions for an Atto Plugin. 1097 * 1098 * See {{#crossLink "M.editor_atto.EditorPlugin"}}{{/crossLink}} for details. 1099 * 1100 * @namespace M.editor_atto 1101 * @class EditorPluginDialogue 1102 */ 1103 1104 function EditorPluginDialogue() {} 1105 1106 EditorPluginDialogue.ATTRS = { 1107 }; 1108 1109 EditorPluginDialogue.prototype = { 1110 /** 1111 * A reference to the instantiated dialogue. 1112 * 1113 * @property _dialogue 1114 * @private 1115 * @type M.core.Dialogue 1116 */ 1117 _dialogue: null, 1118 1119 /** 1120 * Fetch the instantiated dialogue. If a dialogue has not yet been created, instantiate one. 1121 * 1122 * <em><b>Note:</b> Only one dialogue is supported through this interface.</em> 1123 * 1124 * For a full list of options, see documentation for {{#crossLink "M.core.dialogue"}}{{/crossLink}}. 1125 * 1126 * A sensible default is provided for the focusAfterHide attribute. 1127 * 1128 * @method getDialogue 1129 * @param {object} config 1130 * @param {boolean|string|Node} [config.focusAfterHide=undefined] Set the focusAfterHide setting to the 1131 * specified Node according to the following values: 1132 * <ul> 1133 * <li>If true was passed, the first button for this plugin will be used instead; or</li> 1134 * <li>If a String was passed, the named button for this plugin will be used instead; or</li> 1135 * <li>If a Node was passed, that Node will be used instead.</li> 1136 * 1137 * This setting is checked each time that getDialogue is called. 1138 * 1139 * @return {M.core.dialogue} 1140 */ 1141 getDialogue: function(config) { 1142 // Config is an optional param - define a default. 1143 config = config || {}; 1144 1145 var focusAfterHide = false; 1146 if (config.focusAfterHide) { 1147 // Remove the focusAfterHide because we may pass it a non-node value. 1148 focusAfterHide = config.focusAfterHide; 1149 delete config.focusAfterHide; 1150 } 1151 1152 if (!this._dialogue) { 1153 // Merge the default configuration with any provided configuration. 1154 var dialogueConfig = Y.merge({ 1155 visible: false, 1156 modal: true, 1157 close: true, 1158 draggable: true 1159 }, config); 1160 1161 // Instantiate the dialogue. 1162 this._dialogue = new M.core.dialogue(dialogueConfig); 1163 } 1164 1165 if (focusAfterHide !== false) { 1166 if (focusAfterHide === true) { 1167 this._dialogue.set('focusAfterHide', this.buttons[this.buttonNames[0]]); 1168 1169 } else if (typeof focusAfterHide === 'string') { 1170 this._dialogue.set('focusAfterHide', this.buttons[focusAfterHide]); 1171 1172 } else { 1173 this._dialogue.set('focusAfterHide', focusAfterHide); 1174 1175 } 1176 } 1177 1178 return this._dialogue; 1179 } 1180 }; 1181 1182 Y.Base.mix(Y.M.editor_atto.EditorPlugin, [EditorPluginDialogue]); 1183 1184 1185 }, '@VERSION@', { 1186 "requires": [ 1187 "node", 1188 "base", 1189 "escape", 1190 "event", 1191 "event-outside", 1192 "handlebars", 1193 "event-custom", 1194 "timers", 1195 "moodle-editor_atto-menu" 1196 ] 1197 });
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 |