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