Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / command.js
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(assert(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    */
167   cr.defineProperty(Command, 'label', cr.PropertyKind.ATTR);
168
169   /**
170    * Whether the command is disabled or not.
171    */
172   cr.defineProperty(Command, 'disabled', cr.PropertyKind.BOOL_ATTR);
173
174   /**
175    * Whether the command is hidden or not.
176    */
177   cr.defineProperty(Command, 'hidden', cr.PropertyKind.BOOL_ATTR);
178
179   /**
180    * Whether the command is checked or not.
181    */
182   cr.defineProperty(Command, 'checked', cr.PropertyKind.BOOL_ATTR);
183
184   /**
185    * The flag that prevents the shortcut text from being displayed on menu.
186    *
187    * If false, the keyboard shortcut text (eg. "Ctrl+X" for the cut command)
188    * is displayed in menu when the command is assosiated with a menu item.
189    * Otherwise, no text is displayed.
190    */
191   cr.defineProperty(Command, 'hideShortcutText', cr.PropertyKind.BOOL_ATTR);
192
193   /**
194    * Dispatches a canExecute event on the target.
195    * @param {!cr.ui.Command} command The command that we are testing for.
196    * @param {EventTarget} target The target element to dispatch the event on.
197    */
198   function dispatchCanExecuteEvent(command, target) {
199     var e = new CanExecuteEvent(command);
200     target.dispatchEvent(e);
201     command.disabled = !e.canExecute;
202   }
203
204   /**
205    * The command managers for different documents.
206    */
207   var commandManagers = {};
208
209   /**
210    * Keeps track of the focused element and updates the commands when the focus
211    * changes.
212    * @param {!Document} doc The document that we are managing the commands for.
213    * @constructor
214    */
215   function CommandManager(doc) {
216     doc.addEventListener('focus', this.handleFocus_.bind(this), true);
217     // Make sure we add the listener to the bubbling phase so that elements can
218     // prevent the command.
219     doc.addEventListener('keydown', this.handleKeyDown_.bind(this), false);
220   }
221
222   /**
223    * Initializes a command manager for the document as needed.
224    * @param {!Document} doc The document to manage the commands for.
225    */
226   CommandManager.init = function(doc) {
227     var uid = cr.getUid(doc);
228     if (!(uid in commandManagers)) {
229       commandManagers[uid] = new CommandManager(doc);
230     }
231   };
232
233   CommandManager.prototype = {
234
235     /**
236      * Handles focus changes on the document.
237      * @param {Event} e The focus event object.
238      * @private
239      */
240     handleFocus_: function(e) {
241       var target = e.target;
242
243       // Ignore focus on a menu button or command item
244       if (target.menu || target.command)
245         return;
246
247       var commands = Array.prototype.slice.call(
248           target.ownerDocument.querySelectorAll('command'));
249
250       commands.forEach(function(command) {
251         dispatchCanExecuteEvent(command, target);
252       });
253     },
254
255     /**
256      * Handles the keydown event and routes it to the right command.
257      * @param {!Event} e The keydown event.
258      */
259     handleKeyDown_: function(e) {
260       var target = e.target;
261       var commands = Array.prototype.slice.call(
262           target.ownerDocument.querySelectorAll('command'));
263
264       for (var i = 0, command; command = commands[i]; i++) {
265         if (command.matchesEvent(e)) {
266           // When invoking a command via a shortcut, we have to manually check
267           // if it can be executed, since focus might not have been changed
268           // what would have updated the command's state.
269           command.canExecuteChange();
270
271           if (!command.disabled) {
272             e.preventDefault();
273             // We do not want any other element to handle this.
274             e.stopPropagation();
275             command.execute();
276             return;
277           }
278         }
279       }
280     }
281   };
282
283   /**
284    * The event type used for canExecute events.
285    * @param {!cr.ui.Command} command The command that we are evaluating.
286    * @extends {Event}
287    * @constructor
288    * @class
289    */
290   function CanExecuteEvent(command) {
291     var e = new Event('canExecute', {bubbles: true});
292     e.__proto__ = CanExecuteEvent.prototype;
293     e.command = command;
294     return e;
295   }
296
297   CanExecuteEvent.prototype = {
298     __proto__: Event.prototype,
299
300     /**
301      * The current command
302      * @type {cr.ui.Command}
303      */
304     command: null,
305
306     /**
307      * Whether the target can execute the command. Setting this also stops the
308      * propagation.
309      * @type {boolean}
310      */
311     canExecute_: false,
312     get canExecute() {
313       return this.canExecute_;
314     },
315     set canExecute(canExecute) {
316       this.canExecute_ = !!canExecute;
317       this.stopPropagation();
318     }
319   };
320
321   // Export
322   return {
323     Command: Command,
324     CanExecuteEvent: CanExecuteEvent
325   };
326 });