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