[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

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


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