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