a4626e36b1b15739e502e3398b0f7114d59a6328
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / ui / SoftContextMenu.js
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  */
31 WebInspector.SoftContextMenu = function(items, parentMenu)
32 {
33     this._items = items;
34     this._parentMenu = parentMenu;
35 }
36
37 WebInspector.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
379 if (!InspectorFrontendHost.showContextMenu) {
380
381 InspectorFrontendHost.showContextMenu = function(event, items)
382 {
383     new WebInspector.SoftContextMenu(items).show(event);
384 }
385
386 }