[ 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 return null; 461 } 462 463 // The default icon - true for most core plugins. 464 if (!config.icon) { 465 config.icon = 'e/' + config.exec; 466 } 467 468 // The default callback. 469 config.callback = function() { 470 document.execCommand(config.exec, false, null); 471 472 // And mark the text area as updated. 473 this.markUpdated(); 474 }; 475 476 // Return the newly created button. 477 return this.addButton(config); 478 }, 479 480 /** 481 * Add a menu for this plugin to the editor toolbar. 482 * 483 * @method addToolbarMenu 484 * @param {object} config The configuration for this button 485 * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. 486 * @param {string} [config.icon] The icon identifier. 487 * @param {string} [config.iconComponent='core'] The icon component. 488 * @param {string} [config.title=this.name] The string identifier in the plugin's language file. 489 * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if 490 * specified, in the class for the button. 491 * @param {function} config.callback A callback function to call when the button is clicked. 492 * @param {object} [config.callbackArgs] Any arguments to pass to the callback. 493 * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed. 494 * @param {array} config.entries List of menu entries with the string (entry.text) and the handlers (entry.handler). 495 * @param {number} [config.overlayWidth=14] The width of the menu. This will be suffixed with the 'em' unit. 496 * @param {string} [config.menuColor] menu icon background color 497 * @return {Node} The Node representing the newly created button. 498 */ 499 addToolbarMenu: function(config) { 500 var group = this.get('group'), 501 pluginname = this.name, 502 buttonClass = 'atto_' + pluginname + '_button', 503 button, 504 currentFocus; 505 506 if (!config.buttonName) { 507 // Set a default button name - this is used as an identifier in the button object. 508 config.buttonName = pluginname; 509 } else { 510 buttonClass = buttonClass + '_' + config.buttonName; 511 } 512 config.buttonClass = buttonClass; 513 514 // Normalize icon configuration. 515 config = this._normalizeIcon(config); 516 517 if (!config.title) { 518 config.title = 'pluginname'; 519 } 520 var title = M.util.get_string(config.title, 'atto_' + pluginname); 521 522 if (!config.menuColor) { 523 config.menuColor = 'transparent'; 524 } 525 526 // Create the actual button. 527 var template = Y.Handlebars.compile(MENUTEMPLATE); 528 button = Y.Node.create(template({ 529 buttonClass: buttonClass, 530 config: config, 531 title: title 532 })); 533 534 // Append it to the group. 535 group.append(button); 536 537 currentFocus = this.toolbar.getAttribute('aria-activedescendant'); 538 if (!currentFocus) { 539 // Initially set the first button in the toolbar to be the default on keyboard focus. 540 button.setAttribute('tabindex', '0'); 541 this.toolbar.setAttribute('aria-activedescendant', button.generateID()); 542 } 543 544 // Add the standard click handler to the menu. 545 this._buttonHandlers.push( 546 this.toolbar.delegate('click', this._showToolbarMenu, '.' + buttonClass, this, config), 547 this.toolbar.delegate('key', this._showToolbarMenuAndFocus, '40, 32, enter', '.' + buttonClass, this, config) 548 ); 549 550 // Add the button reference to the buttons array for later reference. 551 this.buttonNames.push(config.buttonName); 552 this.buttons[config.buttonName] = button; 553 this.buttonStates[config.buttonName] = this.ENABLED; 554 555 return button; 556 }, 557 558 /** 559 * Display a toolbar menu. 560 * 561 * @method _showToolbarMenu 562 * @param {EventFacade} e 563 * @param {object} config The configuration for the whole toolbar. 564 * @param {Number} [config.overlayWidth=14] The width of the menu 565 * @private 566 */ 567 _showToolbarMenu: function(e, config) { 568 // Prevent default primarily to prevent arrow press changes. 569 e.preventDefault(); 570 571 if (!this.isEnabled()) { 572 // Exit early if the plugin is disabled. 573 return; 574 } 575 576 if (e.currentTarget.ancestor('button', true).hasAttribute(DISABLED)) { 577 // Exit early if the clicked button was disabled. 578 return; 579 } 580 581 var menuDialogue; 582 583 if (!this.menus[config.buttonClass]) { 584 if (!config.overlayWidth) { 585 config.overlayWidth = '14'; 586 } 587 588 if (!config.innerOverlayWidth) { 589 config.innerOverlayWidth = parseInt(config.overlayWidth, 10) - 2 + 'em'; 590 } 591 config.overlayWidth = parseInt(config.overlayWidth, 10) + 'em'; 592 593 this.menus[config.buttonClass] = new Y.M.editor_atto.Menu(config); 594 595 this.menus[config.buttonClass].get('contentBox').delegate('click', 596 this._chooseMenuItem, '.atto_menuentry a', this, config); 597 } 598 599 // Clear the focusAfterHide for any other menus which may be open. 600 Y.Array.each(this.get('host').openMenus, function(menu) { 601 menu.set('focusAfterHide', null); 602 }); 603 604 // Ensure that we focus on this button next time. 605 var creatorButton = this.buttons[config.buttonName]; 606 creatorButton.focus(); 607 this.get('host')._setTabFocus(creatorButton); 608 609 // Get a reference to the menu dialogue. 610 menuDialogue = this.menus[config.buttonClass]; 611 612 // Focus on the button by default after hiding this menu. 613 menuDialogue.set('focusAfterHide', creatorButton); 614 615 // Display the menu. 616 menuDialogue.show(); 617 618 // Position it next to the button which opened it. 619 menuDialogue.align(this.buttons[config.buttonName], [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]); 620 621 this.get('host').openMenus = [menuDialogue]; 622 }, 623 624 /** 625 * Display a toolbar menu and focus upon the first item. 626 * 627 * @method _showToolbarMenuAndFocus 628 * @param {EventFacade} e 629 * @param {object} config The configuration for the whole toolbar. 630 * @param {Number} [config.overlayWidth=14] The width of the menu 631 * @private 632 */ 633 _showToolbarMenuAndFocus: function(e, config) { 634 this._showToolbarMenu(e, config); 635 636 // Focus on the first element in the menu. 637 this.menus[config.buttonClass].get('boundingBox').one('a').focus(); 638 }, 639 640 /** 641 * Select a menu item and call the appropriate callbacks. 642 * 643 * @method _chooseMenuItem 644 * @param {EventFacade} e 645 * @param {object} config 646 * @param {M.core.dialogue} menuDialogue The Dialogue to hide. 647 * @private 648 */ 649 _chooseMenuItem: function(e, config, menuDialogue) { 650 // Get the index from the clicked anchor. 651 var index = e.target.ancestor('a', true).getData('index'), 652 653 // And the normalized callback configuration. 654 buttonConfig = this._normalizeCallback(config.items[index], config.globalItemConfig); 655 656 menuDialogue = this.menus[config.buttonClass]; 657 658 // Prevent the dialogue to be closed because of some browser weirdness. 659 menuDialogue.set('preventHideMenu', true); 660 661 // Call the callback for this button. 662 buttonConfig.callback(e, buttonConfig._callback, buttonConfig.callbackArgs); 663 664 // Cancel the hide menu prevention. 665 menuDialogue.set('preventHideMenu', false); 666 667 // Set the focus after hide so that focus is returned to the editor and changes are made correctly. 668 menuDialogue.set('focusAfterHide', this.get('host').editor); 669 menuDialogue.hide(e); 670 }, 671 672 /** 673 * Normalize and sanitize the configuration variables relating to callbacks. 674 * 675 * @method _normalizeCallback 676 * @param {object} config 677 * @param {function} config.callback A callback function to call when the button is clicked. 678 * @param {object} [config.callbackArgs] Any arguments to pass to the callback. 679 * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed. 680 * @param {object} [inheritFrom] A parent configuration that this configuration may inherit from. 681 * @return {object} The normalized configuration 682 * @private 683 */ 684 _normalizeCallback: function(config, inheritFrom) { 685 if (config._callbackNormalized) { 686 // Return early if the callback has already been normalized. 687 return config; 688 } 689 690 if (!inheritFrom) { 691 // Create an empty inheritFrom to make life easier below. 692 inheritFrom = {}; 693 } 694 695 696 // First we wrap the callback in function to handle formating of text inserted into collapsed selection. 697 config.inlineFormat = config.inlineFormat || inheritFrom.inlineFormat; 698 config._inlineCallback = config.callback || inheritFrom.callback; 699 config._callback = config.callback || inheritFrom.callback; 700 if (config.inlineFormat && typeof config._inlineCallback === 'function') { 701 config._callback = function(e, args) { 702 this.get('host').applyFormat(e, config._inlineCallback, this, args); 703 }; 704 } 705 // We wrap the callback in function to prevent the default action, check whether the editor is 706 // active and focus it, and then mark the field as updated. 707 config.callback = Y.rbind(this._callbackWrapper, this, config._callback, config.callbackArgs); 708 709 config._callbackNormalized = true; 710 711 return config; 712 }, 713 714 /** 715 * Normalize and sanitize the configuration variables relating to icons. 716 * 717 * @method _normalizeIcon 718 * @param {object} config 719 * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead. 720 * @param {string} [config.icon] The icon identifier. 721 * @param {string} [config.iconComponent='core'] The icon component. 722 * @return {object} The normalized configuration 723 * @private 724 */ 725 _normalizeIcon: function(config) { 726 if (!config.iconurl) { 727 // The default icon component. 728 if (!config.iconComponent) { 729 config.iconComponent = 'core'; 730 } 731 config.iconurl = M.util.image_url(config.icon, config.iconComponent); 732 } 733 734 return config; 735 }, 736 737 /** 738 * A wrapper in which to run the callbacks. 739 * 740 * This handles common functionality such as: 741 * <ul> 742 * <li>preventing the default action; and</li> 743 * <li>focusing the editor if relevant.</li> 744 * </ul> 745 * 746 * @method _callbackWrapper 747 * @param {EventFacade} e 748 * @param {Function} callback The function to call which makes the relevant changes. 749 * @param {Array} [callbackArgs] The arguments passed to this callback. 750 * @return {Mixed} The value returned by the callback. 751 * @private 752 */ 753 _callbackWrapper: function(e, callback, callbackArgs) { 754 e.preventDefault(); 755 756 if (!this.isEnabled()) { 757 // Exit early if the plugin is disabled. 758 return; 759 } 760 761 var creatorButton = e.currentTarget.ancestor('button', true); 762 763 if (creatorButton && creatorButton.hasAttribute(DISABLED)) { 764 // Exit early if the clicked button was disabled. 765 return; 766 } 767 768 if (!(YUI.Env.UA.android || this.get('host').isActive())) { 769 // We must not focus for Android here, even if the editor is not active because the keyboard auto-completion 770 // changes the cursor position. 771 // If we save that change, then when we restore the change later we get put in the wrong place. 772 // Android is fine to save the selection without the editor being in focus. 773 this.get('host').focus(); 774 } 775 776 // Save the selection. 777 this.get('host').saveSelection(); 778 779 // Ensure that we focus on this button next time. 780 if (creatorButton) { 781 this.get('host')._setTabFocus(creatorButton); 782 } 783 784 // Build the arguments list, but remove the callback we're calling. 785 var args = [e, callbackArgs]; 786 787 // Restore selection before making changes. 788 this.get('host').restoreSelection(); 789 790 // Actually call the callback now. 791 return callback.apply(this, args); 792 }, 793 794 /** 795 * Add a keyboard listener to call the callback. 796 * 797 * The keyConfig will take either an array of keyConfigurations, in 798 * which case _addKeyboardListener is called multiple times; an object 799 * containing an optional eventtype, optional container, and a set of 800 * keyCodes, or just a string containing the keyCodes. When keyConfig is 801 * not an object, it is wrapped around a function that ensures that 802 * only the expected key modifiers were used. For instance, it checks 803 * that space+ctrl is not triggered when the user presses ctrl+shift+space. 804 * When using an object, the developer should check that manually. 805 * 806 * @method _addKeyboardListener 807 * @param {function} callback 808 * @param {array|object|string} keyConfig 809 * @param {string} [keyConfig.eventtype=key] The type of event 810 * @param {string} [keyConfig.container=.editor_atto_content] The containing element. 811 * @param {string} keyConfig.keyCodes The keycodes to user for the event. 812 * @private 813 * 814 */ 815 _addKeyboardListener: function(callback, keyConfig, buttonName) { 816 var eventtype = 'key', 817 container = CSS.EDITORWRAPPER, 818 keys, 819 handler, 820 modifier; 821 822 if (Y.Lang.isArray(keyConfig)) { 823 // If an Array was specified, call the add function for each element. 824 Y.Array.each(keyConfig, function(config) { 825 this._addKeyboardListener(callback, config); 826 }, this); 827 828 return this; 829 830 } else if (typeof keyConfig === "object") { 831 if (keyConfig.eventtype) { 832 eventtype = keyConfig.eventtype; 833 } 834 835 if (keyConfig.container) { 836 container = keyConfig.container; 837 } 838 839 // Must be specified. 840 keys = keyConfig.keyCodes; 841 handler = callback; 842 843 } else { 844 modifier = this._getDefaultMetaKey(); 845 keys = this._getKeyEvent() + keyConfig + '+' + modifier; 846 if (typeof this._primaryKeyboardShortcut[buttonName] === 'undefined') { 847 this._primaryKeyboardShortcut[buttonName] = this._getDefaultMetaKeyDescription(keyConfig); 848 } 849 850 // Wrap the callback into a handler to check if it uses the specified modifiers, not more. 851 handler = Y.bind(function(modifiers, e) { 852 if (this._eventUsesExactKeyModifiers(modifiers, e)) { 853 callback.apply(this, [e]); 854 } 855 }, this, [modifier]); 856 } 857 858 this._buttonHandlers.push( 859 this.editor.delegate( 860 eventtype, 861 handler, 862 keys, 863 container, 864 this 865 ) 866 ); 867 868 }, 869 870 /** 871 * Checks if a key event was strictly defined for the modifiers passed. 872 * 873 * @method _eventUsesExactKeyModifiers 874 * @param {Array} modifiers List of key modifiers to check for (alt, ctrl, meta or shift). 875 * @param {EventFacade} e The event facade. 876 * @return {Boolean} True if the event was stricly using the modifiers specified. 877 */ 878 _eventUsesExactKeyModifiers: function(modifiers, e) { 879 var exactMatch = true, 880 hasKey; 881 882 if (e.type !== 'key') { 883 return false; 884 } 885 886 hasKey = Y.Array.indexOf(modifiers, 'alt') > -1; 887 exactMatch = exactMatch && ((e.altKey && hasKey) || (!e.altKey && !hasKey)); 888 hasKey = Y.Array.indexOf(modifiers, 'ctrl') > -1; 889 exactMatch = exactMatch && ((e.ctrlKey && hasKey) || (!e.ctrlKey && !hasKey)); 890 hasKey = Y.Array.indexOf(modifiers, 'meta') > -1; 891 exactMatch = exactMatch && ((e.metaKey && hasKey) || (!e.metaKey && !hasKey)); 892 hasKey = Y.Array.indexOf(modifiers, 'shift') > -1; 893 exactMatch = exactMatch && ((e.shiftKey && hasKey) || (!e.shiftKey && !hasKey)); 894 895 return exactMatch; 896 }, 897 898 /** 899 * Determine if this plugin is enabled, based upon the state of it's buttons. 900 * 901 * @method isEnabled 902 * @return {boolean} 903 */ 904 isEnabled: function() { 905 // The first instance of an undisabled button will make this return true. 906 var found = Y.Object.some(this.buttonStates, function(button) { 907 return (button === this.ENABLED); 908 }, this); 909 910 return found; 911 }, 912 913 /** 914 * Enable one button, or all buttons relating to this Plugin. 915 * 916 * If no button is specified, all buttons are disabled. 917 * 918 * @method disableButtons 919 * @param {String} [button] The name of a specific plugin to enable. 920 * @chainable 921 */ 922 disableButtons: function(button) { 923 return this._setButtonState(false, button); 924 }, 925 926 /** 927 * Enable one button, or all buttons relating to this Plugin. 928 * 929 * If no button is specified, all buttons are enabled. 930 * 931 * @method enableButtons 932 * @param {String} [button] The name of a specific plugin to enable. 933 * @chainable 934 */ 935 enableButtons: function(button) { 936 return this._setButtonState(true, button); 937 }, 938 939 /** 940 * Set the button state for one button, or all buttons associated with this plugin. 941 * 942 * @method _setButtonState 943 * @param {Boolean} enable Whether to enable this button. 944 * @param {String} [button] The name of a specific plugin to set state for. 945 * @chainable 946 * @private 947 */ 948 _setButtonState: function(enable, button) { 949 var attributeChange = 'setAttribute'; 950 if (enable) { 951 attributeChange = 'removeAttribute'; 952 } 953 if (button) { 954 if (this.buttons[button]) { 955 this.buttons[button][attributeChange](DISABLED, DISABLED); 956 this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED; 957 } 958 } else { 959 Y.Array.each(this.buttonNames, function(button) { 960 this.buttons[button][attributeChange](DISABLED, DISABLED); 961 this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED; 962 }, this); 963 } 964 965 this.get('host').checkTabFocus(); 966 return this; 967 }, 968 969 /** 970 * Highlight a button, or buttons in the toolbar. 971 * 972 * If no button is specified, all buttons are highlighted. 973 * 974 * @method highlightButtons 975 * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. 976 * @chainable 977 */ 978 highlightButtons: function(button) { 979 return this._changeButtonHighlight(true, button); 980 }, 981 982 /** 983 * Un-highlight a button, or buttons in the toolbar. 984 * 985 * If no button is specified, all buttons are un-highlighted. 986 * 987 * @method unHighlightButtons 988 * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. 989 * @chainable 990 */ 991 unHighlightButtons: function(button) { 992 return this._changeButtonHighlight(false, button); 993 }, 994 995 /** 996 * Highlight a button, or buttons in the toolbar. 997 * 998 * @method _changeButtonHighlight 999 * @param {boolean} highlight true 1000 * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight. 1001 * @protected 1002 * @chainable 1003 */ 1004 _changeButtonHighlight: function(highlight, button) { 1005 var method = 'addClass'; 1006 1007 if (!highlight) { 1008 method = 'removeClass'; 1009 } 1010 if (button) { 1011 if (this.buttons[button]) { 1012 this.buttons[button][method](HIGHLIGHT); 1013 } 1014 } else { 1015 Y.Object.each(this.buttons, function(button) { 1016 button[method](HIGHLIGHT); 1017 }, this); 1018 } 1019 1020 return this; 1021 }, 1022 1023 /** 1024 * Get the default meta key to use with keyboard events. 1025 * 1026 * On a Mac, this will be the 'meta' key for Command; otherwise it will 1027 * be the Control key. 1028 * 1029 * @method _getDefaultMetaKey 1030 * @return {string} 1031 * @private 1032 */ 1033 _getDefaultMetaKey: function() { 1034 if (Y.UA.os === 'macintosh') { 1035 return 'meta'; 1036 } else { 1037 return 'ctrl'; 1038 } 1039 }, 1040 1041 /** 1042 * Get the user-visible description of the meta key to use with keyboard events. 1043 * 1044 * On a Mac, this will be 'Command' ; otherwise it will be 'Control'. 1045 * 1046 * @method _getDefaultMetaKeyDescription 1047 * @return {string} 1048 * @private 1049 */ 1050 _getDefaultMetaKeyDescription: function(keyCode) { 1051 if (Y.UA.os === 'macintosh') { 1052 return M.util.get_string('editor_command_keycode', 'editor_atto', String.fromCharCode(keyCode).toLowerCase()); 1053 } else { 1054 return M.util.get_string('editor_control_keycode', 'editor_atto', String.fromCharCode(keyCode).toLowerCase()); 1055 } 1056 }, 1057 1058 /** 1059 * Get the standard key event to use for keyboard events. 1060 * 1061 * @method _getKeyEvent 1062 * @return {string} 1063 * @private 1064 */ 1065 _getKeyEvent: function() { 1066 return 'down:'; 1067 } 1068 }; 1069 1070 Y.Base.mix(Y.M.editor_atto.EditorPlugin, [EditorPluginButtons]); 1071 // This file is part of Moodle - http://moodle.org/ 1072 // 1073 // Moodle is free software: you can redistribute it and/or modify 1074 // it under the terms of the GNU General Public License as published by 1075 // the Free Software Foundation, either version 3 of the License, or 1076 // (at your option) any later version. 1077 // 1078 // Moodle is distributed in the hope that it will be useful, 1079 // but WITHOUT ANY WARRANTY; without even the implied warranty of 1080 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1081 // GNU General Public License for more details. 1082 // 1083 // You should have received a copy of the GNU General Public License 1084 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 1085 1086 /** 1087 * @module moodle-editor_atto-plugin 1088 * @submodule dialogue 1089 */ 1090 1091 /** 1092 * Dialogue functions for an Atto Plugin. 1093 * 1094 * See {{#crossLink "M.editor_atto.EditorPlugin"}}{{/crossLink}} for details. 1095 * 1096 * @namespace M.editor_atto 1097 * @class EditorPluginDialogue 1098 */ 1099 1100 function EditorPluginDialogue() {} 1101 1102 EditorPluginDialogue.ATTRS = { 1103 }; 1104 1105 EditorPluginDialogue.prototype = { 1106 /** 1107 * A reference to the instantiated dialogue. 1108 * 1109 * @property _dialogue 1110 * @private 1111 * @type M.core.Dialogue 1112 */ 1113 _dialogue: null, 1114 1115 /** 1116 * Fetch the instantiated dialogue. If a dialogue has not yet been created, instantiate one. 1117 * 1118 * <em><b>Note:</b> Only one dialogue is supported through this interface.</em> 1119 * 1120 * For a full list of options, see documentation for {{#crossLink "M.core.dialogue"}}{{/crossLink}}. 1121 * 1122 * A sensible default is provided for the focusAfterHide attribute. 1123 * 1124 * @method getDialogue 1125 * @param {object} config 1126 * @param {boolean|string|Node} [config.focusAfterHide=undefined] Set the focusAfterHide setting to the 1127 * specified Node according to the following values: 1128 * <ul> 1129 * <li>If true was passed, the first button for this plugin will be used instead; or</li> 1130 * <li>If a String was passed, the named button for this plugin will be used instead; or</li> 1131 * <li>If a Node was passed, that Node will be used instead.</li> 1132 * 1133 * This setting is checked each time that getDialogue is called. 1134 * 1135 * @return {M.core.dialogue} 1136 */ 1137 getDialogue: function(config) { 1138 // Config is an optional param - define a default. 1139 config = config || {}; 1140 1141 var focusAfterHide = false; 1142 if (config.focusAfterHide) { 1143 // Remove the focusAfterHide because we may pass it a non-node value. 1144 focusAfterHide = config.focusAfterHide; 1145 delete config.focusAfterHide; 1146 } 1147 1148 if (!this._dialogue) { 1149 // Merge the default configuration with any provided configuration. 1150 var dialogueConfig = Y.merge({ 1151 visible: false, 1152 modal: true, 1153 close: true, 1154 draggable: true 1155 }, config); 1156 1157 // Instantiate the dialogue. 1158 this._dialogue = new M.core.dialogue(dialogueConfig); 1159 } 1160 1161 if (focusAfterHide !== false) { 1162 if (focusAfterHide === true) { 1163 this._dialogue.set('focusAfterHide', this.buttons[this.buttonNames[0]]); 1164 1165 } else if (typeof focusAfterHide === 'string') { 1166 this._dialogue.set('focusAfterHide', this.buttons[focusAfterHide]); 1167 1168 } else { 1169 this._dialogue.set('focusAfterHide', focusAfterHide); 1170 1171 } 1172 } 1173 1174 return this._dialogue; 1175 } 1176 }; 1177 1178 Y.Base.mix(Y.M.editor_atto.EditorPlugin, [EditorPluginDialogue]); 1179 1180 1181 }, '@VERSION@', { 1182 "requires": [ 1183 "node", 1184 "base", 1185 "escape", 1186 "event", 1187 "event-outside", 1188 "handlebars", 1189 "event-custom", 1190 "timers", 1191 "moodle-editor_atto-menu" 1192 ] 1193 });
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 |