• 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
7  /** @const */ var Menu = cr.ui.Menu;
8
9  /**
10   * Handles context menus.
11   * @constructor
12   */
13  function ContextMenuHandler() {}
14
15  ContextMenuHandler.prototype = {
16
17    /**
18     * The menu that we are currently showing.
19     * @type {cr.ui.Menu}
20     */
21    menu_: null,
22    get menu() {
23      return this.menu_;
24    },
25
26    /**
27     * Shows a menu as a context menu.
28     * @param {!Event} e The event triggering the show (usually a contextmenu
29     *     event).
30     * @param {!cr.ui.Menu} menu The menu to show.
31     */
32    showMenu: function(e, menu) {
33      this.menu_ = menu;
34
35      menu.style.display = 'block';
36      // when the menu is shown we steal all keyboard events.
37      var doc = menu.ownerDocument;
38      doc.addEventListener('keydown', this, true);
39      doc.addEventListener('mousedown', this, true);
40      doc.addEventListener('blur', this, true);
41      doc.defaultView.addEventListener('resize', this);
42      menu.addEventListener('contextmenu', this);
43      menu.addEventListener('activate', this);
44      this.positionMenu_(e, menu);
45    },
46
47    /**
48     * Hide the currently shown menu.
49     */
50    hideMenu: function() {
51      var menu = this.menu;
52      if (!menu)
53        return;
54
55      menu.style.display = 'none';
56      var doc = menu.ownerDocument;
57      doc.removeEventListener('keydown', this, true);
58      doc.removeEventListener('mousedown', this, true);
59      doc.removeEventListener('blur', this, true);
60      doc.defaultView.removeEventListener('resize', this);
61      menu.removeEventListener('contextmenu', this);
62      menu.removeEventListener('activate', this);
63      menu.selectedIndex = -1;
64      this.menu_ = null;
65
66      // On windows we might hide the menu in a right mouse button up and if
67      // that is the case we wait some short period before we allow the menu
68      // to be shown again.
69      this.hideTimestamp_ = cr.isWindows ? Date.now() : 0;
70    },
71
72    /**
73     * Positions the menu
74     * @param {!Event} e The event object triggering the showing.
75     * @param {!cr.ui.Menu} menu The menu to position.
76     * @private
77     */
78    positionMenu_: function(e, menu) {
79      // TODO(arv): Handle scrolled documents when needed.
80
81      var element = e.currentTarget;
82      var x, y;
83      // When the user presses the context menu key (on the keyboard) we need
84      // to detect this.
85      if (this.keyIsDown_) {
86        var rect = element.getRectForContextMenu ?
87                       element.getRectForContextMenu() :
88                       element.getBoundingClientRect();
89        var offset = Math.min(rect.width, rect.height) / 2;
90        x = rect.left + offset;
91        y = rect.top + offset;
92      } else {
93        x = e.clientX;
94        y = e.clientY;
95      }
96
97      cr.ui.positionPopupAtPoint(x, y, menu);
98    },
99
100    /**
101     * Handles event callbacks.
102     * @param {!Event} e The event object.
103     */
104    handleEvent: function(e) {
105      // Keep track of keydown state so that we can use that to determine the
106      // reason for the contextmenu event.
107      switch (e.type) {
108        case 'keydown':
109          this.keyIsDown_ = !e.ctrlKey && !e.altKey &&
110              // context menu key or Shift-F10
111              (e.keyCode == 93 && !e.shiftKey ||
112               e.keyIdentifier == 'F10' && e.shiftKey);
113          break;
114
115        case 'keyup':
116          this.keyIsDown_ = false;
117          break;
118      }
119
120      // Context menu is handled even when we have no menu.
121      if (e.type != 'contextmenu' && !this.menu)
122        return;
123
124      switch (e.type) {
125        case 'mousedown':
126          if (!this.menu.contains(e.target))
127            this.hideMenu();
128          else
129            e.preventDefault();
130          break;
131        case 'keydown':
132          // keyIdentifier does not report 'Esc' correctly
133          if (e.keyCode == 27 /* Esc */) {
134            this.hideMenu();
135            e.stopPropagation();
136            e.preventDefault();
137
138          // If the menu is visible we let it handle all the keyboard events.
139          } else if (this.menu) {
140            this.menu.handleKeyDown(e);
141            e.preventDefault();
142            e.stopPropagation();
143          }
144          break;
145
146        case 'activate':
147        case 'blur':
148        case 'resize':
149          this.hideMenu();
150          break;
151
152        case 'contextmenu':
153          if ((!this.menu || !this.menu.contains(e.target)) &&
154              (!this.hideTimestamp_ || Date.now() - this.hideTimestamp_ > 50))
155            this.showMenu(e, e.currentTarget.contextMenu);
156          e.preventDefault();
157          // Don't allow elements further up in the DOM to show their menus.
158          e.stopPropagation();
159          break;
160      }
161    },
162
163    /**
164     * Adds a contextMenu property to an element or element class.
165     * @param {!Element|!Function} element The element or class to add the
166     *     contextMenu property to.
167     */
168    addContextMenuProperty: function(element) {
169      if (typeof element == 'function')
170        element = element.prototype;
171
172      element.__defineGetter__('contextMenu', function() {
173        return this.contextMenu_;
174      });
175      element.__defineSetter__('contextMenu', function(menu) {
176        var oldContextMenu = this.contextMenu;
177
178        if (typeof menu == 'string' && menu[0] == '#') {
179          menu = this.ownerDocument.getElementById(menu.slice(1));
180          cr.ui.decorate(menu, Menu);
181        }
182
183        if (menu === oldContextMenu)
184          return;
185
186        if (oldContextMenu && !menu) {
187          this.removeEventListener('contextmenu', contextMenuHandler);
188          this.removeEventListener('keydown', contextMenuHandler);
189          this.removeEventListener('keyup', contextMenuHandler);
190        }
191        if (menu && !oldContextMenu) {
192          this.addEventListener('contextmenu', contextMenuHandler);
193          this.addEventListener('keydown', contextMenuHandler);
194          this.addEventListener('keyup', contextMenuHandler);
195        }
196
197        this.contextMenu_ = menu;
198
199        if (menu && menu.id)
200          this.setAttribute('contextmenu', '#' + menu.id);
201
202        cr.dispatchPropertyChange(this, 'contextMenu', menu, oldContextMenu);
203      });
204
205      if (!element.getRectForContextMenu) {
206        /**
207         * @return {!ClientRect} The rect to use for positioning the context
208         *     menu when the context menu is not opened using a mouse position.
209         */
210        element.getRectForContextMenu = function() {
211          return this.getBoundingClientRect();
212        };
213      }
214    },
215
216    /**
217     * Sets the given contextMenu to the given element. A contextMenu property
218     * would be added if necessary.
219     * @param {!Element} element The element or class to set the contextMenu to.
220     * @param {!cr.ui.Menu} contextMenu The contextMenu property to be set.
221     */
222    setContextMenu: function(element, contextMenu) {
223      if (!element.contextMenu)
224        this.addContextMenuProperty(element);
225      element.contextMenu = contextMenu;
226    }
227  };
228
229  /**
230   * The singleton context menu handler.
231   * @type {!ContextMenuHandler}
232   */
233  var contextMenuHandler = new ContextMenuHandler;
234
235  // Export
236  return {
237    contextMenuHandler: contextMenuHandler
238  };
239});
240