[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/editor/atto/yui/src/editor/js/ -> editor-plugin-buttons.js (source)

   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]);


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1