- add sources.
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / menu_item.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 cr.define('cr.ui', function() {
6   /** @const */ var Command = cr.ui.Command;
7
8   /**
9    * Creates a new menu item element.
10    * @param {Object=} opt_propertyBag Optional properties.
11    * @constructor
12    * @extends {HTMLDivElement}
13    */
14   var MenuItem = cr.ui.define('div');
15
16   /**
17    * Creates a new menu separator element.
18    * @return {cr.ui.MenuItem} The new separator element.
19    */
20   MenuItem.createSeparator = function() {
21     var el = cr.doc.createElement('hr');
22     MenuItem.decorate(el);
23     return el;
24   };
25
26   MenuItem.prototype = {
27     __proto__: HTMLButtonElement.prototype,
28
29     /**
30      * Initializes the menu item.
31      */
32     decorate: function() {
33       var commandId;
34       if ((commandId = this.getAttribute('command')))
35         this.command = commandId;
36
37       this.addEventListener('mouseup', this.handleMouseUp_);
38
39       // Adding the 'custom-appearance' class prevents widgets.css from changing
40       // the appearance of this element.
41       this.classList.add('custom-appearance');
42
43       // Enable Text to Speech on the menu. Additionaly, ID has to be set, since
44       // it is used in element's aria-activedescendant attribute.
45       if (!this.isSeparator())
46         this.setAttribute('role', 'menuitem');
47
48       var iconUrl;
49       if ((iconUrl = this.getAttribute('icon')))
50         this.iconUrl = iconUrl;
51     },
52
53     /**
54      * The command associated with this menu item. If this is set to a string
55      * of the form "#element-id" then the element is looked up in the document
56      * of the command.
57      * @type {cr.ui.Command}
58      */
59     command_: null,
60     get command() {
61       return this.command_;
62     },
63     set command(command) {
64       if (this.command_) {
65         this.command_.removeEventListener('labelChange', this);
66         this.command_.removeEventListener('disabledChange', this);
67         this.command_.removeEventListener('hiddenChange', this);
68         this.command_.removeEventListener('checkedChange', this);
69       }
70
71       if (typeof command == 'string' && command[0] == '#') {
72         command = this.ownerDocument.getElementById(command.slice(1));
73         cr.ui.decorate(command, Command);
74       }
75
76       this.command_ = command;
77       if (command) {
78         if (command.id)
79           this.setAttribute('command', '#' + command.id);
80
81         if (typeof command.label === 'string')
82           this.label = command.label;
83         this.disabled = command.disabled;
84         this.hidden = command.hidden;
85
86         this.command_.addEventListener('labelChange', this);
87         this.command_.addEventListener('disabledChange', this);
88         this.command_.addEventListener('hiddenChange', this);
89         this.command_.addEventListener('checkedChange', this);
90       }
91
92       this.updateShortcut_();
93     },
94
95     /**
96      * The text label.
97      * @type {string}
98      */
99     get label() {
100       return this.textContent;
101     },
102     set label(label) {
103       this.textContent = label;
104     },
105
106     /**
107      * Menu icon.
108      * @type {string}
109      */
110     get iconUrl() {
111       return this.style.backgroundImage;
112     },
113     set iconUrl(url) {
114       this.style.backgroundImage = 'url(' + url + ')';
115     },
116
117     /**
118      * @return {boolean} Whether the menu item is a separator.
119      */
120     isSeparator: function() {
121       return this.tagName == 'HR';
122     },
123
124     /**
125      * Updates shortcut text according to associated command. If command has
126      * multiple shortcuts, only first one is displayed.
127      */
128     updateShortcut_: function() {
129       this.removeAttribute('shortcutText');
130
131       if (!(this.command_ && this.command_.shortcut))
132         return;
133
134       var shortcuts = this.command_.shortcut.split(/\s+/);
135
136       if (shortcuts.length == 0)
137         return;
138
139       var shortcut = shortcuts[0];
140       var mods = {};
141       var ident = '';
142       shortcut.split('-').forEach(function(part) {
143         var partUc = part.toUpperCase();
144         switch (partUc) {
145           case 'CTRL':
146           case 'ALT':
147           case 'SHIFT':
148           case 'META':
149             mods[partUc] = true;
150             break;
151           default:
152             console.assert(!ident, 'Shortcut has two non-modifier keys');
153             ident = part;
154         }
155       });
156
157       var shortcutText = '';
158
159       // TODO(zvorygin): if more cornercases appear - optimize following
160       // code. Currently 'Enter' keystroke is passed as 'Enter', and 'Space'
161       // is passed as 'U+0020'
162       if (ident == 'U+0020')
163         ident = 'Space';
164
165       ['CTRL', 'ALT', 'SHIFT', 'META'].forEach(function(mod) {
166         if (mods[mod])
167           shortcutText += loadTimeData.getString('SHORTCUT_' + mod) + '+';
168       });
169
170       if (ident.indexOf('U+') != 0) {
171         shortcutText +=
172             loadTimeData.getString('SHORTCUT_' + ident.toUpperCase());
173       } else {
174         shortcutText +=
175             String.fromCharCode(parseInt(ident.substring(2), 16));
176       }
177
178       this.setAttribute('shortcutText', shortcutText);
179     },
180
181     /**
182      * Handles mouseup events. This dispatches an activate event; if there is an
183      * associated command, that command is executed.
184      * @param {Event} e The mouseup event object.
185      * @private
186      */
187     handleMouseUp_: function(e) {
188       // Only dispatch an activate event for left or middle click.
189       if (e.button > 1)
190         return;
191
192       if (!this.disabled && !this.isSeparator() && this.selected) {
193         // Store |contextElement| since it'll be removed by {Menu} on handling
194         // 'activate' event.
195         var contextElement = this.parentNode.contextElement;
196         var activationEvent = cr.doc.createEvent('Event');
197         activationEvent.initEvent('activate', true, true);
198         activationEvent.originalEvent = e;
199         // Dispatch command event followed by executing the command object.
200         if (this.dispatchEvent(activationEvent)) {
201           var command = this.command;
202           if (command) {
203             command.execute(contextElement);
204             cr.ui.swallowDoubleClick(e);
205           }
206         }
207       }
208     },
209
210     /**
211      * Updates command according to the node on which this menu was invoked.
212      * @param {Node=} opt_node Node on which menu was opened.
213      */
214     updateCommand: function(opt_node) {
215       if (this.command_) {
216         this.command_.canExecuteChange(opt_node);
217       }
218     },
219
220     /**
221      * Handles changes to the associated command.
222      * @param {Event} e The event object.
223      */
224     handleEvent: function(e) {
225       switch (e.type) {
226         case 'disabledChange':
227           this.disabled = this.command.disabled;
228           break;
229         case 'hiddenChange':
230           this.hidden = this.command.hidden;
231           break;
232         case 'labelChange':
233           this.label = this.command.label;
234           break;
235         case 'checkedChange':
236           this.checked = this.command.checked;
237           break;
238       }
239     }
240   };
241
242   /**
243    * Whether the menu item is disabled or not.
244    * @type {boolean}
245    */
246   cr.defineProperty(MenuItem, 'disabled', cr.PropertyKind.BOOL_ATTR);
247
248   /**
249    * Whether the menu item is hidden or not.
250    * @type {boolean}
251    */
252   cr.defineProperty(MenuItem, 'hidden', cr.PropertyKind.BOOL_ATTR);
253
254   /**
255    * Whether the menu item is selected or not.
256    * @type {boolean}
257    */
258   cr.defineProperty(MenuItem, 'selected', cr.PropertyKind.BOOL_ATTR);
259
260   /**
261    * Whether the menu item is checked or not.
262    * @type {boolean}
263    */
264   cr.defineProperty(MenuItem, 'checked', cr.PropertyKind.BOOL_ATTR);
265
266   /**
267    * Whether the menu item is checkable or not.
268    * @type {boolean}
269    */
270   cr.defineProperty(MenuItem, 'checkable', cr.PropertyKind.BOOL_ATTR);
271
272   // Export
273   return {
274     MenuItem: MenuItem
275   };
276 });