[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/editor/atto/yui/build/moodle-editor_atto-plugin/ -> moodle-editor_atto-plugin.js (source)

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


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