• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5cr.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_ ||
132          !this.command_.shortcut ||
133          this.command_.hideShortcutText)
134        return;
135
136      var shortcuts = this.command_.shortcut.split(/\s+/);
137
138      if (shortcuts.length == 0)
139        return;
140
141      var shortcut = shortcuts[0];
142      var mods = {};
143      var ident = '';
144      shortcut.split('-').forEach(function(part) {
145        var partUc = part.toUpperCase();
146        switch (partUc) {
147          case 'CTRL':
148          case 'ALT':
149          case 'SHIFT':
150          case 'META':
151            mods[partUc] = true;
152            break;
153          default:
154            console.assert(!ident, 'Shortcut has two non-modifier keys');
155            ident = part;
156        }
157      });
158
159      var shortcutText = '';
160
161      // TODO(zvorygin): if more cornercases appear - optimize following
162      // code. Currently 'Enter' keystroke is passed as 'Enter', and 'Space'
163      // is passed as 'U+0020'
164      if (ident == 'U+0020')
165        ident = 'Space';
166
167      ['CTRL', 'ALT', 'SHIFT', 'META'].forEach(function(mod) {
168        if (mods[mod])
169          shortcutText += loadTimeData.getString('SHORTCUT_' + mod) + '+';
170      });
171
172      if (ident.indexOf('U+') != 0) {
173        shortcutText +=
174            loadTimeData.getString('SHORTCUT_' + ident.toUpperCase());
175      } else {
176        shortcutText +=
177            String.fromCharCode(parseInt(ident.substring(2), 16));
178      }
179
180      this.setAttribute('shortcutText', shortcutText);
181    },
182
183    /**
184     * Handles mouseup events. This dispatches an activate event; if there is an
185     * associated command, that command is executed.
186     * @param {Event} e The mouseup event object.
187     * @private
188     */
189    handleMouseUp_: function(e) {
190      // Only dispatch an activate event for left or middle click.
191      if (e.button > 1)
192        return;
193
194      if (!this.disabled && !this.isSeparator() && this.selected) {
195        // Store |contextElement| since it'll be removed by {Menu} on handling
196        // 'activate' event.
197        var contextElement = this.parentNode.contextElement;
198        var activationEvent = cr.doc.createEvent('Event');
199        activationEvent.initEvent('activate', true, true);
200        activationEvent.originalEvent = e;
201        // Dispatch command event followed by executing the command object.
202        if (this.dispatchEvent(activationEvent)) {
203          var command = this.command;
204          if (command) {
205            command.execute(contextElement);
206            cr.ui.swallowDoubleClick(e);
207          }
208        }
209      }
210    },
211
212    /**
213     * Updates command according to the node on which this menu was invoked.
214     * @param {Node=} opt_node Node on which menu was opened.
215     */
216    updateCommand: function(opt_node) {
217      if (this.command_) {
218        this.command_.canExecuteChange(opt_node);
219      }
220    },
221
222    /**
223     * Handles changes to the associated command.
224     * @param {Event} e The event object.
225     */
226    handleEvent: function(e) {
227      switch (e.type) {
228        case 'disabledChange':
229          this.disabled = this.command.disabled;
230          break;
231        case 'hiddenChange':
232          this.hidden = this.command.hidden;
233          break;
234        case 'labelChange':
235          this.label = this.command.label;
236          break;
237        case 'checkedChange':
238          this.checked = this.command.checked;
239          break;
240      }
241    }
242  };
243
244  /**
245   * Whether the menu item is disabled or not.
246   * @type {boolean}
247   */
248  cr.defineProperty(MenuItem, 'disabled', cr.PropertyKind.BOOL_ATTR);
249
250  /**
251   * Whether the menu item is hidden or not.
252   * @type {boolean}
253   */
254  cr.defineProperty(MenuItem, 'hidden', cr.PropertyKind.BOOL_ATTR);
255
256  /**
257   * Whether the menu item is selected or not.
258   * @type {boolean}
259   */
260  cr.defineProperty(MenuItem, 'selected', cr.PropertyKind.BOOL_ATTR);
261
262  /**
263   * Whether the menu item is checked or not.
264   * @type {boolean}
265   */
266  cr.defineProperty(MenuItem, 'checked', cr.PropertyKind.BOOL_ATTR);
267
268  /**
269   * Whether the menu item is checkable or not.
270   * @type {boolean}
271   */
272  cr.defineProperty(MenuItem, 'checkable', cr.PropertyKind.BOOL_ATTR);
273
274  // Export
275  return {
276    MenuItem: MenuItem
277  };
278});
279