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
18cr.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