• 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 */
7  var Menu = cr.ui.Menu;
8
9  /** @const */
10  var positionPopupAroundElement = cr.ui.positionPopupAroundElement;
11
12   /**
13    * Enum for type of hide. Delayed is used when called by clicking on a
14    * checkable menu item.
15    * @enum {number}
16    */
17   var HideType = {
18     INSTANT: 0,
19     DELAYED: 1
20   };
21
22  /**
23   * Creates a new menu button element.
24   * @param {Object=} opt_propertyBag Optional properties.
25   * @constructor
26   * @extends {HTMLButtonElement}
27   */
28  var MenuButton = cr.ui.define('button');
29
30  MenuButton.prototype = {
31    __proto__: HTMLButtonElement.prototype,
32
33    /**
34     * Initializes the menu button.
35     */
36    decorate: function() {
37      this.addEventListener('mousedown', this);
38      this.addEventListener('keydown', this);
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      this.classList.add('menu-button');  // For styles in menu_button.css.
44
45      var menu;
46      if ((menu = this.getAttribute('menu')))
47        this.menu = menu;
48
49      // An event tracker for events we only connect to while the menu is
50      // displayed.
51      this.showingEvents_ = new EventTracker();
52
53      this.anchorType = cr.ui.AnchorType.BELOW;
54      this.invertLeftRight = false;
55    },
56
57    /**
58     * The menu associated with the menu button.
59     * @type {cr.ui.Menu}
60     */
61    get menu() {
62      return this.menu_;
63    },
64    set menu(menu) {
65      if (typeof menu == 'string' && menu[0] == '#') {
66        menu = this.ownerDocument.getElementById(menu.slice(1));
67        cr.ui.decorate(menu, Menu);
68      }
69
70      this.menu_ = menu;
71      if (menu) {
72        if (menu.id)
73          this.setAttribute('menu', '#' + menu.id);
74      }
75    },
76
77    /**
78     * Handles event callbacks.
79     * @param {Event} e The event object.
80     */
81    handleEvent: function(e) {
82      if (!this.menu)
83        return;
84
85      switch (e.type) {
86        case 'mousedown':
87          if (e.currentTarget == this.ownerDocument) {
88            if (!this.contains(e.target) && !this.menu.contains(e.target))
89              this.hideMenu();
90            else
91              e.preventDefault();
92          } else {
93            if (this.isMenuShown()) {
94              this.hideMenu();
95            } else if (e.button == 0) {  // Only show the menu when using left
96                                         // mouse button.
97              this.showMenu(false);
98
99              // Prevent the button from stealing focus on mousedown.
100              e.preventDefault();
101            }
102          }
103
104          // Hide the focus ring on mouse click.
105          this.classList.add('using-mouse');
106          break;
107        case 'keydown':
108          this.handleKeyDown(e);
109          // If the menu is visible we let it handle all the keyboard events.
110          if (this.isMenuShown() && e.currentTarget == this.ownerDocument) {
111            if (this.menu.handleKeyDown(e)) {
112              e.preventDefault();
113              e.stopPropagation();
114            }
115          }
116
117          // Show the focus ring on keypress.
118          this.classList.remove('using-mouse');
119          break;
120        case 'focus':
121          if (!this.contains(e.target) && !this.menu.contains(e.target)) {
122            this.hideMenu();
123            // Show the focus ring on focus - if it's come from a mouse event,
124            // the focus ring will be hidden in the mousedown event handler,
125            // executed after this.
126            this.classList.remove('using-mouse');
127          }
128          break;
129        case 'activate':
130          var hideDelayed = e.target instanceof cr.ui.MenuItem &&
131              e.target.checkable;
132          this.hideMenu(hideDelayed ? HideType.DELAYED : HideType.INSTANT);
133          break;
134        case 'scroll':
135          if (!(e.target == this.menu || this.menu.contains(e.target)))
136            this.hideMenu();
137          break;
138        case 'popstate':
139        case 'resize':
140          this.hideMenu();
141          break;
142      }
143    },
144
145    /**
146     * Shows the menu.
147     * @param {boolean} shouldSetFocus Whether to set focus on the
148     *     selected menu item.
149     */
150    showMenu: function(shouldSetFocus) {
151      this.hideMenu();
152
153      this.menu.updateCommands(this);
154
155      var event = document.createEvent('UIEvents');
156      event.initUIEvent('menushow', true, true, window, null);
157
158      if (!this.dispatchEvent(event))
159        return;
160
161      this.menu.hidden = false;
162
163      this.setAttribute('menu-shown', '');
164
165      // When the menu is shown we steal all keyboard events.
166      var doc = this.ownerDocument;
167      var win = doc.defaultView;
168      this.showingEvents_.add(doc, 'keydown', this, true);
169      this.showingEvents_.add(doc, 'mousedown', this, true);
170      this.showingEvents_.add(doc, 'focus', this, true);
171      this.showingEvents_.add(doc, 'scroll', this, true);
172      this.showingEvents_.add(win, 'popstate', this);
173      this.showingEvents_.add(win, 'resize', this);
174      this.showingEvents_.add(this.menu, 'activate', this);
175      this.positionMenu_();
176
177      if (shouldSetFocus)
178        this.menu.focusSelectedItem();
179    },
180
181    /**
182     * Hides the menu. If your menu can go out of scope, make sure to call this
183     * first.
184     * @param {HideType=} opt_hideType Type of hide.
185     *     default: HideType.INSTANT.
186     */
187    hideMenu: function(opt_hideType) {
188      if (!this.isMenuShown())
189        return;
190
191      this.removeAttribute('menu-shown');
192      if (opt_hideType == HideType.DELAYED)
193        this.menu.classList.add('hide-delayed');
194      else
195        this.menu.classList.remove('hide-delayed');
196      this.menu.hidden = true;
197
198      this.showingEvents_.removeAll();
199      this.focus();
200    },
201
202    /**
203     * Whether the menu is shown.
204     */
205    isMenuShown: function() {
206      return this.hasAttribute('menu-shown');
207    },
208
209    /**
210     * Positions the menu below the menu button. At this point we do not use any
211     * advanced positioning logic to ensure the menu fits in the viewport.
212     * @private
213     */
214    positionMenu_: function() {
215      positionPopupAroundElement(this, this.menu, this.anchorType,
216                                 this.invertLeftRight);
217    },
218
219    /**
220     * Handles the keydown event for the menu button.
221     */
222    handleKeyDown: function(e) {
223      switch (e.keyIdentifier) {
224        case 'Down':
225        case 'Up':
226        case 'Enter':
227        case 'U+0020': // Space
228          if (!this.isMenuShown())
229            this.showMenu(true);
230          e.preventDefault();
231          break;
232        case 'Esc':
233        case 'U+001B': // Maybe this is remote desktop playing a prank?
234        case 'U+0009': // Tab
235          this.hideMenu();
236          break;
237      }
238    }
239  };
240
241  /**
242   * Helper for styling a menu button with a drop-down arrow indicator.
243   * Creates a new 2D canvas context and draws a downward-facing arrow into it.
244   * @param {string} canvasName The name of the canvas. The canvas can be
245   *     addressed from CSS using -webkit-canvas(<canvasName>).
246   * @param {number} width The width of the canvas and the arrow.
247   * @param {number} height The height of the canvas and the arrow.
248   * @param {string} colorSpec The CSS color to use when drawing the arrow.
249   */
250  function createDropDownArrowCanvas(canvasName, width, height, colorSpec) {
251    var ctx = document.getCSSCanvasContext('2d', canvasName, width, height);
252    ctx.fillStyle = ctx.strokeStyle = colorSpec;
253    ctx.beginPath();
254    ctx.moveTo(0, 0);
255    ctx.lineTo(width, 0);
256    ctx.lineTo(height, height);
257    ctx.closePath();
258    ctx.fill();
259    ctx.stroke();
260  };
261
262  /** @const */ var ARROW_WIDTH = 6;
263  /** @const */ var ARROW_HEIGHT = 3;
264
265  /**
266   * Create the images used to style drop-down-style MenuButtons.
267   * This should be called before creating any MenuButtons that will have the
268   * CSS class 'drop-down'. If no colors are specified, defaults will be used.
269   * @param {=string} normalColor CSS color for the default button state.
270   * @param {=string} hoverColor CSS color for the hover button state.
271   * @param {=string} activeColor CSS color for the active button state.
272   */
273  MenuButton.createDropDownArrows = function(
274      normalColor, hoverColor, activeColor) {
275    normalColor = normalColor || 'rgb(192, 195, 198)';
276    hoverColor = hoverColor || 'rgb(48, 57, 66)';
277    activeColor = activeColor || 'white';
278
279    createDropDownArrowCanvas(
280        'drop-down-arrow', ARROW_WIDTH, ARROW_HEIGHT, normalColor);
281    createDropDownArrowCanvas(
282        'drop-down-arrow-hover', ARROW_WIDTH, ARROW_HEIGHT, hoverColor);
283    createDropDownArrowCanvas(
284        'drop-down-arrow-active', ARROW_WIDTH, ARROW_HEIGHT, activeColor);
285  };
286
287  // Export
288  return {
289    MenuButton: MenuButton,
290    HideType: HideType
291  };
292});
293