[ 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 /* global YUI */ 16 17 /** 18 * @module moodle-editor_atto-plugin 19 * @submodule buttons 20 */ 21 22 /** 23 * Button functions for an Atto Plugin. 24 * 25 * See {{#crossLink "M.editor_atto.EditorPlugin"}}{{/crossLink}} for details. 26 * 27 * @namespace M.editor_atto 28 * @class EditorPluginButtons 29 */ 30 31 var MENUTEMPLATE = '' + 32 '<button class="{{buttonClass}} atto_hasmenu" ' + 33 'tabindex="-1" ' + 34 'type="button" ' + 35 'title="{{title}}">' + 36 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" ' + 37 'style="background-color:{{config.menuColor}};" src="{{config.iconurl}}" />' + 38 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" ' + 39 'src="{{image_url "t/expanded" "moodle"}}"/>' + 40 '</button>'; 41 42 var DISABLED = 'disabled', 43 HIGHLIGHT = 'highlight', 44 LOGNAME = 'moodle-editor_atto-editor-plugin', 45 CSS = { 46 EDITORWRAPPER: '.editor_atto_content' 47 }; 48 49 function EditorPluginButtons() {} 50 51 EditorPluginButtons.ATTRS = { 52 }; 53 54 EditorPluginButtons.prototype = { 55 /** 56 * All of the buttons that belong to this plugin instance. 57 * 58 * Buttons are stored by button name. 59 * 60 * @property buttons 61 * @type object 62 */ 63 buttons: null, 64 65 /** 66 * A list of each of the button names. 67 * 68 * @property buttonNames 69 * @type array 70 */ 71 buttonNames: null, 72 73 /** 74 * A read-only view of the current state for each button. Mappings are stored by name. 75 * 76 * Possible states are: 77 * <ul> 78 * <li>{{#crossLink "M.editor_atto.EditorPluginButtons/ENABLED:property"}}{{/crossLink}}; and</li> 79 * <li>{{#crossLink "M.editor_atto.EditorPluginButtons/DISABLED:property"}}{{/crossLink}}.</li> 80 * </ul> 81 * 82 * @property buttonStates 83 * @type object 84 */ 85 buttonStates: null, 86 87 /** 88 * The menus belonging to this plugin instance. 89 * 90 * @property menus 91 * @type object 92 */ 93 menus: null, 94 95 /** 96 * The state for a disabled button. 97 * 98 * @property DISABLED 99 * @type Number 100 * @static 101 * @value 0 102 */ 103 DISABLED: 0, 104 105 /** 106 * The state for an enabled button. 107 * 108 * @property ENABLED 109 * @type Number 110 * @static 111 * @value 1 112 */ 113 ENABLED: 1, 114 115 /** 116 * The list of Event Handlers for buttons. 117 * 118 * @property _buttonHandlers 119 * @protected 120 * @type array 121 */ 122 _buttonHandlers: null, 123 124 /** 125 * Hide handlers which are cancelled when the menu is hidden. 126 * 127 * @property _menuHideHandlers 128 * @protected 129 * @type array 130 */ 131 _menuHideHandlers: null, 132 133 /** 134 * A textual description of the primary keyboard shortcut for this 135 * plugin. 136 * 137 * This will be null if no keyboard shortcut has been registered. 138 * 139 * @property _primaryKeyboardShortcut 140 * @protected 141 * @type String 142 * @default null 143 */ 144 _primaryKeyboardShortcut: null, 145 146 /** 147 * An list of objects returned by Y.soon(). 148 * 149 * The keys will be the buttonName of the button, and the value the Y.soon() object. 150 * 151 * @property _highlightQueue 152 * @protected 153 * @type Object 154 * @default null 155 */ 156 _highlightQueue: null, 157 158 /** 159 * Add a button for this plugin to the toolbar. 160 * 161 * @method addButton 162 * @param {object} config The configuration for this button 163 * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. 164 * @param {string} [config.icon] The icon identifier. 165 * @param {string} [config.iconComponent='core'] The icon component. 166 * @param {string} [config.keys] The shortcut key that can call this plugin from the keyboard. 167 * @param {string} [config.keyDescription] An optional description for the keyboard shortcuts. 168 * If not specified, this is automatically generated based on config.keys. 169 * If multiple key bindings are supplied to config.keys, then only the first is used. 170 * If set to false, then no description is added to the title. 171 * @param {string} [config.tags] The tags that trigger this button to be highlighted. 172 * @param {boolean} [config.tagMatchRequiresAll=true] Working in combination with the tags parameter, when true 173 * every tag of the selection has to match. When false, only one match is needed. Only set this to false when 174 * necessary as it is much less efficient. 175 * See {{#crossLink "M.editor_atto.EditorSelection/selectionFilterMatches:method"}}{{/crossLink}} for more information. 176 * @param {string} [config.title=this.name] The string identifier in the plugin's language file. 177 * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if 178 * specified, in the class for the button. 179 * @param {function} config.callback A callback function to call when the button is clicked. 180 * @param {object} [config.callbackArgs] Any arguments to pass to the callback. 181 * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed. 182 * @return {Node} The Node representing the newly created button. 183 */ 184 addButton: function(config) { 185 var group = this.get('group'), 186 pluginname = this.name, 187 buttonClass = 'atto_' + pluginname + '_button', 188 button, 189 host = this.get('host'); 190 191 if (config.exec) { 192 buttonClass = buttonClass + '_' + config.exec; 193 } 194 195 if (!config.buttonName) { 196 // Set a default button name - this is used as an identifier in the button object. 197 config.buttonName = config.exec || pluginname; 198 } else { 199 buttonClass = buttonClass + '_' + config.buttonName; 200 } 201 config.buttonClass = buttonClass; 202 203 // Normalize icon configuration. 204 config = this._normalizeIcon(config); 205 206 if (!config.title) { 207 config.title = 'pluginname'; 208 } 209 var title = M.util.get_string(config.title, 'atto_' + pluginname); 210 211 // Create the actual button. 212 button = Y.Node.create('<button type="button" class="' + buttonClass + '"' + 213 'tabindex="-1">' + 214 '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" src="' + 215 config.iconurl + '"/>' + 216 '</button>'); 217 button.setAttribute('title', title); 218 219 // Append it to the group. 220 group.append(button); 221 222 var currentfocus = this.toolbar.getAttribute('aria-activedescendant'); 223 if (!currentfocus) { 224 // Initially set the first button in the toolbar to be the default on keyboard focus. 225 button.setAttribute('tabindex', '0'); 226 this.toolbar.setAttribute('aria-activedescendant', button.generateID()); 227 this.get('host')._tabFocus = button; 228 } 229 230 // Normalize the callback parameters. 231 config = this._normalizeCallback(config); 232 233 // Add the standard click handler to the button. 234 this._buttonHandlers.push( 235 this.toolbar.delegate('click', config.callback, '.' + buttonClass, this) 236 ); 237 238 // Handle button click via shortcut key. 239 if (config.keys) { 240 if (typeof config.keyDescription !== 'undefined') { 241 // A keyboard shortcut description was specified - use it. 242 this._primaryKeyboardShortcut[buttonClass] = config.keyDescription; 243 } 244 this._addKeyboardListener(config.callback, config.keys, buttonClass); 245 246 if (this._primaryKeyboardShortcut[buttonClass]) { 247 // If we have a valid keyboard shortcut description, then set it with the title. 248 button.setAttribute('title', M.util.get_string('plugin_title_shortcut', 'editor_atto', { 249 title: title, 250 shortcut: this._primaryKeyboardShortcut[buttonClass] 251 })); 252 } 253 } 254 255 // Handle highlighting of the button. 256 if (config.tags) { 257 var tagMatchRequiresAll = true; 258 if (typeof config.tagMatchRequiresAll === 'boolean') { 259 tagMatchRequiresAll = config.tagMatchRequiresAll; 260 } 261 this._buttonHandlers.push( 262 host.on(['atto:selectionchanged', 'change'], function(e) { 263 if (typeof this._highlightQueue[config.buttonName] !== 'undefined') { 264 this._highlightQueue[config.buttonName].cancel(); 265 } 266 // Async the highlighting. 267 this._highlightQueue[config.buttonName] = Y.soon(Y.bind(function(e) { 268 if (host.selectionFilterMatches(config.tags, e.selectedNodes, tagMatchRequiresAll)) { 269 this.highlightButtons(config.buttonName); 270 } else { 271 this.unHighlightButtons(config.buttonName); 272 } 273 }, this, e)); 274 }, this) 275 ); 276 } 277 278 // Add the button reference to the buttons array for later reference. 279 this.buttonNames.push(config.buttonName); 280 this.buttons[config.buttonName] = button; 281 this.buttonStates[config.buttonName] = this.ENABLED; 282 return button; 283 }, 284 285 /** 286 * Add a basic button which ties into the execCommand. 287 * 288 * See {{#crossLink "M.editor_atto.EditorPluginButtons/addButton:method"}}addButton{{/crossLink}} 289 * for full details of the optional parameters. 290 * 291 * @method addBasicButton 292 * @param {object} config The button configuration 293 * @param {string} config.exec The execCommand to call on the document. 294 * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. 295 * @param {string} [config.icon] The icon identifier. 296 * @param {string} [config.iconComponent='core'] The icon component. 297 * @param {string} [config.keys] The shortcut key that can call this plugin from the keyboard. 298 * @param {string} [config.tags] The tags that trigger this button to be highlighted. 299 * @param {boolean} [config.tagMatchRequiresAll=false] Working in combination with the tags parameter, highlight 300 * this button when any match is good enough. 301 * 302 * See {{#crossLink "M.editor_atto.EditorSelection/selectionFilterMatches:method"}}{{/crossLink}} for more information. 303 * @param {string} [config.title=this.name] The string identifier in the plugin's language file. 304 * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if 305 * specified, in the class for the button. 306 * @return {Node} The Node representing the newly created button. 307 */ 308 addBasicButton: function(config) { 309 if (!config.exec) { 310 Y.log('No exec command specified. Cannot proceed.', 311 'warn', 'moodle-editor_atto-plugin'); 312 return null; 313 } 314 315 // The default icon - true for most core plugins. 316 if (!config.icon) { 317 config.icon = 'e/' + config.exec; 318 } 319 320 // The default callback. 321 config.callback = function() { 322 document.execCommand(config.exec, false, null); 323 324 // And mark the text area as updated. 325 this.markUpdated(); 326 }; 327 328 // Return the newly created button. 329 return this.addButton(config); 330 }, 331 332 /** 333 * Add a menu for this plugin to the editor toolbar. 334 * 335 * @method addToolbarMenu 336 * @param {object} config The configuration for this button 337 * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. 338 * @param {string} [config.icon] The icon identifier. 339 * @param {string} [config.iconComponent='core'] The icon component. 340 * @param {string} [config.title=this.name] The string identifier in the plugin's language file. 341 * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if 342 * specified, in the class for the button. 343 * @param {function} config.callback A callback function to call when the button is clicked. 344 * @param {object} [config.callbackArgs] Any arguments to pass to the callback. 345 * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed. 346 * @param {array} config.entries List of menu entries with the string (entry.text) and the handlers (entry.handler). 347 * @param {number} [config.overlayWidth=14] The width of the menu. This will be suffixed with the 'em' unit. 348 * @param {string} [config.menuColor] menu icon background color 349 * @return {Node} The Node representing the newly created button. 350 */ 351 addToolbarMenu: function(config) { 352 var group = this.get('group'), 353 pluginname = this.name, 354 buttonClass = 'atto_' + pluginname + '_button', 355 button, 356 currentFocus; 357 358 if (!config.buttonName) { 359 // Set a default button name - this is used as an identifier in the button object. 360 config.buttonName = pluginname; 361 } else { 362 buttonClass = buttonClass + '_' + config.buttonName; 363 } 364 config.buttonClass = buttonClass; 365 366 // Normalize icon configuration. 367 config = this._normalizeIcon(config); 368 369 if (!config.title) { 370 config.title = 'pluginname'; 371 } 372 var title = M.util.get_string(config.title, 'atto_' + pluginname); 373 374 if (!config.menuColor) { 375 config.menuColor = 'transparent'; 376 } 377 378 // Create the actual button. 379 var template = Y.Handlebars.compile(MENUTEMPLATE); 380 button = Y.Node.create(template({ 381 buttonClass: buttonClass, 382 config: config, 383 title: title 384 })); 385 386 // Append it to the group. 387 group.append(button); 388 389 currentFocus = this.toolbar.getAttribute('aria-activedescendant'); 390 if (!currentFocus) { 391 // Initially set the first button in the toolbar to be the default on keyboard focus. 392 button.setAttribute('tabindex', '0'); 393 this.toolbar.setAttribute('aria-activedescendant', button.generateID()); 394 } 395 396 // Add the standard click handler to the menu. 397 this._buttonHandlers.push( 398 this.toolbar.delegate('click', this._showToolbarMenu, '.' + buttonClass, this, config), 399 this.toolbar.delegate('key', this._showToolbarMenuAndFocus, '40, 32, enter', '.' + buttonClass, this, config) 400 ); 401 402 // Add the button reference to the buttons array for later reference. 403 this.buttonNames.push(config.buttonName); 404 this.buttons[config.buttonName] = button; 405 this.buttonStates[config.buttonName] = this.ENABLED; 406 407 return button; 408 }, 409 410 /** 411 * Display a toolbar menu. 412 * 413 * @method _showToolbarMenu 414 * @param {EventFacade} e 415 * @param {object} config The configuration for the whole toolbar. 416 * @param {Number} [config.overlayWidth=14] The width of the menu 417 * @private 418 */ 419 _showToolbarMenu: function(e, config) { 420 // Prevent default primarily to prevent arrow press changes. 421 e.preventDefault(); 422 423 if (!this.isEnabled()) { 424 // Exit early if the plugin is disabled. 425 return; 426 } 427 428 if (e.currentTarget.ancestor('button', true).hasAttribute(DISABLED)) { 429 // Exit early if the clicked button was disabled. 430 return; 431 } 432 433 var menuDialogue; 434 435 if (!this.menus[config.buttonClass]) { 436 if (!config.overlayWidth) { 437 config.overlayWidth = '14'; 438 } 439 440 if (!config.innerOverlayWidth) { 441 config.innerOverlayWidth = parseInt(config.overlayWidth, 10) - 2 + 'em'; 442 } 443 config.overlayWidth = parseInt(config.overlayWidth, 10) + 'em'; 444 445 this.menus[config.buttonClass] = new Y.M.editor_atto.Menu(config); 446 447 this.menus[config.buttonClass].get('contentBox').delegate('click', 448 this._chooseMenuItem, '.atto_menuentry a', this, config); 449 } 450 451 // Clear the focusAfterHide for any other menus which may be open. 452 Y.Array.each(this.get('host').openMenus, function(menu) { 453 menu.set('focusAfterHide', null); 454 }); 455 456 // Ensure that we focus on this button next time. 457 var creatorButton = this.buttons[config.buttonName]; 458 creatorButton.focus(); 459 this.get('host')._setTabFocus(creatorButton); 460 461 // Get a reference to the menu dialogue. 462 menuDialogue = this.menus[config.buttonClass]; 463 464 // Focus on the button by default after hiding this menu. 465 menuDialogue.set('focusAfterHide', creatorButton); 466 467 // Display the menu. 468 menuDialogue.show(); 469 470 // Position it next to the button which opened it. 471 menuDialogue.align(this.buttons[config.buttonName], [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]); 472 473 this.get('host').openMenus = [menuDialogue]; 474 }, 475 476 /** 477 * Display a toolbar menu and focus upon the first item. 478 * 479 * @method _showToolbarMenuAndFocus 480 * @param {EventFacade} e 481 * @param {object} config The configuration for the whole toolbar. 482 * @param {Number} [config.overlayWidth=14] The width of the menu 483 * @private 484 */ 485 _showToolbarMenuAndFocus: function(e, config) { 486 this._showToolbarMenu(e, config); 487 488 // Focus on the first element in the menu. 489 this.menus[config.buttonClass].get('boundingBox').one('a').focus(); 490 }, 491 492 /** 493 * Select a menu item and call the appropriate callbacks. 494 * 495 * @method _chooseMenuItem 496 * @param {EventFacade} e 497 * @param {object} config 498 * @param {M.core.dialogue} menuDialogue The Dialogue to hide. 499 * @private 500 */ 501 _chooseMenuItem: function(e, config, menuDialogue) { 502 // Get the index from the clicked anchor. 503 var index = e.target.ancestor('a', true).getData('index'), 504 505 // And the normalized callback configuration. 506 buttonConfig = this._normalizeCallback(config.items[index], config.globalItemConfig); 507 508 menuDialogue = this.menus[config.buttonClass]; 509 510 // Prevent the dialogue to be closed because of some browser weirdness. 511 menuDialogue.set('preventHideMenu', true); 512 513 // Call the callback for this button. 514 buttonConfig.callback(e, buttonConfig._callback, buttonConfig.callbackArgs); 515 516 // Cancel the hide menu prevention. 517 menuDialogue.set('preventHideMenu', false); 518 519 // Set the focus after hide so that focus is returned to the editor and changes are made correctly. 520 menuDialogue.set('focusAfterHide', this.get('host').editor); 521 menuDialogue.hide(e); 522 }, 523 524 /** 525 * Normalize and sanitize the configuration variables relating to callbacks. 526 * 527 * @method _normalizeCallback 528 * @param {object} config 529 * @param {function} config.callback A callback function to call when the button is clicked. 530 * @param {object} [config.callbackArgs] Any arguments to pass to the callback. 531 * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed. 532 * @param {object} [inheritFrom] A parent configuration that this configuration may inherit from. 533 * @return {object} The normalized configuration 534 * @private 535 */ 536 _normalizeCallback: function(config, inheritFrom) { 537 if (config._callbackNormalized) { 538 // Return early if the callback has already been normalized. 539 return config; 540 } 541 542 if (!inheritFrom) { 543 // Create an empty inheritFrom to make life easier below. 544 inheritFrom = {}; 545 } 546 547 548 // First we wrap the callback in function to handle formating of text inserted into collapsed selection. 549 config.inlineFormat = config.inlineFormat || inheritFrom.inlineFormat; 550 config._inlineCallback = config.callback || inheritFrom.callback; 551 config._callback = config.callback || inheritFrom.callback; 552 if (config.inlineFormat && typeof config._inlineCallback === 'function') { 553 config._callback = function(e, args) { 554 this.get('host').applyFormat(e, config._inlineCallback, this, args); 555 }; 556 } 557 // We wrap the callback in function to prevent the default action, check whether the editor is 558 // active and focus it, and then mark the field as updated. 559 config.callback = Y.rbind(this._callbackWrapper, this, config._callback, config.callbackArgs); 560 561 config._callbackNormalized = true; 562 563 return config; 564 }, 565 566 /** 567 * Normalize and sanitize the configuration variables relating to icons. 568 * 569 * @method _normalizeIcon 570 * @param {object} config 571 * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. 572 * @param {string} [config.icon] The icon identifier. 573 * @param {string} [config.iconComponent='core'] The icon component. 574 * @return {object} The normalized configuration 575 * @private 576 */ 577 _normalizeIcon: function(config) { 578 if (!config.iconurl) { 579 // The default icon component. 580 if (!config.iconComponent) { 581 config.iconComponent = 'core'; 582 } 583 config.iconurl = M.util.image_url(config.icon, config.iconComponent); 584 } 585 586 return config; 587 }, 588 589 /** 590 * A wrapper in which to run the callbacks. 591 * 592 * This handles common functionality such as: 593 * <ul> 594 * <li>preventing the default action; and</li> 595 * <li>focusing the editor if relevant.</li> 596 * </ul> 597 * 598 * @method _callbackWrapper 599 * @param {EventFacade} e 600 * @param {Function} callback The function to call which makes the relevant changes. 601 * @param {Array} [callbackArgs] The arguments passed to this callback. 602 * @return {Mixed} The value returned by the callback. 603 * @private 604 */ 605 _callbackWrapper: function(e, callback, callbackArgs) { 606 e.preventDefault(); 607 608 if (!this.isEnabled()) { 609 // Exit early if the plugin is disabled. 610 return; 611 } 612 613 var creatorButton = e.currentTarget.ancestor('button', true); 614 615 if (creatorButton && creatorButton.hasAttribute(DISABLED)) { 616 // Exit early if the clicked button was disabled. 617 return; 618 } 619 620 if (!(YUI.Env.UA.android || this.get('host').isActive())) { 621 // We must not focus for Android here, even if the editor is not active because the keyboard auto-completion 622 // changes the cursor position. 623 // If we save that change, then when we restore the change later we get put in the wrong place. 624 // Android is fine to save the selection without the editor being in focus. 625 this.get('host').focus(); 626 } 627 628 // Save the selection. 629 this.get('host').saveSelection(); 630 631 // Ensure that we focus on this button next time. 632 if (creatorButton) { 633 this.get('host')._setTabFocus(creatorButton); 634 } 635 636 // Build the arguments list, but remove the callback we're calling. 637 var args = [e, callbackArgs]; 638 639 // Restore selection before making changes. 640 this.get('host').restoreSelection(); 641 642 // Actually call the callback now. 643 return callback.apply(this, args); 644 }, 645 646 /** 647 * Add a keyboard listener to call the callback. 648 * 649 * The keyConfig will take either an array of keyConfigurations, in 650 * which case _addKeyboardListener is called multiple times; an object 651 * containing an optional eventtype, optional container, and a set of 652 * keyCodes, or just a string containing the keyCodes. When keyConfig is 653 * not an object, it is wrapped around a function that ensures that 654 * only the expected key modifiers were used. For instance, it checks 655 * that space+ctrl is not triggered when the user presses ctrl+shift+space. 656 * When using an object, the developer should check that manually. 657 * 658 * @method _addKeyboardListener 659 * @param {function} callback 660 * @param {array|object|string} keyConfig 661 * @param {string} [keyConfig.eventtype=key] The type of event 662 * @param {string} [keyConfig.container=.editor_atto_content] The containing element. 663 * @param {string} keyConfig.keyCodes The keycodes to user for the event. 664 * @private 665 * 666 */ 667 _addKeyboardListener: function(callback, keyConfig, buttonName) { 668 var eventtype = 'key', 669 container = CSS.EDITORWRAPPER, 670 keys, 671 handler, 672 modifier; 673 674 if (Y.Lang.isArray(keyConfig)) { 675 // If an Array was specified, call the add function for each element. 676 Y.Array.each(keyConfig, function(config) { 677 this._addKeyboardListener(callback, config); 678 }, this); 679 680 return this; 681 682 } else if (typeof keyConfig === "object") { 683 if (keyConfig.eventtype) { 684 eventtype = keyConfig.eventtype; 685 } 686 687 if (keyConfig.container) { 688 container = keyConfig.container; 689 } 690 691 // Must be specified. 692 keys = keyConfig.keyCodes; 693 handler = callback; 694 695 } else { 696 modifier = this._getDefaultMetaKey(); 697 keys = this._getKeyEvent() + keyConfig + '+' + modifier; 698 if (typeof this._primaryKeyboardShortcut[buttonName] === 'undefined') { 699 this._primaryKeyboardShortcut[buttonName] = this._getDefaultMetaKeyDescription(keyConfig); 700 } 701 702 // Wrap the callback into a handler to check if it uses the specified modifiers, not more. 703 handler = Y.bind(function(modifiers, e) { 704 if (this._eventUsesExactKeyModifiers(modifiers, e)) { 705 callback.apply(this, [e]); 706 } 707 }, this, [modifier]); 708 } 709 710 this._buttonHandlers.push( 711 this.editor.delegate( 712 eventtype, 713 handler, 714 keys, 715 container, 716 this 717 ) 718 ); 719 720 Y.log('Atto shortcut registered: ' + keys + ' now triggers for ' + buttonName, 721 'debug', LOGNAME); 722 }, 723 724 /** 725 * Checks if a key event was strictly defined for the modifiers passed. 726 * 727 * @method _eventUsesExactKeyModifiers 728 * @param {Array} modifiers List of key modifiers to check for (alt, ctrl, meta or shift). 729 * @param {EventFacade} e The event facade. 730 * @return {Boolean} True if the event was stricly using the modifiers specified. 731 */ 732 _eventUsesExactKeyModifiers: function(modifiers, e) { 733 var exactMatch = true, 734 hasKey; 735 736 if (e.type !== 'key') { 737 return false; 738 } 739 740 hasKey = Y.Array.indexOf(modifiers, 'alt') > -1; 741 exactMatch = exactMatch && ((e.altKey && hasKey) || (!e.altKey && !hasKey)); 742 hasKey = Y.Array.indexOf(modifiers, 'ctrl') > -1; 743 exactMatch = exactMatch && ((e.ctrlKey && hasKey) || (!e.ctrlKey && !hasKey)); 744 hasKey = Y.Array.indexOf(modifiers, 'meta') > -1; 745 exactMatch = exactMatch && ((e.metaKey && hasKey) || (!e.metaKey && !hasKey)); 746 hasKey = Y.Array.indexOf(modifiers, 'shift') > -1; 747 exactMatch = exactMatch && ((e.shiftKey && hasKey) || (!e.shiftKey && !hasKey)); 748 749 return exactMatch; 750 }, 751 752 /** 753 * Determine if this plugin is enabled, based upon the state of it's buttons. 754 * 755 * @method isEnabled 756 * @return {boolean} 757 */ 758 isEnabled: function() { 759 // The first instance of an undisabled button will make this return true. 760 var found = Y.Object.some(this.buttonStates, function(button) { 761 return (button === this.ENABLED); 762 }, this); 763 764 return found; 765 }, 766 767 /** 768 * Enable one button, or all buttons relating to this Plugin. 769 * 770 * If no button is specified, all buttons are disabled. 771 * 772 * @method disableButtons 773 * @param {String} [button] The name of a specific plugin to enable. 774 * @chainable 775 */ 776 disableButtons: function(button) { 777 return this._setButtonState(false, button); 778 }, 779 780 /** 781 * Enable one button, or all buttons relating to this Plugin. 782 * 783 * If no button is specified, all buttons are enabled. 784 * 785 * @method enableButtons 786 * @param {String} [button] The name of a specific plugin to enable. 787 * @chainable 788 */ 789 enableButtons: function(button) { 790 return this._setButtonState(true, button); 791 }, 792 793 /** 794 * Set the button state for one button, or all buttons associated with this plugin. 795 * 796 * @method _setButtonState 797 * @param {Boolean} enable Whether to enable this button. 798 * @param {String} [button] The name of a specific plugin to set state for. 799 * @chainable 800 * @private 801 */ 802 _setButtonState: function(enable, button) { 803 var attributeChange = 'setAttribute'; 804 if (enable) { 805 attributeChange = 'removeAttribute'; 806 } 807 if (button) { 808 if (this.buttons[button]) { 809 this.buttons[button][attributeChange](DISABLED, DISABLED); 810 this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED; 811 } 812 } else { 813 Y.Array.each(this.buttonNames, function(button) { 814 this.buttons[button][attributeChange](DISABLED, DISABLED); 815 this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED; 816 }, this); 817 } 818 819 this.get('host').checkTabFocus(); 820 return this; 821 }, 822 823 /** 824 * Highlight a button, or buttons in the toolbar. 825 * 826 * If no button is specified, all buttons are highlighted. 827 * 828 * @method highlightButtons 829 * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. 830 * @chainable 831 */ 832 highlightButtons: function(button) { 833 return this._changeButtonHighlight(true, button); 834 }, 835 836 /** 837 * Un-highlight a button, or buttons in the toolbar. 838 * 839 * If no button is specified, all buttons are un-highlighted. 840 * 841 * @method unHighlightButtons 842 * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. 843 * @chainable 844 */ 845 unHighlightButtons: function(button) { 846 return this._changeButtonHighlight(false, button); 847 }, 848 849 /** 850 * Highlight a button, or buttons in the toolbar. 851 * 852 * @method _changeButtonHighlight 853 * @param {boolean} highlight true 854 * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. 855 * @protected 856 * @chainable 857 */ 858 _changeButtonHighlight: function(highlight, button) { 859 var method = 'addClass'; 860 861 if (!highlight) { 862 method = 'removeClass'; 863 } 864 if (button) { 865 if (this.buttons[button]) { 866 this.buttons[button][method](HIGHLIGHT); 867 } 868 } else { 869 Y.Object.each(this.buttons, function(button) { 870 button[method](HIGHLIGHT); 871 }, this); 872 } 873 874 return this; 875 }, 876 877 /** 878 * Get the default meta key to use with keyboard events. 879 * 880 * On a Mac, this will be the 'meta' key for Command; otherwise it will 881 * be the Control key. 882 * 883 * @method _getDefaultMetaKey 884 * @return {string} 885 * @private 886 */ 887 _getDefaultMetaKey: function() { 888 if (Y.UA.os === 'macintosh') { 889 return 'meta'; 890 } else { 891 return 'ctrl'; 892 } 893 }, 894 895 /** 896 * Get the user-visible description of the meta key to use with keyboard events. 897 * 898 * On a Mac, this will be 'Command' ; otherwise it will be 'Control'. 899 * 900 * @method _getDefaultMetaKeyDescription 901 * @return {string} 902 * @private 903 */ 904 _getDefaultMetaKeyDescription: function(keyCode) { 905 if (Y.UA.os === 'macintosh') { 906 return M.util.get_string('editor_command_keycode', 'editor_atto', String.fromCharCode(keyCode).toLowerCase()); 907 } else { 908 return M.util.get_string('editor_control_keycode', 'editor_atto', String.fromCharCode(keyCode).toLowerCase()); 909 } 910 }, 911 912 /** 913 * Get the standard key event to use for keyboard events. 914 * 915 * @method _getKeyEvent 916 * @return {string} 917 * @private 918 */ 919 _getKeyEvent: function() { 920 return 'down:'; 921 } 922 }; 923 924 Y.Base.mix(Y.M.editor_atto.EditorPlugin, [EditorPluginButtons]);
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 |