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.<!InspectorFrontendHostAPI.ContextMenuDescriptor>} items
29 * @param {function(string)} itemSelectedCallback
30 * @param {!WebInspector.SoftContextMenu=} parentMenu
32 WebInspector
.SoftContextMenu = function(items
, itemSelectedCallback
, parentMenu
)
35 this._itemSelectedCallback
= itemSelectedCallback
;
36 this._parentMenu
= parentMenu
;
39 WebInspector
.SoftContextMenu
.prototype = {
41 * @param {!Document} document
45 show: function(document
, x
, y
)
47 if (!this._items
.length
)
50 this._document
= document
;
53 this._time
= new Date().getTime();
55 // Create context menu.
56 this.element
= createElementWithClass("div", "soft-context-menu");
57 var root
= WebInspector
.createShadowRootWithCoreStyles(this.element
);
58 root
.appendChild(WebInspector
.Widget
.createStyleElement("ui/softContextMenu.css"));
59 this._contextMenuElement
= root
.createChild("div");
60 this.element
.style
.top
= y
+ "px";
61 this.element
.style
.left
= x
+ "px";
63 var maxHeight
= WebInspector
.Dialog
.modalHostView().element
.offsetHeight
;
64 maxHeight
-= y
- WebInspector
.Dialog
.modalHostView().element
.totalOffsetTop();
65 this.element
.style
.maxHeight
= maxHeight
+ "px";
67 this._contextMenuElement
.tabIndex
= 0;
68 this._contextMenuElement
.addEventListener("mouseup", consumeEvent
, false);
69 this._contextMenuElement
.addEventListener("keydown", this._menuKeyDown
.bind(this), false);
71 for (var i
= 0; i
< this._items
.length
; ++i
)
72 this._contextMenuElement
.appendChild(this._createMenuItem(this._items
[i
]));
74 // Install glass pane capturing events.
75 if (!this._parentMenu
) {
76 this._glassPaneElement
= createElementWithClass("div", "soft-context-menu-glass-pane fill");
77 this._glassPaneElement
.tabIndex
= 0;
78 this._glassPaneElement
.addEventListener("mouseup", this._glassPaneMouseUp
.bind(this), false);
79 this._glassPaneElement
.appendChild(this.element
);
80 document
.body
.appendChild(this._glassPaneElement
);
81 this._discardMenuOnResizeListener
= this._discardMenu
.bind(this, true);
82 document
.defaultView
.addEventListener("resize", this._discardMenuOnResizeListener
, false);
85 this._parentMenu
._parentGlassPaneElement().appendChild(this.element
);
88 // Re-position menu in case it does not fit.
89 if (document
.body
.offsetWidth
< this.element
.offsetLeft
+ this.element
.offsetWidth
)
90 this.element
.style
.left
= Math
.max(0, document
.body
.offsetWidth
- this.element
.offsetWidth
) + "px";
91 if (document
.body
.offsetHeight
< this.element
.offsetTop
+ this.element
.offsetHeight
)
92 this.element
.style
.top
= Math
.max(0, document
.body
.offsetHeight
- this.element
.offsetHeight
) + "px";
97 this._discardMenu(true);
100 _parentGlassPaneElement: function()
102 if (this._glassPaneElement
)
103 return this._glassPaneElement
;
104 if (this._parentMenu
)
105 return this._parentMenu
._parentGlassPaneElement();
109 _createMenuItem: function(item
)
111 if (item
.type
=== "separator")
112 return this._createSeparator();
114 if (item
.type
=== "subMenu")
115 return this._createSubMenu(item
);
117 var menuItemElement
= createElementWithClass("div", "soft-context-menu-item");
118 var checkMarkElement
= menuItemElement
.createChild("div", "checkmark");
120 checkMarkElement
.style
.opacity
= "0";
123 var wrapper
= menuItemElement
.createChild("div", "soft-context-menu-custom-item");
124 wrapper
.appendChild(item
.element
);
125 menuItemElement
._isCustom
= true;
126 return menuItemElement
;
129 menuItemElement
.createTextChild(item
.label
);
130 menuItemElement
.createChild("span", "soft-context-menu-shortcut").textContent
= item
.shortcut
;
132 menuItemElement
.addEventListener("mousedown", this._menuItemMouseDown
.bind(this), false);
133 menuItemElement
.addEventListener("mouseup", this._menuItemMouseUp
.bind(this), false);
135 // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
136 menuItemElement
.addEventListener("mouseover", this._menuItemMouseOver
.bind(this), false);
137 menuItemElement
.addEventListener("mouseleave", this._menuItemMouseLeave
.bind(this), false);
139 menuItemElement
._actionId
= item
.id
;
140 return menuItemElement
;
143 _createSubMenu: function(item
)
145 var menuItemElement
= createElementWithClass("div", "soft-context-menu-item");
146 menuItemElement
._subItems
= item
.subItems
;
148 // Occupy the same space on the left in all items.
149 var checkMarkElement
= menuItemElement
.createChild("span", "soft-context-menu-item-checkmark");
150 checkMarkElement
.textContent
= "\u2713 "; // Checkmark Unicode symbol
151 checkMarkElement
.style
.opacity
= "0";
153 menuItemElement
.createTextChild(item
.label
);
155 var subMenuArrowElement
= menuItemElement
.createChild("span", "soft-context-menu-item-submenu-arrow");
156 subMenuArrowElement
.textContent
= "\u25B6"; // BLACK RIGHT-POINTING TRIANGLE
158 menuItemElement
.addEventListener("mousedown", this._menuItemMouseDown
.bind(this), false);
159 menuItemElement
.addEventListener("mouseup", this._menuItemMouseUp
.bind(this), false);
161 // Manually manage hover highlight since :hover does not work in case of click-and-hold menu invocation.
162 menuItemElement
.addEventListener("mouseover", this._menuItemMouseOver
.bind(this), false);
163 menuItemElement
.addEventListener("mouseleave", this._menuItemMouseLeave
.bind(this), false);
165 return menuItemElement
;
168 _createSeparator: function()
170 var separatorElement
= createElementWithClass("div", "soft-context-menu-separator");
171 separatorElement
._isSeparator
= true;
172 separatorElement
.addEventListener("mouseover", this._hideSubMenu
.bind(this), false);
173 separatorElement
.createChild("div", "separator-line");
174 return separatorElement
;
177 _menuItemMouseDown: function(event
)
179 // Do not let separator's mouse down hit menu's handler - we need to receive mouse up!
183 _menuItemMouseUp: function(event
)
185 this._triggerAction(event
.target
, event
);
191 this._contextMenuElement
.focus();
194 _triggerAction: function(menuItemElement
, event
)
196 if (!menuItemElement
._subItems
) {
197 this._discardMenu(true, event
);
198 if (typeof menuItemElement
._actionId
!== "undefined") {
199 this._itemSelectedCallback(menuItemElement
._actionId
);
200 delete menuItemElement
._actionId
;
205 this._showSubMenu(menuItemElement
);
209 _showSubMenu: function(menuItemElement
)
211 if (menuItemElement
._subMenuTimer
) {
212 clearTimeout(menuItemElement
._subMenuTimer
);
213 delete menuItemElement
._subMenuTimer
;
218 this._subMenu
= new WebInspector
.SoftContextMenu(menuItemElement
._subItems
, this._itemSelectedCallback
, this);
219 var menuLeft
= menuItemElement
.totalOffsetLeft();
220 var menuRight
= menuLeft
+ menuItemElement
.offsetWidth
;
221 var menuX
= menuRight
- 3;
222 if (menuRight
+ menuItemElement
.offsetWidth
> this._document
.body
.offsetWidth
)
223 menuX
= Math
.max(0, menuLeft
- menuItemElement
.offsetWidth
);
224 this._subMenu
.show(this._document
, menuX
, menuItemElement
.totalOffsetTop() - 1);
227 _hideSubMenu: function()
231 this._subMenu
._discardSubMenus();
235 _menuItemMouseOver: function(event
)
237 this._highlightMenuItem(event
.target
);
240 _menuItemMouseLeave: function(event
)
242 if (!this._subMenu
|| !event
.relatedTarget
) {
243 this._highlightMenuItem(null);
247 var relatedTarget
= event
.relatedTarget
;
248 if (relatedTarget
.classList
.contains("soft-context-menu-glass-pane"))
249 this._highlightMenuItem(null);
252 _highlightMenuItem: function(menuItemElement
)
254 if (this._highlightedMenuItemElement
=== menuItemElement
)
258 if (this._highlightedMenuItemElement
) {
259 this._highlightedMenuItemElement
.classList
.remove("soft-context-menu-item-mouse-over");
260 if (this._highlightedMenuItemElement
._subItems
&& this._highlightedMenuItemElement
._subMenuTimer
) {
261 clearTimeout(this._highlightedMenuItemElement
._subMenuTimer
);
262 delete this._highlightedMenuItemElement
._subMenuTimer
;
265 this._highlightedMenuItemElement
= menuItemElement
;
266 if (this._highlightedMenuItemElement
) {
267 this._highlightedMenuItemElement
.classList
.add("soft-context-menu-item-mouse-over");
268 this._contextMenuElement
.focus();
269 if (this._highlightedMenuItemElement
._subItems
&& !this._highlightedMenuItemElement
._subMenuTimer
)
270 this._highlightedMenuItemElement
._subMenuTimer
= setTimeout(this._showSubMenu
.bind(this, this._highlightedMenuItemElement
), 150);
274 _highlightPrevious: function()
276 var menuItemElement
= this._highlightedMenuItemElement
? this._highlightedMenuItemElement
.previousSibling
: this._contextMenuElement
.lastChild
;
277 while (menuItemElement
&& (menuItemElement
._isSeparator
|| menuItemElement
._isCustom
))
278 menuItemElement
= menuItemElement
.previousSibling
;
280 this._highlightMenuItem(menuItemElement
);
283 _highlightNext: function()
285 var menuItemElement
= this._highlightedMenuItemElement
? this._highlightedMenuItemElement
.nextSibling
: this._contextMenuElement
.firstChild
;
286 while (menuItemElement
&& (menuItemElement
._isSeparator
|| menuItemElement
._isCustom
))
287 menuItemElement
= menuItemElement
.nextSibling
;
289 this._highlightMenuItem(menuItemElement
);
292 _menuKeyDown: function(event
)
294 switch (event
.keyIdentifier
) {
296 this._highlightPrevious(); break;
298 this._highlightNext(); break;
300 if (this._parentMenu
) {
301 this._highlightMenuItem(null);
302 this._parentMenu
._focus();
306 if (!this._highlightedMenuItemElement
)
308 if (this._highlightedMenuItemElement
._subItems
) {
309 this._showSubMenu(this._highlightedMenuItemElement
);
310 this._subMenu
._focus();
311 this._subMenu
._highlightNext();
314 case "U+001B": // Escape
315 this._discardMenu(true, event
); break;
317 if (!isEnterKey(event
))
320 case "U+0020": // Space
321 if (this._highlightedMenuItemElement
)
322 this._triggerAction(this._highlightedMenuItemElement
, event
);
328 _glassPaneMouseUp: function(event
)
330 // Return if this is simple 'click', since dispatched on glass pane, can't use 'click' event.
331 if (new Date().getTime() - this._time
< 300)
333 if (event
.target
=== this.element
)
335 this._discardMenu(true, event
);
340 * @param {boolean} closeParentMenus
341 * @param {!Event=} event
343 _discardMenu: function(closeParentMenus
, event
)
345 if (this._subMenu
&& !closeParentMenus
)
347 if (this._glassPaneElement
) {
348 var glassPane
= this._glassPaneElement
;
349 delete this._glassPaneElement
;
350 // This can re-enter discardMenu due to blur.
351 this._document
.body
.removeChild(glassPane
);
352 if (this._parentMenu
) {
353 delete this._parentMenu
._subMenu
;
354 if (closeParentMenus
)
355 this._parentMenu
._discardMenu(closeParentMenus
, event
);
360 } else if (this._parentMenu
&& this._contextMenuElement
.parentElement
) {
361 this._discardSubMenus();
362 if (closeParentMenus
)
363 this._parentMenu
._discardMenu(closeParentMenus
, event
);
368 if (this._discardMenuOnResizeListener
) {
369 this._document
.defaultView
.removeEventListener(this._discardMenuOnResizeListener
);
370 delete this._discardMenuOnResizeListener
;
374 _discardSubMenus: function()
377 this._subMenu
._discardSubMenus();
378 this._contextMenuElement
.remove();
379 if (this._parentMenu
)
380 delete this._parentMenu
._subMenu
;