Home | History | Annotate | Download | only in ui
      1 // Copyright (c) 2010 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      * Wether the keyboard shortcut object mathes 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 
     84     /**
     85      * Executes the command. This dispatches a command event on the active
     86      * element. If the command is {@code disabled} this does nothing.
     87      */
     88     execute: function() {
     89       if (this.disabled)
     90         return;
     91       var doc = this.ownerDocument;
     92       if (doc.activeElement) {
     93         var e = new cr.Event('command', true, false);
     94         e.command = this;
     95         doc.activeElement.dispatchEvent(e);
     96       }
     97     },
     98 
     99     /**
    100      * Call this when there have been changes that might change whether the
    101      * command can be executed or not.
    102      */
    103     canExecuteChange: function() {
    104       dispatchCanExecuteEvent(this, this.ownerDocument.activeElement);
    105     },
    106 
    107     /**
    108      * The keyboard shortcut that triggers the command. This is a string
    109      * consisting of a keyIdentifier (as reported by WebKit in keydown) as
    110      * well as optional key modifiers joinded with a '-'.
    111      *
    112      * Multiple keyboard shortcuts can be provided by separating them by
    113      * whitespace.
    114      *
    115      * For example:
    116      *   "F1"
    117      *   "U+0008-Meta" for Apple command backspace.
    118      *   "U+0041-Ctrl" for Control A
    119      *   "U+007F U+0008-Meta" for Delete and Command Backspace
    120      *
    121      * @type {string}
    122      */
    123     shortcut_: '',
    124     get shortcut() {
    125       return this.shortcut_;
    126     },
    127     set shortcut(shortcut) {
    128       var oldShortcut = this.shortcut_;
    129       if (shortcut !== oldShortcut) {
    130         this.keyboardShortcuts_ = shortcut.split(/\s+/).map(function(shortcut) {
    131           return new KeyboardShortcut(shortcut);
    132         });
    133 
    134         // Set this after the keyboardShortcuts_ since that might throw.
    135         this.shortcut_ = shortcut;
    136         cr.dispatchPropertyChange(this, 'shortcut', this.shortcut_,
    137                                   oldShortcut);
    138       }
    139     },
    140 
    141     /**
    142      * Whether the event object matches the shortcut for this command.
    143      * @param {!Event} e The key event object.
    144      * @return {boolean} Whether it matched or not.
    145      */
    146     matchesEvent: function(e) {
    147       if (!this.keyboardShortcuts_)
    148         return false;
    149 
    150       return this.keyboardShortcuts_.some(function(keyboardShortcut) {
    151         return keyboardShortcut.matchesEvent(e);
    152         });
    153       }
    154   };
    155 
    156   /**
    157    * The label of the command.
    158    * @type {string}
    159    */
    160   cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR);
    161 
    162   /**
    163    * Whether the command is disabled or not.
    164    * @type {boolean}
    165    */
    166   cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR);
    167 
    168   /**
    169    * Whether the command is hidden or not.
    170    * @type {boolean}
    171    */
    172   cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR);
    173 
    174   /**
    175    * Whether the command is checked or not.
    176    * @type {boolean}
    177    */
    178   cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR);
    179 
    180   /**
    181    * Dispatches a canExecute event on the target.
    182    * @param {cr.ui.Command} command The command that we are testing for.
    183    * @param {Element} target The target element to dispatch the event on.
    184    */
    185   function dispatchCanExecuteEvent(command, target) {
    186     var e = new CanExecuteEvent(command, true)
    187     target.dispatchEvent(e);
    188     command.disabled = !e.canExecute;
    189   }
    190 
    191   /**
    192    * The command managers for different documents.
    193    */
    194   var commandManagers = {};
    195 
    196   /**
    197    * Keeps track of the focused element and updates the commands when the focus
    198    * changes.
    199    * @param {!Document} doc The document that we are managing the commands for.
    200    * @constructor
    201    */
    202   function CommandManager(doc) {
    203     doc.addEventListener('focus', this.handleFocus_.bind(this), true);
    204     // Make sure we add the listener to the bubbling phase so that elements can
    205     // prevent the command.
    206     doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false);
    207   }
    208 
    209   /**
    210    * Initializes a command manager for the document as needed.
    211    * @param {!Document} doc The document to manage the commands for.
    212    */
    213   CommandManager.init = function(doc) {
    214     var uid = cr.getUid(doc);
    215     if (!(uid in commandManagers)) {
    216       commandManagers[uid] = new CommandManager(doc);
    217     }
    218   },
    219 
    220   CommandManager.prototype = {
    221 
    222     /**
    223      * Handles focus changes on the document.
    224      * @param {Event} e The focus event object.
    225      * @private
    226      */
    227     handleFocus_: function(e) {
    228       var target = e.target;
    229       var commands = Array.prototype.slice.call(
    230           target.ownerDocument.querySelectorAll('command'));
    231 
    232       commands.forEach(function(command) {
    233         dispatchCanExecuteEvent(command, target);
    234       });
    235     },
    236 
    237     /**
    238      * Handles the keydown event and routes it to the right command.
    239      * @param {!Event} e The keydown event.
    240      */
    241     handleKeyDown_: function(e) {
    242       var target = e.target;
    243       var commands = Array.prototype.slice.call(
    244           target.ownerDocument.querySelectorAll('command'));
    245 
    246       for (var i = 0, command; command = commands[i]; i++) {
    247         if (!command.disabled && command.matchesEvent(e)) {
    248           e.preventDefault();
    249           // We do not want any other element to handle this.
    250           e.stopPropagation();
    251 
    252           command.execute();
    253           return;
    254         }
    255       }
    256     }
    257   };
    258 
    259   /**
    260    * The event type used for canExecute events.
    261    * @param {!cr.ui.Command} command The command that we are evaluating.
    262    * @extends {Event}
    263    */
    264   function CanExecuteEvent(command) {
    265     var e = command.ownerDocument.createEvent('Event');
    266     e.initEvent('canExecute', true, false);
    267     e.__proto__ = CanExecuteEvent.prototype;
    268     e.command = command;
    269     return e;
    270   }
    271 
    272   CanExecuteEvent.prototype = {
    273     __proto__: Event.prototype,
    274 
    275     /**
    276      * The current command
    277      * @type {cr.ui.Command}
    278      */
    279     command: null,
    280 
    281     /**
    282      * Whether the target can execute the command. Setting this also stops the
    283      * propagation.
    284      * @type {boolean}
    285      */
    286     canExecute_: false,
    287     get canExecute() {
    288       return this.canExecute_;
    289     },
    290     set canExecute(canExecute) {
    291       this.canExecute_ = !!canExecute;
    292       this.stopPropagation();
    293     }
    294   };
    295 
    296   // Export
    297   return {
    298     Command: Command,
    299     CanExecuteEvent: CanExecuteEvent
    300   };
    301 });
    302