Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / ui / SoftContextMenu.js
blobf56780e223c0ca7b82da65e77a89b0c46ee4cb8a
1 /*
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
6 * are met:
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.
26 /**
27 * @constructor
28 * @param {!Array.<!InspectorFrontendHostAPI.ContextMenuDescriptor>} items
29 * @param {function(string)} itemSelectedCallback
30 * @param {!WebInspector.SoftContextMenu=} parentMenu
32 WebInspector.SoftContextMenu = function(items, itemSelectedCallback, parentMenu)
34 this._items = items;
35 this._itemSelectedCallback = itemSelectedCallback;
36 this._parentMenu = parentMenu;
39 WebInspector.SoftContextMenu.prototype = {
40 /**
41 * @param {!Document} document
42 * @param {number} x
43 * @param {number} y
45 show: function(document, x, y)
47 if (!this._items.length)
48 return;
50 this._document = document;
51 this._x = x;
52 this._y = y;
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);
83 this._focus();
84 } else {
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";
95 discard: function()
97 this._discardMenu(true);
100 _parentGlassPaneElement: function()
102 if (this._glassPaneElement)
103 return this._glassPaneElement;
104 if (this._parentMenu)
105 return this._parentMenu._parentGlassPaneElement();
106 return null;
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");
119 if (!item.checked)
120 checkMarkElement.style.opacity = "0";
122 if (item.element) {
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!
180 event.consume(true);
183 _menuItemMouseUp: function(event)
185 this._triggerAction(event.target, event);
186 event.consume();
189 _focus: function()
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;
202 return;
205 this._showSubMenu(menuItemElement);
206 event.consume();
209 _showSubMenu: function(menuItemElement)
211 if (menuItemElement._subMenuTimer) {
212 clearTimeout(menuItemElement._subMenuTimer);
213 delete menuItemElement._subMenuTimer;
215 if (this._subMenu)
216 return;
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()
229 if (!this._subMenu)
230 return;
231 this._subMenu._discardSubMenus();
232 this._focus();
235 _menuItemMouseOver: function(event)
237 this._highlightMenuItem(event.target);
240 _menuItemMouseLeave: function(event)
242 if (!this._subMenu || !event.relatedTarget) {
243 this._highlightMenuItem(null);
244 return;
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)
255 return;
257 this._hideSubMenu();
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;
279 if (menuItemElement)
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;
288 if (menuItemElement)
289 this._highlightMenuItem(menuItemElement);
292 _menuKeyDown: function(event)
294 switch (event.keyIdentifier) {
295 case "Up":
296 this._highlightPrevious(); break;
297 case "Down":
298 this._highlightNext(); break;
299 case "Left":
300 if (this._parentMenu) {
301 this._highlightMenuItem(null);
302 this._parentMenu._focus();
304 break;
305 case "Right":
306 if (!this._highlightedMenuItemElement)
307 break;
308 if (this._highlightedMenuItemElement._subItems) {
309 this._showSubMenu(this._highlightedMenuItemElement);
310 this._subMenu._focus();
311 this._subMenu._highlightNext();
313 break;
314 case "U+001B": // Escape
315 this._discardMenu(true, event); break;
316 case "Enter":
317 if (!isEnterKey(event))
318 break;
319 // Fall through
320 case "U+0020": // Space
321 if (this._highlightedMenuItemElement)
322 this._triggerAction(this._highlightedMenuItemElement, event);
323 break;
325 event.consume(true);
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)
332 return;
333 if (event.target === this.element)
334 return;
335 this._discardMenu(true, event);
336 event.consume();
340 * @param {boolean} closeParentMenus
341 * @param {!Event=} event
343 _discardMenu: function(closeParentMenus, event)
345 if (this._subMenu && !closeParentMenus)
346 return;
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);
358 if (event)
359 event.consume(true);
360 } else if (this._parentMenu && this._contextMenuElement.parentElement) {
361 this._discardSubMenus();
362 if (closeParentMenus)
363 this._parentMenu._discardMenu(closeParentMenus, event);
365 if (event)
366 event.consume(true);
368 if (this._discardMenuOnResizeListener) {
369 this._document.defaultView.removeEventListener(this._discardMenuOnResizeListener);
370 delete this._discardMenuOnResizeListener;
374 _discardSubMenus: function()
376 if (this._subMenu)
377 this._subMenu._discardSubMenus();
378 this._contextMenuElement.remove();
379 if (this._parentMenu)
380 delete this._parentMenu._subMenu;