2 * Copyright (C) 2011 Google Inc. All Rights Reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 * @param {!Array.<!WebInspector.ContextMenuItem>} items
29 * @param {!WebInspector.SoftContextMenu=} parentMenu
31 WebInspector.SoftContextMenu = function(items, parentMenu)
34 this._parentMenu = parentMenu;
37 WebInspector.SoftContextMenu.prototype = {
39 * @param {!Event} event
45 this._time = new Date().getTime();
47 // Absolutely position menu for iframes.
48 var absoluteX = event.pageX;
49 var absoluteY = event.pageY;
50 var targetElement = event.target;
51 while (targetElement && window !== targetElement.ownerDocument.defaultView) {
52 var frameElement = targetElement.ownerDocument.defaultView.frameElement;
53 absoluteY += frameElement.totalOffsetTop();
54 absoluteX += frameElement.totalOffsetLeft();
55 targetElement = frameElement;
58 // Create context menu.
60 this._contextMenuElement = document.createElement("div");
61 this._contextMenuElement.className = "soft-context-menu";
62 this._contextMenuElement.tabIndex = 0;
63 this._contextMenuElement.style.top = absoluteY + "px";
64 this._contextMenuElement.style.left = absoluteX + "px";
66 this._contextMenuElement.addEventListener("mouseup", consumeEvent, false);
67 this._contextMenuElement.addEventListener("keydown", this._menuKeyDown.bind(this), false);
69 for (var i = 0; i < this._items.length; ++i)
70 this._contextMenuElement.appendChild(this._createMenuItem(this._items[i]));
72 // Install glass pane capturing events.
73 if (!this._parentMenu) {
74 this._glassPaneElement = document.createElement("div");
75 this._glassPaneElement.className = "soft-context-menu-glass-pane";
76 this._glassPaneElement.tabIndex = 0;
77 this._glassPaneElement.addEventListener("mouseup", this._glassPaneMouseUp.bind(this), false);
78 this._glassPaneElement.appendChild(this._contextMenuElement);
79 document.body.appendChild(this._glassPaneElement);
82 this._parentMenu._parentGlassPaneElement().appendChild(this._contextMenuElement);
84 // Re-position menu in case it does not fit.
85 if (document.body.offsetWidth < this._contextMenuElement.offsetLeft + this._contextMenuElement.offsetWidth)
86 this._contextMenuElement.style.left = (absoluteX - this._contextMenuElement.offsetWidth) + "px";
87 if (document.body.offsetHeight < this._contextMenuElement.offsetTop + this._contextMenuElement.offsetHeight)
88 this._contextMenuElement.style.top = (document.body.offsetHeight - this._contextMenuElement.offsetHeight) + "px";
93 _parentGlassPaneElement: function()
95 if (this._glassPaneElement)
96 return this._glassPaneElement;
98 return this._parentMenu._parentGlassPaneElement();
102 _createMenuItem: function(item)
104 if (item.type === "separator")
105 return this._createSeparator();
107 if (item.type === "subMenu")
108 return this._createSubMenu(item);
110 var menuItemElement = document.createElement("div");
111 menuItemElement.className = "soft-context-menu-item";
113 var checkMarkElement = document.createElement("span");
114 checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
115 checkMarkElement.className = "soft-context-menu-item-checkmark";
117 checkMarkElement.style.opacity = "0";
119 menuItemElement.appendChild(checkMarkElement);
120 menuItemElement.appendChild(document.createTextNode(item.label));
122 menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
123 menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
125 // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
126 menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);
127 menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false);
129 menuItemElement._actionId = item.id;
130 return menuItemElement;
133 _createSubMenu: function(item)
135 var menuItemElement = document.createElement("div");
136 menuItemElement.className = "soft-context-menu-item";
137 menuItemElement._subItems = item.subItems;
139 // Occupy the same space on the left in all items.
140 var checkMarkElement = document.createElement("span");
141 checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
142 checkMarkElement.className = "soft-context-menu-item-checkmark";
143 checkMarkElement.style.opacity = "0";
144 menuItemElement.appendChild(checkMarkElement);
146 var subMenuArrowElement = document.createElement("span");
147 subMenuArrowElement.textContent = "\u25B6"; // BLACK RIGHT-POINTING TRIANGLE
148 subMenuArrowElement.className = "soft-context-menu-item-submenu-arrow";
150 menuItemElement.appendChild(document.createTextNode(item.label));
151 menuItemElement.appendChild(subMenuArrowElement);
153 menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
154 menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
156 // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
157 menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bind(this), false);
158 menuItemElement.addEventListener("mouseout", this._menuItemMouseOut.bind(this), false);
160 return menuItemElement;
163 _createSeparator: function()
165 var separatorElement = document.createElement("div");
166 separatorElement.className = "soft-context-menu-separator";
167 separatorElement._isSeparator = true;
168 separatorElement.addEventListener("mouseover", this._hideSubMenu.bind(this), false);
169 separatorElement.createChild("div", "separator-line");
170 return separatorElement;
173 _menuItemMouseDown: function(event)
175 // Do not let separator's mouse down hit menu's handler - we need to receive mouse up!
179 _menuItemMouseUp: function(event)
181 this._triggerAction(event.target, event);
187 this._contextMenuElement.focus();
190 _triggerAction: function(menuItemElement, event)
192 if (!menuItemElement._subItems) {
193 this._discardMenu(true, event);
194 if (typeof menuItemElement._actionId !== "undefined") {
195 WebInspector.contextMenuItemSelected(menuItemElement._actionId);
196 delete menuItemElement._actionId;
201 this._showSubMenu(menuItemElement, event);
205 _showSubMenu: function(menuItemElement, event)
207 if (menuItemElement._subMenuTimer) {
208 clearTimeout(menuItemElement._subMenuTimer);
209 delete menuItemElement._subMenuTimer;
214 this._subMenu = new WebInspector.SoftContextMenu(menuItemElement._subItems, this);
215 this._subMenu.show(this._buildMouseEventForSubMenu(menuItemElement));
218 _buildMouseEventForSubMenu: function(subMenuItemElement)
220 var subMenuOffset = { x: subMenuItemElement.offsetWidth - 3, y: subMenuItemElement.offsetTop - 1 };
221 var targetX = this._x + subMenuOffset.x;
222 var targetY = this._y + subMenuOffset.y;
223 var targetPageX = parseInt(this._contextMenuElement.style.left, 10) + subMenuOffset.x;
224 var targetPageY = parseInt(this._contextMenuElement.style.top, 10) + subMenuOffset.y;
225 return { x: targetX, y: targetY, pageX: targetPageX, pageY: targetPageY, consume: function() {} };
228 _hideSubMenu: function()
232 this._subMenu._discardSubMenus();
236 _menuItemMouseOver: function(event)
238 this._highlightMenuItem(event.target);
241 _menuItemMouseOut: function(event)
243 if (!this._subMenu || !event.relatedTarget) {
244 this._highlightMenuItem(null);
248 var relatedTarget = event.relatedTarget;
249 if (this._contextMenuElement.isSelfOrAncestor(relatedTarget) || relatedTarget.classList.contains("soft-context-menu-glass-pane"))
250 this._highlightMenuItem(null);
253 _highlightMenuItem: function(menuItemElement)
255 if (this._highlightedMenuItemElement === menuItemElement)
259 if (this._highlightedMenuItemElement) {
260 this._highlightedMenuItemElement.classList.remove("soft-context-menu-item-mouse-over");
261 if (this._highlightedMenuItemElement._subItems && this._highlightedMenuItemElement._subMenuTimer) {
262 clearTimeout(this._highlightedMenuItemElement._subMenuTimer);
263 delete this._highlightedMenuItemElement._subMenuTimer;
266 this._highlightedMenuItemElement = menuItemElement;
267 if (this._highlightedMenuItemElement) {
268 this._highlightedMenuItemElement.classList.add("soft-context-menu-item-mouse-over");
269 this._contextMenuElement.focus();
270 if (this._highlightedMenuItemElement._subItems && !this._highlightedMenuItemElement._subMenuTimer)
271 this._highlightedMenuItemElement._subMenuTimer = setTimeout(this._showSubMenu.bind(this, this._highlightedMenuItemElement, this._buildMouseEventForSubMenu(this._highlightedMenuItemElement)), 150);
275 _highlightPrevious: function()
277 var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.previousSibling : this._contextMenuElement.lastChild;
278 while (menuItemElement && menuItemElement._isSeparator)
279 menuItemElement = menuItemElement.previousSibling;
281 this._highlightMenuItem(menuItemElement);
284 _highlightNext: function()
286 var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.nextSibling : this._contextMenuElement.firstChild;
287 while (menuItemElement && menuItemElement._isSeparator)
288 menuItemElement = menuItemElement.nextSibling;
290 this._highlightMenuItem(menuItemElement);
293 _menuKeyDown: function(event)
295 switch (event.keyIdentifier) {
297 this._highlightPrevious(); break;
299 this._highlightNext(); break;
301 if (this._parentMenu) {
302 this._highlightMenuItem(null);
303 this._parentMenu._focus();
307 if (!this._highlightedMenuItemElement)
309 if (this._highlightedMenuItemElement._subItems) {
310 this._showSubMenu(this._highlightedMenuItemElement, this._buildMouseEventForSubMenu(this._highlightedMenuItemElement));
311 this._subMenu._focus();
312 this._subMenu._highlightNext();
315 case "U+001B": // Escape
316 this._discardMenu(true, event); break;
318 if (!isEnterKey(event))
321 case "U+0020": // Space
322 if (this._highlightedMenuItemElement)
323 this._triggerAction(this._highlightedMenuItemElement, event);
329 _glassPaneMouseUp: function(event)
331 // Return if this is simple 'click', since dispatched on glass pane, can't use 'click' event.
332 if (event.x === this._x && event.y === this._y && new Date().getTime() - this._time < 300)
334 this._discardMenu(true, event);
339 * @param {boolean} closeParentMenus
340 * @param {!Event=} event
342 _discardMenu: function(closeParentMenus, event)
344 if (this._subMenu && !closeParentMenus)
346 if (this._glassPaneElement) {
347 var glassPane = this._glassPaneElement;
348 delete this._glassPaneElement;
349 // This can re-enter discardMenu due to blur.
350 document.body.removeChild(glassPane);
351 if (this._parentMenu) {
352 delete this._parentMenu._subMenu;
353 if (closeParentMenus)
354 this._parentMenu._discardMenu(closeParentMenus, event);
359 } else if (this._parentMenu && this._contextMenuElement.parentElement) {
360 this._discardSubMenus();
361 if (closeParentMenus)
362 this._parentMenu._discardMenu(closeParentMenus, event);
369 _discardSubMenus: function()
372 this._subMenu._discardSubMenus();
373 this._contextMenuElement.remove();
374 if (this._parentMenu)
375 delete this._parentMenu._subMenu;
379 if (!InspectorFrontendHost.showContextMenu) {
381 InspectorFrontendHost.showContextMenu = function(event, items)
383 new WebInspector.SoftContextMenu(items).show(event);