1// Copyright (c) 2011 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 Menu = cr.ui.Menu;
7  const positionPopupAroundElement = cr.ui.positionPopupAroundElement;
8
9  /**
10   * Creates a new menu button element.
11   * @param {Object=} opt_propertyBag Optional properties.
12   * @constructor
13   * @extends {HTMLButtonElement}
14   */
15  var MenuButton = cr.ui.define('button');
16
17  MenuButton.prototype = {
18    __proto__: HTMLButtonElement.prototype,
19
20    /**
21     * Initializes the menu button.
22     */
23    decorate: function() {
24      this.addEventListener('mousedown', this);
25      this.addEventListener('keydown', this);
26
27      var menu;
28      if ((menu = this.getAttribute('menu')))
29        this.menu = menu;
30
31      // An event tracker for events we only connect to while the menu is
32      // displayed.
33      this.showingEvents_ = new EventTracker();
34    },
35
36    /**
37     * The menu associated with the menu button.
38     * @type {cr.ui.Menu}
39     */
40    get menu() {
41      return this.menu_;
42    },
43    set menu(menu) {
44      if (typeof menu == 'string' && menu[0] == '#') {
45        menu = this.ownerDocument.getElementById(menu.slice(1));
46        cr.ui.decorate(menu, Menu);
47      }
48
49      this.menu_ = menu;
50      if (menu) {
51        if (menu.id)
52          this.setAttribute('menu', '#' + menu.id);
53      }
54    },
55
56    /**
57     * Handles event callbacks.
58     * @param {Event} e The event object.
59     */
60    handleEvent: function(e) {
61      if (!this.menu)
62        return;
63
64      switch (e.type) {
65        case 'mousedown':
66          if (e.currentTarget == this.ownerDocument) {
67            if (!this.contains(e.target) && !this.menu.contains(e.target))
68              this.hideMenu();
69            else
70              e.preventDefault();
71          } else {
72            if (this.isMenuShown()) {
73              this.hideMenu();
74            } else if (e.button == 0) {  // Only show the menu when using left
75                                         // mouse button.
76              this.showMenu();
77              // Prevent the button from stealing focus on mousedown.
78              e.preventDefault();
79            }
80          }
81          break;
82        case 'keydown':
83          this.handleKeyDown(e);
84          // If the menu is visible we let it handle all the keyboard events.
85          if (this.isMenuShown() && e.currentTarget == this.ownerDocument) {
86            this.menu.handleKeyDown(e);
87            e.preventDefault();
88            e.stopPropagation();
89          }
90          break;
91
92        case 'activate':
93        case 'blur':
94        case 'resize':
95          this.hideMenu();
96          break;
97      }
98    },
99
100    /**
101     * Shows the menu.
102     */
103    showMenu: function() {
104      this.hideMenu();
105
106      this.menu.style.display = 'block';
107      this.setAttribute('menu-shown', '');
108
109      // when the menu is shown we steal all keyboard events.
110      var doc = this.ownerDocument;
111      var win = doc.defaultView;
112      this.showingEvents_.add(doc, 'keydown', this, true);
113      this.showingEvents_.add(doc, 'mousedown', this, true);
114      this.showingEvents_.add(doc, 'blur', this, true);
115      this.showingEvents_.add(win, 'resize', this);
116      this.showingEvents_.add(this.menu, 'activate', this);
117      this.positionMenu_();
118    },
119
120    /**
121     * Hides the menu. If your menu can go out of scope, make sure to call this
122     * first.
123     */
124    hideMenu: function() {
125      if (!this.isMenuShown())
126        return;
127
128      this.removeAttribute('menu-shown');
129      this.menu.style.display = 'none';
130
131      this.showingEvents_.removeAll();
132      this.menu.selectedIndex = -1;
133    },
134
135    /**
136     * Whether the menu is shown.
137     */
138    isMenuShown: function() {
139      return this.hasAttribute('menu-shown');
140    },
141
142    /**
143     * Positions the menu below the menu button. At this point we do not use any
144     * advanced positioning logic to ensure the menu fits in the viewport.
145     * @private
146     */
147    positionMenu_: function() {
148      positionPopupAroundElement(this, this.menu, cr.ui.AnchorType.BELOW);
149    },
150
151    /**
152     * Handles the keydown event for the menu button.
153     */
154    handleKeyDown: function(e) {
155      switch (e.keyIdentifier) {
156        case 'Down':
157        case 'Up':
158        case 'Enter':
159        case 'U+0020': // Space
160          if (!this.isMenuShown())
161            this.showMenu();
162          e.preventDefault();
163          break;
164        case 'Esc':
165        case 'U+001B': // Maybe this is remote desktop playing a prank?
166          this.hideMenu();
167          break;
168      }
169    }
170  };
171
172  // Export
173  return {
174    MenuButton: MenuButton
175  };
176});
177