Home | History | Annotate | Download | only in ui
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview A command is an abstraction of an action a user can do in the
      7  * UI.
      8  *
      9  * When the focus changes in the document for each command a canExecute event
     10  * is dispatched on the active element. By listening to this event you can
     11  * enable and disable the command by setting the event.canExecute property.
     12  *
     13  * When a command is executed a command event is dispatched on the active
     14  * element. Note that you should stop the propagation after you have handled the
     15  * command if there might be other command listeners higher up in the DOM tree.
     16  */
     17 
     18 cr.define('cr.ui', function() {
     19 
     20   /**
     21    * This is used to identify keyboard shortcuts.
     22    * @param {string} shortcut The text used to describe the keys for this
     23    *     keyboard shortcut.
     24    * @constructor
     25    */
     26   function KeyboardShortcut(shortcut) {
     27     var mods = {};
     28     var ident = '';
     29     shortcut.split('-').forEach(function(part) {
     30       var partLc = part.toLowerCase();
     31       switch (partLc) {
     32         case 'alt':
     33         case 'ctrl':
     34         case 'meta':
     35         case 'shift':
     36           mods[partLc + 'Key'] = true;
     37           break;
     38         default:
     39           if (ident)
     40             throw Error('Invalid shortcut');
     41           ident = part;
     42       }
     43     });
     44 
     45     this.ident_ = ident;
     46     this.mods_ = mods;
     47   }
     48 
     49   KeyboardShortcut.prototype = {
     50     /**
     51      * Whether the keyboard shortcut object matches a keyboard event.
     52      * @param {!Event} e The keyboard event object.
     53      * @return {boolean} Whether we found a match or not.
     54      */
     55     matchesEvent: function(e) {
     56       if (e.keyIdentifier == this.ident_) {
     57         // All keyboard modifiers needs to match.
     58         var mods = this.mods_;
     59         return ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].every(function(k) {
     60           return e[k] == !!mods[k];
     61         });
     62       }
     63       return false;
     64     }
     65   };
     66 
     67   /**
     68    * Creates a new command element.
     69    * @constructor
     70    * @extends {HTMLElement}
     71    */
     72   var Command = cr.ui.define('command');
     73 
     74   Command.prototype = {
     75     __proto__: HTMLElement.prototype,
     76 
     77     /**
     78      * Initializes the command.
     79      */
     80     decorate: function() {
     81       CommandManager.init(this.ownerDocument);
     82 
     83       if (this.hasAttribute('shortcut'))
     84         this.shortcut = this.getAttribute('shortcut');
     85     },
     86 
     87     /**
     88      * Executes the command by dispatching a command event on the given element.
     89      * If |element| isn't given, the active element is used instead.
     90      * If the command is {@code disabled} this does nothing.
     91      * @param {HTMLElement=} opt_element Optional element to dispatch event on.
     92      */
     93     execute: function(opt_element) {
     94       if (this.disabled)
     95         return;
     96       var doc = this.ownerDocument;
     97       if (doc.activeElement) {
     98         var e = new Event('command', {bubbles: true});
     99         e.command = this;
    100 
    101         (opt_element || doc.activeElement).dispatchEvent(e);
    102       }
    103     },
    104 
    105     /**
    106      * Call this when there have been changes that might change whether the
    107      * command can be executed or not.
    108      * @param {Node=} opt_node Node for which to actuate command state.
    109      */
    110     canExecuteChange: function(opt_node) {
    111       dispatchCanExecuteEvent(this,
    112                               opt_node || this.ownerDocument.activeElement);
    113     },
    114 
    115     /**
    116      * The keyboard shortcut that triggers the command. This is a string
    117      * consisting of a keyIdentifier (as reported by WebKit in keydown) as
    118      * well as optional key modifiers joinded with a '-'.
    119      *
    120      * Multiple keyboard shortcuts can be provided by separating them by
    121      * whitespace.
    122      *
    123      * For example:
    124      *   "F1"
    125      *   "U+0008-Meta" for Apple command backspace.
    126      *   "U+0041-Ctrl" for Control A
    127      *   "U+007F U+0008-Meta" for Delete and Command Backspace
    128      *
    129      * @type {string}
    130      */
    131     shortcut_: '',
    132     get shortcut() {
    133       return this.shortcut_;
    134     },
    135     set shortcut(shortcut) {
    136       var oldShortcut = this.shortcut_;
    137       if (shortcut !== oldShortcut) {
    138         this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) {
    139           return new KeyboardShortcut(shortcut);
    140         });
    141 
    142         // Set this after the keyboardShortcuts_ since that might throw.
    143         this.shortcut_ = shortcut;
    144         cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_,
    145                                   oldShortcut);
    146       }
    147     },
    148 
    149     /**
    150      * Whether the event object matches the shortcut for this command.
    151      * @param {!Event} e The key event object.
    152      * @return {boolean} Whether it matched or not.
    153      */
    154     matchesEvent: function(e) {
    155       if (!this.keyboardShortcuts_)
    156         return false;
    157 
    158       return this.keyboardShortcuts_.some(function(keyboardShortcut) {
    159         return keyboardShortcut.matchesEvent(e);
    160         });
    161       }
    162   };
    163 
    164   /**
    165    * The label of the command.
    166    * @type {string}
    167    */
    168   cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR);
    169 
    170   /**
    171    * Whether the command is disabled or not.
    172    * @type {boolean}
    173    */
    174   cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR);
    175 
    176   /**
    177    * Whether the command is hidden or not.
    178    * @type {boolean}
    179    */
    180   cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR);
    181 
    182   /**
    183    * Whether the command is checked or not.
    184    * @type {boolean}
    185    */
    186   cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR);
    187 
    188   /**
    189    * The flag that prevents the shortcut text from being displayed on menu.
    190    *
    191    * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command)
    192    * is displayed in menu when the command is assosiated with a menu item.
    193    * Otherwise, no text is displayed.
    194    *
    195    * @type {boolean}
    196    */
    197   cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR);
    198 
    199   /**
    200    * Dispatches a canExecute event on the target.
    201    * @param {cr.ui.Command} command The command that we are testing for.
    202    * @param {Element} target The target element to dispatch the event on.
    203    */
    204   function dispatchCanExecuteEvent(command, target) {
    205     var e = new CanExecuteEvent(command, true);
    206     target.dispatchEvent(e);
    207     command.disabled = !e.canExecute;
    208   }
    209 
    210   /**
    211    * The command managers for different documents.
    212    */
    213   var commandManagers = {};
    214 
    215   /**
    216    * Keeps track of the focused element and updates the commands when the focus
    217    * changes.
    218    * @param {!Document} doc The document that we are managing the commands for.
    219    * @constructor
    220    */
    221   function CommandManager(doc) {
    222     doc.addEventListener('focus', this.handleFocus_.bind(this), true);
    223     // Make sure we add the listener to the bubbling phase so that elements can
    224     // prevent the command.
    225     doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false);
    226   }
    227 
    228   /**
    229    * Initializes a command manager for the document as needed.
    230    * @param {!Document} doc The document to manage the commands for.
    231    */
    232   CommandManager.init = function(doc) {
    233     var uid = cr.getUid(doc);
    234     if (!(uid in commandManagers)) {
    235       commandManagers[uid] = new CommandManager(doc);
    236     }
    237   };
    238 
    239   CommandManager.prototype = {
    240 
    241     /**
    242      * Handles focus changes on the document.
    243      * @param {Event} e The focus event object.
    244      * @private
    245      */
    246     handleFocus_: function(e) {
    247       var target = e.target;
    248 
    249       // Ignore focus on a menu button or command item
    250       if (target.menu || target.command)
    251         return;
    252 
    253       var commands = Array.prototype.slice.call(
    254           target.ownerDocument.querySelectorAll('command'));
    255 
    256       commands.forEach(function(command) {
    257         dispatchCanExecuteEvent(command, target);
    258       });
    259     },
    260 
    261     /**
    262      * Handles the keydown event and routes it to the right command.
    263      * @param {!Event} e The keydown event.
    264      */
    265     handleKeyDown_: function(e) {
    266       var target = e.target;
    267       var commands = Array.prototype.slice.call(
    268           target.ownerDocument.querySelectorAll('command'));
    269 
    270       for (var i = 0, command; command = commands[i]; i++) {
    271         if (command.matchesEvent(e)) {
    272           // When invoking a command via a shortcut, we have to manually check
    273           // if it can be executed, since focus might not have been changed
    274           // what would have updated the command's state.
    275           command.canExecuteChange();
    276 
    277           if (!command.disabled) {
    278             e.preventDefault();
    279             // We do not want any other element to handle this.
    280             e.stopPropagation();
    281             command.execute();
    282             return;
    283           }
    284         }
    285       }
    286     }
    287   };
    288 
    289   /**
    290    * The event type used for canExecute events.
    291    * @param {!cr.ui.Command} command The command that we are evaluating.
    292    * @extends {Event}
    293    * @constructor
    294    * @class
    295    */
    296   function CanExecuteEvent(command) {
    297     var e = new Event('canExecute', {bubbles: true});
    298     e.__proto__ = CanExecuteEvent.prototype;
    299     e.command = command;
    300     return e;
    301   }
    302 
    303   CanExecuteEvent.prototype = {
    304     __proto__: Event.prototype,
    305 
    306     /**
    307      * The current command
    308      * @type {cr.ui.Command}
    309      */
    310     command: null,
    311 
    312     /**
    313      * Whether the target can execute the command. Setting this also stops the
    314      * propagation.
    315      * @type {boolean}
    316      */
    317     canExecute_: false,
    318     get canExecute() {
    319       return this.canExecute_;
    320     },
    321     set canExecute(canExecute) {
    322       this.canExecute_ = !!canExecute;
    323       this.stopPropagation();
    324     }
    325   };
    326 
    327   // Export
    328   return {
    329     Command: Command,
    330     CanExecuteEvent: CanExecuteEvent
    331   };
    332 });
    333