OLD | NEW |
1 /* | 1 /* |
2 * Copyright (C) 2011 Google Inc. All Rights Reserved. | 2 * Copyright (C) 2011 Google Inc. All Rights Reserved. |
3 * | 3 * |
4 * Redistribution and use in source and binary forms, with or without | 4 * Redistribution and use in source and binary forms, with or without |
5 * modification, are permitted provided that the following conditions | 5 * modification, are permitted provided that the following conditions |
6 * are met: | 6 * are met: |
7 * 1. Redistributions of source code must retain the above copyright | 7 * 1. Redistributions of source code must retain the above copyright |
8 * notice, this list of conditions and the following disclaimer. | 8 * notice, this list of conditions and the following disclaimer. |
9 * 2. Redistributions in binary form must reproduce the above copyright | 9 * 2. Redistributions in binary form must reproduce the above copyright |
10 * notice, this list of conditions and the following disclaimer in the | 10 * notice, this list of conditions and the following disclaimer in the |
11 * documentation and/or other materials provided with the distribution. | 11 * documentation and/or other materials provided with the distribution. |
12 * | 12 * |
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY | 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | 15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR | 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | 18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | 19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | 20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | 21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 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. | 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
24 */ | 24 */ |
25 | |
26 /** | 25 /** |
27 * @constructor | 26 * @unrestricted |
28 * @param {!Array.<!InspectorFrontendHostAPI.ContextMenuDescriptor>} items | |
29 * @param {function(string)} itemSelectedCallback | |
30 * @param {!WebInspector.SoftContextMenu=} parentMenu | |
31 */ | 27 */ |
32 WebInspector.SoftContextMenu = function(items, itemSelectedCallback, parentMenu) | 28 WebInspector.SoftContextMenu = class { |
33 { | 29 /** |
| 30 * @param {!Array.<!InspectorFrontendHostAPI.ContextMenuDescriptor>} items |
| 31 * @param {function(string)} itemSelectedCallback |
| 32 * @param {!WebInspector.SoftContextMenu=} parentMenu |
| 33 */ |
| 34 constructor(items, itemSelectedCallback, parentMenu) { |
34 this._items = items; | 35 this._items = items; |
35 this._itemSelectedCallback = itemSelectedCallback; | 36 this._itemSelectedCallback = itemSelectedCallback; |
36 this._parentMenu = parentMenu; | 37 this._parentMenu = parentMenu; |
| 38 } |
| 39 |
| 40 /** |
| 41 * @param {!Document} document |
| 42 * @param {number} x |
| 43 * @param {number} y |
| 44 */ |
| 45 show(document, x, y) { |
| 46 if (!this._items.length) |
| 47 return; |
| 48 |
| 49 this._document = document; |
| 50 this._x = x; |
| 51 this._y = y; |
| 52 this._time = new Date().getTime(); |
| 53 |
| 54 // Create context menu. |
| 55 this.element = createElementWithClass('div', 'soft-context-menu'); |
| 56 var root = WebInspector.createShadowRootWithCoreStyles(this.element, 'ui/sof
tContextMenu.css'); |
| 57 this._contextMenuElement = root.createChild('div'); |
| 58 this.element.style.top = y + 'px'; |
| 59 var subMenuOverlap = 3; |
| 60 this.element.style.left = (this._parentMenu ? x - subMenuOverlap : x) + 'px'
; |
| 61 |
| 62 this._contextMenuElement.tabIndex = 0; |
| 63 this._contextMenuElement.addEventListener('mouseup', (e) => e.consume(), fal
se); |
| 64 this._contextMenuElement.addEventListener('keydown', this._menuKeyDown.bind(
this), false); |
| 65 |
| 66 for (var i = 0; i < this._items.length; ++i) |
| 67 this._contextMenuElement.appendChild(this._createMenuItem(this._items[i]))
; |
| 68 |
| 69 // Install glass pane capturing events. |
| 70 if (!this._parentMenu) { |
| 71 this._glassPaneElement = createElementWithClass('div', 'soft-context-menu-
glass-pane fill'); |
| 72 this._glassPaneElement.tabIndex = 0; |
| 73 this._glassPaneElement.style.zIndex = '20000'; |
| 74 this._glassPaneElement.addEventListener('mouseup', this._glassPaneMouseUp.
bind(this), false); |
| 75 this._glassPaneElement.appendChild(this.element); |
| 76 document.body.appendChild(this._glassPaneElement); |
| 77 this._discardMenuOnResizeListener = this._discardMenu.bind(this, true); |
| 78 document.defaultView.addEventListener('resize', this._discardMenuOnResizeL
istener, false); |
| 79 } else { |
| 80 this._parentMenu._parentGlassPaneElement().appendChild(this.element); |
| 81 } |
| 82 |
| 83 // Re-position menu in case it does not fit. |
| 84 if (document.body.offsetWidth < this.element.offsetLeft + this.element.offse
tWidth) { |
| 85 this.element.style.left = |
| 86 Math.max( |
| 87 WebInspector.Dialog.modalHostView().element.totalOffsetLeft(), thi
s._parentMenu ? |
| 88 this._parentMenu.element.offsetLeft - this.element.offsetWidth
+ subMenuOverlap : |
| 89 document.body.offsetWidth - this.element.offsetWidth) + |
| 90 'px'; |
| 91 } |
| 92 |
| 93 // Move submenus upwards if it does not fit. |
| 94 if (this._parentMenu && document.body.offsetHeight < this.element.offsetTop
+ this.element.offsetHeight) { |
| 95 y = Math.max( |
| 96 WebInspector.Dialog.modalHostView().element.totalOffsetTop(), |
| 97 document.body.offsetHeight - this.element.offsetHeight); |
| 98 this.element.style.top = y + 'px'; |
| 99 } |
| 100 |
| 101 var maxHeight = WebInspector.Dialog.modalHostView().element.offsetHeight; |
| 102 maxHeight -= y - WebInspector.Dialog.modalHostView().element.totalOffsetTop(
); |
| 103 this.element.style.maxHeight = maxHeight + 'px'; |
| 104 |
| 105 this._focus(); |
| 106 } |
| 107 |
| 108 discard() { |
| 109 this._discardMenu(true); |
| 110 } |
| 111 |
| 112 _parentGlassPaneElement() { |
| 113 if (this._glassPaneElement) |
| 114 return this._glassPaneElement; |
| 115 if (this._parentMenu) |
| 116 return this._parentMenu._parentGlassPaneElement(); |
| 117 return null; |
| 118 } |
| 119 |
| 120 _createMenuItem(item) { |
| 121 if (item.type === 'separator') |
| 122 return this._createSeparator(); |
| 123 |
| 124 if (item.type === 'subMenu') |
| 125 return this._createSubMenu(item); |
| 126 |
| 127 var menuItemElement = createElementWithClass('div', 'soft-context-menu-item'
); |
| 128 var checkMarkElement = menuItemElement.createChild('div', 'checkmark'); |
| 129 if (!item.checked) |
| 130 checkMarkElement.style.opacity = '0'; |
| 131 |
| 132 if (item.element) { |
| 133 var wrapper = menuItemElement.createChild('div', 'soft-context-menu-custom
-item'); |
| 134 wrapper.appendChild(item.element); |
| 135 menuItemElement._isCustom = true; |
| 136 return menuItemElement; |
| 137 } |
| 138 |
| 139 if (!item.enabled) |
| 140 menuItemElement.classList.add('soft-context-menu-disabled'); |
| 141 menuItemElement.createTextChild(item.label); |
| 142 menuItemElement.createChild('span', 'soft-context-menu-shortcut').textConten
t = item.shortcut; |
| 143 |
| 144 menuItemElement.addEventListener('mousedown', this._menuItemMouseDown.bind(t
his), false); |
| 145 menuItemElement.addEventListener('mouseup', this._menuItemMouseUp.bind(this)
, false); |
| 146 |
| 147 // Manually manage hover highlight since :hover does not work in case of cli
ck-and-hold menu invocation. |
| 148 menuItemElement.addEventListener('mouseover', this._menuItemMouseOver.bind(t
his), false); |
| 149 menuItemElement.addEventListener('mouseleave', this._menuItemMouseLeave.bind
(this), false); |
| 150 |
| 151 menuItemElement._actionId = item.id; |
| 152 return menuItemElement; |
| 153 } |
| 154 |
| 155 _createSubMenu(item) { |
| 156 var menuItemElement = createElementWithClass('div', 'soft-context-menu-item'
); |
| 157 menuItemElement._subItems = item.subItems; |
| 158 |
| 159 // Occupy the same space on the left in all items. |
| 160 var checkMarkElement = menuItemElement.createChild('span', 'soft-context-men
u-item-checkmark checkmark'); |
| 161 checkMarkElement.textContent = '\u2713 '; // Checkmark Unicode symbol |
| 162 checkMarkElement.style.opacity = '0'; |
| 163 |
| 164 menuItemElement.createTextChild(item.label); |
| 165 |
| 166 var subMenuArrowElement = menuItemElement.createChild('span', 'soft-context-
menu-item-submenu-arrow'); |
| 167 subMenuArrowElement.textContent = '\u25B6'; // BLACK RIGHT-POINTING TRIANGL
E |
| 168 |
| 169 menuItemElement.addEventListener('mousedown', this._menuItemMouseDown.bind(t
his), false); |
| 170 menuItemElement.addEventListener('mouseup', this._menuItemMouseUp.bind(this)
, false); |
| 171 |
| 172 // Manually manage hover highlight since :hover does not work in case of cli
ck-and-hold menu invocation. |
| 173 menuItemElement.addEventListener('mouseover', this._menuItemMouseOver.bind(t
his), false); |
| 174 menuItemElement.addEventListener('mouseleave', this._menuItemMouseLeave.bind
(this), false); |
| 175 |
| 176 return menuItemElement; |
| 177 } |
| 178 |
| 179 _createSeparator() { |
| 180 var separatorElement = createElementWithClass('div', 'soft-context-menu-sepa
rator'); |
| 181 separatorElement._isSeparator = true; |
| 182 separatorElement.createChild('div', 'separator-line'); |
| 183 return separatorElement; |
| 184 } |
| 185 |
| 186 _menuItemMouseDown(event) { |
| 187 // Do not let separator's mouse down hit menu's handler - we need to receive
mouse up! |
| 188 event.consume(true); |
| 189 } |
| 190 |
| 191 _menuItemMouseUp(event) { |
| 192 this._triggerAction(event.target, event); |
| 193 event.consume(); |
| 194 } |
| 195 |
| 196 _focus() { |
| 197 this._contextMenuElement.focus(); |
| 198 } |
| 199 |
| 200 _triggerAction(menuItemElement, event) { |
| 201 if (!menuItemElement._subItems) { |
| 202 this._discardMenu(true, event); |
| 203 if (typeof menuItemElement._actionId !== 'undefined') { |
| 204 this._itemSelectedCallback(menuItemElement._actionId); |
| 205 delete menuItemElement._actionId; |
| 206 } |
| 207 return; |
| 208 } |
| 209 |
| 210 this._showSubMenu(menuItemElement); |
| 211 event.consume(); |
| 212 } |
| 213 |
| 214 _showSubMenu(menuItemElement) { |
| 215 if (menuItemElement._subMenuTimer) { |
| 216 clearTimeout(menuItemElement._subMenuTimer); |
| 217 delete menuItemElement._subMenuTimer; |
| 218 } |
| 219 if (this._subMenu) |
| 220 return; |
| 221 |
| 222 this._subMenu = new WebInspector.SoftContextMenu(menuItemElement._subItems,
this._itemSelectedCallback, this); |
| 223 var topPadding = 4; |
| 224 this._subMenu.show( |
| 225 this._document, menuItemElement.totalOffsetLeft() + menuItemElement.offs
etWidth, |
| 226 menuItemElement.totalOffsetTop() - 1 - topPadding); |
| 227 } |
| 228 |
| 229 _hideSubMenu() { |
| 230 if (!this._subMenu) |
| 231 return; |
| 232 this._subMenu._discardSubMenus(); |
| 233 this._focus(); |
| 234 } |
| 235 |
| 236 _menuItemMouseOver(event) { |
| 237 this._highlightMenuItem(event.target, true); |
| 238 } |
| 239 |
| 240 _menuItemMouseLeave(event) { |
| 241 if (!this._subMenu || !event.relatedTarget) { |
| 242 this._highlightMenuItem(null, true); |
| 243 return; |
| 244 } |
| 245 |
| 246 var relatedTarget = event.relatedTarget; |
| 247 if (relatedTarget.classList.contains('soft-context-menu-glass-pane')) |
| 248 this._highlightMenuItem(null, true); |
| 249 } |
| 250 |
| 251 /** |
| 252 * @param {?Element} menuItemElement |
| 253 * @param {boolean} scheduleSubMenu |
| 254 */ |
| 255 _highlightMenuItem(menuItemElement, scheduleSubMenu) { |
| 256 if (this._highlightedMenuItemElement === menuItemElement) |
| 257 return; |
| 258 |
| 259 this._hideSubMenu(); |
| 260 if (this._highlightedMenuItemElement) { |
| 261 this._highlightedMenuItemElement.classList.remove('soft-context-menu-item-
mouse-over'); |
| 262 if (this._highlightedMenuItemElement._subItems && this._highlightedMenuIte
mElement._subMenuTimer) { |
| 263 clearTimeout(this._highlightedMenuItemElement._subMenuTimer); |
| 264 delete this._highlightedMenuItemElement._subMenuTimer; |
| 265 } |
| 266 } |
| 267 this._highlightedMenuItemElement = menuItemElement; |
| 268 if (this._highlightedMenuItemElement) { |
| 269 this._highlightedMenuItemElement.classList.add('soft-context-menu-item-mou
se-over'); |
| 270 this._contextMenuElement.focus(); |
| 271 if (scheduleSubMenu && this._highlightedMenuItemElement._subItems && |
| 272 !this._highlightedMenuItemElement._subMenuTimer) |
| 273 this._highlightedMenuItemElement._subMenuTimer = |
| 274 setTimeout(this._showSubMenu.bind(this, this._highlightedMenuItemEle
ment), 150); |
| 275 } |
| 276 } |
| 277 |
| 278 _highlightPrevious() { |
| 279 var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMe
nuItemElement.previousSibling : |
| 280 this._contextMenuEl
ement.lastChild; |
| 281 while (menuItemElement && (menuItemElement._isSeparator || menuItemElement._
isCustom)) |
| 282 menuItemElement = menuItemElement.previousSibling; |
| 283 if (menuItemElement) |
| 284 this._highlightMenuItem(menuItemElement, false); |
| 285 } |
| 286 |
| 287 _highlightNext() { |
| 288 var menuItemElement = this._highlightedMenuItemElement ? this._highlightedMe
nuItemElement.nextSibling : |
| 289 this._contextMenuEl
ement.firstChild; |
| 290 while (menuItemElement && (menuItemElement._isSeparator || menuItemElement._
isCustom)) |
| 291 menuItemElement = menuItemElement.nextSibling; |
| 292 if (menuItemElement) |
| 293 this._highlightMenuItem(menuItemElement, false); |
| 294 } |
| 295 |
| 296 _menuKeyDown(event) { |
| 297 switch (event.key) { |
| 298 case 'ArrowUp': |
| 299 this._highlightPrevious(); |
| 300 break; |
| 301 case 'ArrowDown': |
| 302 this._highlightNext(); |
| 303 break; |
| 304 case 'ArrowLeft': |
| 305 if (this._parentMenu) { |
| 306 this._highlightMenuItem(null, false); |
| 307 this._parentMenu._hideSubMenu(); |
| 308 } |
| 309 break; |
| 310 case 'ArrowRight': |
| 311 if (!this._highlightedMenuItemElement) |
| 312 break; |
| 313 if (this._highlightedMenuItemElement._subItems) { |
| 314 this._showSubMenu(this._highlightedMenuItemElement); |
| 315 this._subMenu._focus(); |
| 316 this._subMenu._highlightNext(); |
| 317 } |
| 318 break; |
| 319 case 'Escape': |
| 320 this._discardMenu(false, event); |
| 321 break; |
| 322 case 'Enter': |
| 323 if (!isEnterKey(event)) |
| 324 break; |
| 325 // Fall through |
| 326 case ' ': // Space |
| 327 if (this._highlightedMenuItemElement) |
| 328 this._triggerAction(this._highlightedMenuItemElement, event); |
| 329 if (this._highlightedMenuItemElement._subItems) { |
| 330 this._subMenu._focus(); |
| 331 this._subMenu._highlightNext(); |
| 332 } |
| 333 break; |
| 334 } |
| 335 event.consume(true); |
| 336 } |
| 337 |
| 338 _glassPaneMouseUp(event) { |
| 339 // Return if this is simple 'click', since dispatched on glass pane, can't u
se 'click' event. |
| 340 if (new Date().getTime() - this._time < 300) |
| 341 return; |
| 342 if (event.target === this.element) |
| 343 return; |
| 344 this._discardMenu(true, event); |
| 345 event.consume(); |
| 346 } |
| 347 |
| 348 /** |
| 349 * @param {boolean} closeParentMenus |
| 350 * @param {!Event=} event |
| 351 */ |
| 352 _discardMenu(closeParentMenus, event) { |
| 353 if (this._subMenu && !closeParentMenus) |
| 354 return; |
| 355 if (this._glassPaneElement) { |
| 356 var glassPane = this._glassPaneElement; |
| 357 delete this._glassPaneElement; |
| 358 // This can re-enter discardMenu due to blur. |
| 359 this._document.body.removeChild(glassPane); |
| 360 if (this._parentMenu) { |
| 361 delete this._parentMenu._subMenu; |
| 362 if (closeParentMenus) |
| 363 this._parentMenu._discardMenu(closeParentMenus, event); |
| 364 else |
| 365 this._parentMenu._focus(); |
| 366 } |
| 367 |
| 368 if (event) |
| 369 event.consume(true); |
| 370 } else if (this._parentMenu && this._contextMenuElement.parentElementOrShado
wHost()) { |
| 371 this._discardSubMenus(); |
| 372 if (closeParentMenus) |
| 373 this._parentMenu._discardMenu(closeParentMenus, event); |
| 374 else |
| 375 this._parentMenu._focus(); |
| 376 if (event) |
| 377 event.consume(true); |
| 378 } |
| 379 if (this._discardMenuOnResizeListener) { |
| 380 this._document.defaultView.removeEventListener('resize', this._discardMenu
OnResizeListener, false); |
| 381 delete this._discardMenuOnResizeListener; |
| 382 } |
| 383 } |
| 384 |
| 385 _discardSubMenus() { |
| 386 if (this._subMenu) |
| 387 this._subMenu._discardSubMenus(); |
| 388 if (this.element) |
| 389 this.element.remove(); |
| 390 if (this._parentMenu) |
| 391 delete this._parentMenu._subMenu; |
| 392 } |
37 }; | 393 }; |
38 | |
39 WebInspector.SoftContextMenu.prototype = { | |
40 /** | |
41 * @param {!Document} document | |
42 * @param {number} x | |
43 * @param {number} y | |
44 */ | |
45 show: function(document, x, y) | |
46 { | |
47 if (!this._items.length) | |
48 return; | |
49 | |
50 this._document = document; | |
51 this._x = x; | |
52 this._y = y; | |
53 this._time = new Date().getTime(); | |
54 | |
55 // Create context menu. | |
56 this.element = createElementWithClass("div", "soft-context-menu"); | |
57 var root = WebInspector.createShadowRootWithCoreStyles(this.element, "ui
/softContextMenu.css"); | |
58 this._contextMenuElement = root.createChild("div"); | |
59 this.element.style.top = y + "px"; | |
60 var subMenuOverlap = 3; | |
61 this.element.style.left = (this._parentMenu ? x - subMenuOverlap : x) +
"px"; | |
62 | |
63 this._contextMenuElement.tabIndex = 0; | |
64 this._contextMenuElement.addEventListener("mouseup", (e) => e.consume(),
false); | |
65 this._contextMenuElement.addEventListener("keydown", this._menuKeyDown.b
ind(this), false); | |
66 | |
67 for (var i = 0; i < this._items.length; ++i) | |
68 this._contextMenuElement.appendChild(this._createMenuItem(this._item
s[i])); | |
69 | |
70 // Install glass pane capturing events. | |
71 if (!this._parentMenu) { | |
72 this._glassPaneElement = createElementWithClass("div", "soft-context
-menu-glass-pane fill"); | |
73 this._glassPaneElement.tabIndex = 0; | |
74 this._glassPaneElement.style.zIndex = "20000"; | |
75 this._glassPaneElement.addEventListener("mouseup", this._glassPaneMo
useUp.bind(this), false); | |
76 this._glassPaneElement.appendChild(this.element); | |
77 document.body.appendChild(this._glassPaneElement); | |
78 this._discardMenuOnResizeListener = this._discardMenu.bind(this, tru
e); | |
79 document.defaultView.addEventListener("resize", this._discardMenuOnR
esizeListener, false); | |
80 } else { | |
81 this._parentMenu._parentGlassPaneElement().appendChild(this.element)
; | |
82 } | |
83 | |
84 // Re-position menu in case it does not fit. | |
85 if (document.body.offsetWidth < this.element.offsetLeft + this.element.o
ffsetWidth) { | |
86 this.element.style.left = Math.max(WebInspector.Dialog.modalHostView
().element.totalOffsetLeft(), this._parentMenu | |
87 ? this._parentMenu.element.offsetLeft - this.element.offsetWidth
+ subMenuOverlap | |
88 : document.body.offsetWidth - this.element.offsetWidth) + "px"; | |
89 } | |
90 | |
91 // Move submenus upwards if it does not fit. | |
92 if (this._parentMenu && document.body.offsetHeight < this.element.offset
Top + this.element.offsetHeight) { | |
93 y = Math.max(WebInspector.Dialog.modalHostView().element.totalOffset
Top(), document.body.offsetHeight - this.element.offsetHeight); | |
94 this.element.style.top = y + "px"; | |
95 } | |
96 | |
97 var maxHeight = WebInspector.Dialog.modalHostView().element.offsetHeight
; | |
98 maxHeight -= y - WebInspector.Dialog.modalHostView().element.totalOffset
Top(); | |
99 this.element.style.maxHeight = maxHeight + "px"; | |
100 | |
101 this._focus(); | |
102 }, | |
103 | |
104 discard: function() | |
105 { | |
106 this._discardMenu(true); | |
107 }, | |
108 | |
109 _parentGlassPaneElement: function() | |
110 { | |
111 if (this._glassPaneElement) | |
112 return this._glassPaneElement; | |
113 if (this._parentMenu) | |
114 return this._parentMenu._parentGlassPaneElement(); | |
115 return null; | |
116 }, | |
117 | |
118 _createMenuItem: function(item) | |
119 { | |
120 if (item.type === "separator") | |
121 return this._createSeparator(); | |
122 | |
123 if (item.type === "subMenu") | |
124 return this._createSubMenu(item); | |
125 | |
126 var menuItemElement = createElementWithClass("div", "soft-context-menu-i
tem"); | |
127 var checkMarkElement = menuItemElement.createChild("div", "checkmark"); | |
128 if (!item.checked) | |
129 checkMarkElement.style.opacity = "0"; | |
130 | |
131 if (item.element) { | |
132 var wrapper = menuItemElement.createChild("div", "soft-context-menu-
custom-item"); | |
133 wrapper.appendChild(item.element); | |
134 menuItemElement._isCustom = true; | |
135 return menuItemElement; | |
136 } | |
137 | |
138 if (!item.enabled) | |
139 menuItemElement.classList.add("soft-context-menu-disabled"); | |
140 menuItemElement.createTextChild(item.label); | |
141 menuItemElement.createChild("span", "soft-context-menu-shortcut").textCo
ntent = item.shortcut; | |
142 | |
143 menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bi
nd(this), false); | |
144 menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(t
his), false); | |
145 | |
146 // Manually manage hover highlight since :hover does not work in case of
click-and-hold menu invocation. | |
147 menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bi
nd(this), false); | |
148 menuItemElement.addEventListener("mouseleave", this._menuItemMouseLeave.
bind(this), false); | |
149 | |
150 menuItemElement._actionId = item.id; | |
151 return menuItemElement; | |
152 }, | |
153 | |
154 _createSubMenu: function(item) | |
155 { | |
156 var menuItemElement = createElementWithClass("div", "soft-context-menu-i
tem"); | |
157 menuItemElement._subItems = item.subItems; | |
158 | |
159 // Occupy the same space on the left in all items. | |
160 var checkMarkElement = menuItemElement.createChild("span", "soft-context
-menu-item-checkmark checkmark"); | |
161 checkMarkElement.textContent = "\u2713 "; // Checkmark Unicode symbol | |
162 checkMarkElement.style.opacity = "0"; | |
163 | |
164 menuItemElement.createTextChild(item.label); | |
165 | |
166 var subMenuArrowElement = menuItemElement.createChild("span", "soft-cont
ext-menu-item-submenu-arrow"); | |
167 subMenuArrowElement.textContent = "\u25B6"; // BLACK RIGHT-POINTING TRIA
NGLE | |
168 | |
169 menuItemElement.addEventListener("mousedown", this._menuItemMouseDown.bi
nd(this), false); | |
170 menuItemElement.addEventListener("mouseup", this._menuItemMouseUp.bind(t
his), false); | |
171 | |
172 // Manually manage hover highlight since :hover does not work in case of
click-and-hold menu invocation. | |
173 menuItemElement.addEventListener("mouseover", this._menuItemMouseOver.bi
nd(this), false); | |
174 menuItemElement.addEventListener("mouseleave", this._menuItemMouseLeave.
bind(this), false); | |
175 | |
176 return menuItemElement; | |
177 }, | |
178 | |
179 _createSeparator: function() | |
180 { | |
181 var separatorElement = createElementWithClass("div", "soft-context-menu-
separator"); | |
182 separatorElement._isSeparator = true; | |
183 separatorElement.createChild("div", "separator-line"); | |
184 return separatorElement; | |
185 }, | |
186 | |
187 _menuItemMouseDown: function(event) | |
188 { | |
189 // Do not let separator's mouse down hit menu's handler - we need to rec
eive mouse up! | |
190 event.consume(true); | |
191 }, | |
192 | |
193 _menuItemMouseUp: function(event) | |
194 { | |
195 this._triggerAction(event.target, event); | |
196 event.consume(); | |
197 }, | |
198 | |
199 _focus: function() | |
200 { | |
201 this._contextMenuElement.focus(); | |
202 }, | |
203 | |
204 _triggerAction: function(menuItemElement, event) | |
205 { | |
206 if (!menuItemElement._subItems) { | |
207 this._discardMenu(true, event); | |
208 if (typeof menuItemElement._actionId !== "undefined") { | |
209 this._itemSelectedCallback(menuItemElement._actionId); | |
210 delete menuItemElement._actionId; | |
211 } | |
212 return; | |
213 } | |
214 | |
215 this._showSubMenu(menuItemElement); | |
216 event.consume(); | |
217 }, | |
218 | |
219 _showSubMenu: function(menuItemElement) | |
220 { | |
221 if (menuItemElement._subMenuTimer) { | |
222 clearTimeout(menuItemElement._subMenuTimer); | |
223 delete menuItemElement._subMenuTimer; | |
224 } | |
225 if (this._subMenu) | |
226 return; | |
227 | |
228 this._subMenu = new WebInspector.SoftContextMenu(menuItemElement._subIte
ms, this._itemSelectedCallback, this); | |
229 var topPadding = 4; | |
230 this._subMenu.show(this._document, menuItemElement.totalOffsetLeft() + m
enuItemElement.offsetWidth, menuItemElement.totalOffsetTop() - 1 - topPadding); | |
231 }, | |
232 | |
233 _hideSubMenu: function() | |
234 { | |
235 if (!this._subMenu) | |
236 return; | |
237 this._subMenu._discardSubMenus(); | |
238 this._focus(); | |
239 }, | |
240 | |
241 _menuItemMouseOver: function(event) | |
242 { | |
243 this._highlightMenuItem(event.target, true); | |
244 }, | |
245 | |
246 _menuItemMouseLeave: function(event) | |
247 { | |
248 if (!this._subMenu || !event.relatedTarget) { | |
249 this._highlightMenuItem(null, true); | |
250 return; | |
251 } | |
252 | |
253 var relatedTarget = event.relatedTarget; | |
254 if (relatedTarget.classList.contains("soft-context-menu-glass-pane")) | |
255 this._highlightMenuItem(null, true); | |
256 }, | |
257 | |
258 /** | |
259 * @param {?Element} menuItemElement | |
260 * @param {boolean} scheduleSubMenu | |
261 */ | |
262 _highlightMenuItem: function(menuItemElement, scheduleSubMenu) | |
263 { | |
264 if (this._highlightedMenuItemElement === menuItemElement) | |
265 return; | |
266 | |
267 this._hideSubMenu(); | |
268 if (this._highlightedMenuItemElement) { | |
269 this._highlightedMenuItemElement.classList.remove("soft-context-menu
-item-mouse-over"); | |
270 if (this._highlightedMenuItemElement._subItems && this._highlightedM
enuItemElement._subMenuTimer) { | |
271 clearTimeout(this._highlightedMenuItemElement._subMenuTimer); | |
272 delete this._highlightedMenuItemElement._subMenuTimer; | |
273 } | |
274 } | |
275 this._highlightedMenuItemElement = menuItemElement; | |
276 if (this._highlightedMenuItemElement) { | |
277 this._highlightedMenuItemElement.classList.add("soft-context-menu-it
em-mouse-over"); | |
278 this._contextMenuElement.focus(); | |
279 if (scheduleSubMenu && this._highlightedMenuItemElement._subItems &&
!this._highlightedMenuItemElement._subMenuTimer) | |
280 this._highlightedMenuItemElement._subMenuTimer = setTimeout(this
._showSubMenu.bind(this, this._highlightedMenuItemElement), 150); | |
281 } | |
282 }, | |
283 | |
284 _highlightPrevious: function() | |
285 { | |
286 var menuItemElement = this._highlightedMenuItemElement ? this._highlight
edMenuItemElement.previousSibling : this._contextMenuElement.lastChild; | |
287 while (menuItemElement && (menuItemElement._isSeparator || menuItemEleme
nt._isCustom)) | |
288 menuItemElement = menuItemElement.previousSibling; | |
289 if (menuItemElement) | |
290 this._highlightMenuItem(menuItemElement, false); | |
291 }, | |
292 | |
293 _highlightNext: function() | |
294 { | |
295 var menuItemElement = this._highlightedMenuItemElement ? this._highlight
edMenuItemElement.nextSibling : this._contextMenuElement.firstChild; | |
296 while (menuItemElement && (menuItemElement._isSeparator || menuItemEleme
nt._isCustom)) | |
297 menuItemElement = menuItemElement.nextSibling; | |
298 if (menuItemElement) | |
299 this._highlightMenuItem(menuItemElement, false); | |
300 }, | |
301 | |
302 _menuKeyDown: function(event) | |
303 { | |
304 switch (event.key) { | |
305 case "ArrowUp": | |
306 this._highlightPrevious(); break; | |
307 case "ArrowDown": | |
308 this._highlightNext(); break; | |
309 case "ArrowLeft": | |
310 if (this._parentMenu) { | |
311 this._highlightMenuItem(null, false); | |
312 this._parentMenu._hideSubMenu(); | |
313 } | |
314 break; | |
315 case "ArrowRight": | |
316 if (!this._highlightedMenuItemElement) | |
317 break; | |
318 if (this._highlightedMenuItemElement._subItems) { | |
319 this._showSubMenu(this._highlightedMenuItemElement); | |
320 this._subMenu._focus(); | |
321 this._subMenu._highlightNext(); | |
322 } | |
323 break; | |
324 case "Escape": | |
325 this._discardMenu(false, event); break; | |
326 case "Enter": | |
327 if (!isEnterKey(event)) | |
328 break; | |
329 // Fall through | |
330 case " ": // Space | |
331 if (this._highlightedMenuItemElement) | |
332 this._triggerAction(this._highlightedMenuItemElement, event); | |
333 if (this._highlightedMenuItemElement._subItems) { | |
334 this._subMenu._focus(); | |
335 this._subMenu._highlightNext(); | |
336 } | |
337 break; | |
338 } | |
339 event.consume(true); | |
340 }, | |
341 | |
342 _glassPaneMouseUp: function(event) | |
343 { | |
344 // Return if this is simple 'click', since dispatched on glass pane, can
't use 'click' event. | |
345 if (new Date().getTime() - this._time < 300) | |
346 return; | |
347 if (event.target === this.element) | |
348 return; | |
349 this._discardMenu(true, event); | |
350 event.consume(); | |
351 }, | |
352 | |
353 /** | |
354 * @param {boolean} closeParentMenus | |
355 * @param {!Event=} event | |
356 */ | |
357 _discardMenu: function(closeParentMenus, event) | |
358 { | |
359 if (this._subMenu && !closeParentMenus) | |
360 return; | |
361 if (this._glassPaneElement) { | |
362 var glassPane = this._glassPaneElement; | |
363 delete this._glassPaneElement; | |
364 // This can re-enter discardMenu due to blur. | |
365 this._document.body.removeChild(glassPane); | |
366 if (this._parentMenu) { | |
367 delete this._parentMenu._subMenu; | |
368 if (closeParentMenus) | |
369 this._parentMenu._discardMenu(closeParentMenus, event); | |
370 else | |
371 this._parentMenu._focus(); | |
372 } | |
373 | |
374 if (event) | |
375 event.consume(true); | |
376 } else if (this._parentMenu && this._contextMenuElement.parentElementOrS
hadowHost()) { | |
377 this._discardSubMenus(); | |
378 if (closeParentMenus) | |
379 this._parentMenu._discardMenu(closeParentMenus, event); | |
380 else | |
381 this._parentMenu._focus(); | |
382 if (event) | |
383 event.consume(true); | |
384 } | |
385 if (this._discardMenuOnResizeListener) { | |
386 this._document.defaultView.removeEventListener("resize", this._disca
rdMenuOnResizeListener, false); | |
387 delete this._discardMenuOnResizeListener; | |
388 } | |
389 }, | |
390 | |
391 _discardSubMenus: function() | |
392 { | |
393 if (this._subMenu) | |
394 this._subMenu._discardSubMenus(); | |
395 if (this.element) | |
396 this.element.remove(); | |
397 if (this._parentMenu) | |
398 delete this._parentMenu._subMenu; | |
399 } | |
400 }; | |
OLD | NEW |