• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2011 Google Inc. All Rights Reserved.
3 *
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.
12 *
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.
24 */
25
26/**
27 * @constructor
28 * @param {!Array.<!WebInspector.ContextMenuItem>} items
29 * @param {!WebInspector.SoftContextMenu=} parentMenu
30 */
31WebInspector.SoftContextMenu = function(items, parentMenu)
32{
33    this._items = items;
34    this._parentMenu = parentMenu;
35}
36
37WebInspector.SoftContextMenu.prototype = {
38    /**
39     * @param {!Event} event
40     */
41    show: function(event)
42    {
43        this._x = event.x;
44        this._y = event.y;
45        this._time = new Date().getTime();
46
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;
56        }
57
58        // Create context menu.
59        var targetRect;
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";
65
66        this._contextMenuElement.addEventListener("mouseup", consumeEvent, false);
67        this._contextMenuElement.addEventListener("keydown", this._menuKeyDown.bind(this), false);
68
69        for (var i = 0; i < this._items.length; ++i)
70            this._contextMenuElement.appendChild(this._createMenuItem(this._items[i]));
71
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);
80            this._focus();
81        } else
82            this._parentMenu._parentGlassPaneElement().appendChild(this._contextMenuElement);
83
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";
89
90        event.consume(true);
91    },
92
93    _parentGlassPaneElement: function()
94    {
95        if (this._glassPaneElement)
96            return this._glassPaneElement;
97        if (this._parentMenu)
98            return this._parentMenu._parentGlassPaneElement();
99        return null;
100    },
101
102    _createMenuItem: function(item)
103    {
104        if (item.type === "separator")
105            return this._createSeparator();
106
107        if (item.type === "subMenu")
108            return this._createSubMenu(item);
109
110        var menuItemElement = document.createElement("div");
111        menuItemElement.className = "soft-context-menu-item";
112
113        var checkMarkElement = document.createElement("span");
114        checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol
115        checkMarkElement.className = "soft-context-menu-item-checkmark";
116        if (!item.checked)
117            checkMarkElement.style.opacity = "0";
118
119        menuItemElement.appendChild(checkMarkElement);
120        menuItemElement.appendChild(document.createTextNode(item.label));
121
122        menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
123        menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
124
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);
128
129        menuItemElement._actionId = item.id;
130        return menuItemElement;
131    },
132
133    _createSubMenu: function(item)
134    {
135        var menuItemElement = document.createElement("div");
136        menuItemElement.className = "soft-context-menu-item";
137        menuItemElement._subItems = item.subItems;
138
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);
145
146        var subMenuArrowElement = document.createElement("span");
147        subMenuArrowElement.textContent = "\u25B6"; // BLACK RIGHT-POINTING TRIANGLE
148        subMenuArrowElement.className = "soft-context-menu-item-submenu-arrow";
149
150        menuItemElement.appendChild(document.createTextNode(item.label));
151        menuItemElement.appendChild(subMenuArrowElement);
152
153        menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bind(this), false);
154        menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(this), false);
155
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);
159
160        return menuItemElement;
161    },
162
163    _createSeparator: function()
164    {
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;
171    },
172
173    _menuItemMouseDown: function(event)
174    {
175        // Do not let separator's mouse down hit menu's handler - we need to receive mouse up!
176        event.consume(true);
177    },
178
179    _menuItemMouseUp: function(event)
180    {
181        this._triggerAction(event.target, event);
182        event.consume();
183    },
184
185    _focus: function()
186    {
187        this._contextMenuElement.focus();
188    },
189
190    _triggerAction: function(menuItemElement, event)
191    {
192        if (!menuItemElement._subItems) {
193            this._discardMenu(true, event);
194            if (typeof menuItemElement._actionId !== "undefined") {
195                WebInspector.contextMenuItemSelected(menuItemElement._actionId);
196                delete menuItemElement._actionId;
197            }
198            return;
199        }
200
201        this._showSubMenu(menuItemElement, event);
202        event.consume();
203    },
204
205    _showSubMenu: function(menuItemElement, event)
206    {
207        if (menuItemElement._subMenuTimer) {
208            clearTimeout(menuItemElement._subMenuTimer);
209            delete menuItemElement._subMenuTimer;
210        }
211        if (this._subMenu)
212            return;
213
214        this._subMenu = new WebInspector.SoftContextMenu(menuItemElement._subItems, this);
215        this._subMenu.show(this._buildMouseEventForSubMenu(menuItemElement));
216    },
217
218    _buildMouseEventForSubMenu: function(subMenuItemElement)
219    {
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() {} };
226    },
227
228    _hideSubMenu: function()
229    {
230        if (!this._subMenu)
231            return;
232        this._subMenu._discardSubMenus();
233        this._focus();
234    },
235
236    _menuItemMouseOver: function(event)
237    {
238        this._highlightMenuItem(event.target);
239    },
240
241    _menuItemMouseOut: function(event)
242    {
243        if (!this._subMenu || !event.relatedTarget) {
244            this._highlightMenuItem(null);
245            return;
246        }
247
248        var relatedTarget = event.relatedTarget;
249        if (this._contextMenuElement.isSelfOrAncestor(relatedTarget) || relatedTarget.classList.contains("soft-context-menu-glass-pane"))
250            this._highlightMenuItem(null);
251    },
252
253    _highlightMenuItem: function(menuItemElement)
254    {
255        if (this._highlightedMenuItemElement ===  menuItemElement)
256            return;
257
258        this._hideSubMenu();
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;
264            }
265        }
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);
272        }
273    },
274
275    _highlightPrevious: function()
276    {
277        var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.previousSibling : this._contextMenuElement.lastChild;
278        while (menuItemElement && menuItemElement._isSeparator)
279            menuItemElement = menuItemElement.previousSibling;
280        if (menuItemElement)
281            this._highlightMenuItem(menuItemElement);
282    },
283
284    _highlightNext: function()
285    {
286        var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMenuItemElement.nextSibling : this._contextMenuElement.firstChild;
287        while (menuItemElement && menuItemElement._isSeparator)
288            menuItemElement = menuItemElement.nextSibling;
289        if (menuItemElement)
290            this._highlightMenuItem(menuItemElement);
291    },
292
293    _menuKeyDown: function(event)
294    {
295        switch (event.keyIdentifier) {
296        case "Up":
297            this._highlightPrevious(); break;
298        case "Down":
299            this._highlightNext(); break;
300        case "Left":
301            if (this._parentMenu) {
302                this._highlightMenuItem(null);
303                this._parentMenu._focus();
304            }
305            break;
306        case "Right":
307            if (!this._highlightedMenuItemElement)
308                break;
309            if (this._highlightedMenuItemElement._subItems) {
310                this._showSubMenu(this._highlightedMenuItemElement, this._buildMouseEventForSubMenu(this._highlightedMenuItemElement));
311                this._subMenu._focus();
312                this._subMenu._highlightNext();
313            }
314            break;
315        case "U+001B": // Escape
316            this._discardMenu(true, event); break;
317        case "Enter":
318            if (!isEnterKey(event))
319                break;
320            // Fall through
321        case "U+0020": // Space
322            if (this._highlightedMenuItemElement)
323                this._triggerAction(this._highlightedMenuItemElement, event);
324            break;
325        }
326        event.consume(true);
327    },
328
329    _glassPaneMouseUp: function(event)
330    {
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)
333            return;
334        this._discardMenu(true, event);
335        event.consume();
336    },
337
338    /**
339     * @param {boolean} closeParentMenus
340     * @param {!Event=} event
341     */
342    _discardMenu: function(closeParentMenus, event)
343    {
344        if (this._subMenu && !closeParentMenus)
345            return;
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);
355            }
356
357            if (event)
358                event.consume(true);
359        } else if (this._parentMenu && this._contextMenuElement.parentElement) {
360            this._discardSubMenus();
361            if (closeParentMenus)
362                this._parentMenu._discardMenu(closeParentMenus, event);
363
364            if (event)
365                event.consume(true);
366        }
367    },
368
369    _discardSubMenus: function()
370    {
371        if (this._subMenu)
372            this._subMenu._discardSubMenus();
373        this._contextMenuElement.remove();
374        if (this._parentMenu)
375            delete this._parentMenu._subMenu;
376    }
377}
378
379if (!InspectorFrontendHost.showContextMenu) {
380
381InspectorFrontendHost.showContextMenu = function(event, items)
382{
383    new WebInspector.SoftContextMenu(items).show(event);
384}
385
386}
387