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