OLD | NEW |
1 /* | 1 /* |
2 * Copyright (C) 2013 Google Inc. All rights reserved. | 2 * Copyright (C) 2013 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 are | 5 * modification, are permitted provided that the following conditions are |
6 * met: | 6 * met: |
7 * | 7 * |
8 * * Redistributions of source code must retain the above copyright | 8 * * Redistributions of source code must retain the above copyright |
9 * notice, this list of conditions and the following disclaimer. | 9 * notice, this list of conditions and the following disclaimer. |
10 * * Redistributions in binary form must reproduce the above | 10 * * Redistributions in binary form must reproduce the above |
11 * copyright notice, this list of conditions and the following disclaimer | 11 * copyright notice, this list of conditions and the following disclaimer |
12 * in the documentation and/or other materials provided with the | 12 * in the documentation and/or other materials provided with the |
13 * distribution. | 13 * distribution. |
14 * * Neither the name of Google Inc. nor the names of its | 14 * * Neither the name of Google Inc. nor the names of its |
15 * contributors may be used to endorse or promote products derived from | 15 * contributors may be used to endorse or promote products derived from |
16 * this software without specific prior written permission. | 16 * this software without specific prior written permission. |
17 * | 17 * |
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
29 */ | 29 */ |
30 | |
31 /** | 30 /** |
32 * @interface | 31 * @interface |
33 */ | 32 */ |
34 WebInspector.SuggestBoxDelegate = function() | 33 WebInspector.SuggestBoxDelegate = function() {}; |
35 { | |
36 }; | |
37 | 34 |
38 WebInspector.SuggestBoxDelegate.prototype = { | 35 WebInspector.SuggestBoxDelegate.prototype = { |
39 /** | 36 /** |
40 * @param {string} suggestion | 37 * @param {string} suggestion |
41 * @param {boolean=} isIntermediateSuggestion | 38 * @param {boolean=} isIntermediateSuggestion |
42 */ | 39 */ |
43 applySuggestion: function(suggestion, isIntermediateSuggestion) { }, | 40 applySuggestion: function(suggestion, isIntermediateSuggestion) {}, |
44 | 41 |
45 /** | 42 /** |
46 * acceptSuggestion will be always called after call to applySuggestion with
isIntermediateSuggestion being equal to false. | 43 * acceptSuggestion will be always called after call to applySuggestion with i
sIntermediateSuggestion being equal to false. |
47 */ | 44 */ |
48 acceptSuggestion: function() { }, | 45 acceptSuggestion: function() {}, |
49 }; | 46 }; |
50 | 47 |
51 /** | 48 /** |
52 * @constructor | |
53 * @implements {WebInspector.StaticViewportControl.Provider} | 49 * @implements {WebInspector.StaticViewportControl.Provider} |
54 * @param {!WebInspector.SuggestBoxDelegate} suggestBoxDelegate | 50 * @unrestricted |
55 * @param {number=} maxItemsHeight | |
56 * @param {boolean=} captureEnter | |
57 */ | 51 */ |
58 WebInspector.SuggestBox = function(suggestBoxDelegate, maxItemsHeight, captureEn
ter) | 52 WebInspector.SuggestBox = class { |
59 { | 53 /** |
| 54 * @param {!WebInspector.SuggestBoxDelegate} suggestBoxDelegate |
| 55 * @param {number=} maxItemsHeight |
| 56 * @param {boolean=} captureEnter |
| 57 */ |
| 58 constructor(suggestBoxDelegate, maxItemsHeight, captureEnter) { |
60 this._suggestBoxDelegate = suggestBoxDelegate; | 59 this._suggestBoxDelegate = suggestBoxDelegate; |
61 this._length = 0; | 60 this._length = 0; |
62 this._selectedIndex = -1; | 61 this._selectedIndex = -1; |
63 this._selectedElement = null; | 62 this._selectedElement = null; |
64 this._maxItemsHeight = maxItemsHeight; | 63 this._maxItemsHeight = maxItemsHeight; |
65 this._maybeHideBound = this._maybeHide.bind(this); | 64 this._maybeHideBound = this._maybeHide.bind(this); |
66 this._container = createElementWithClass("div", "suggest-box-container"); | 65 this._container = createElementWithClass('div', 'suggest-box-container'); |
67 this._viewport = new WebInspector.StaticViewportControl(this); | 66 this._viewport = new WebInspector.StaticViewportControl(this); |
68 this._element = this._viewport.element; | 67 this._element = this._viewport.element; |
69 this._element.classList.add("suggest-box"); | 68 this._element.classList.add('suggest-box'); |
70 this._container.appendChild(this._element); | 69 this._container.appendChild(this._element); |
71 this._element.addEventListener("mousedown", this._onBoxMouseDown.bind(this),
true); | 70 this._element.addEventListener('mousedown', this._onBoxMouseDown.bind(this),
true); |
72 this._detailsPopup = this._container.createChild("div", "suggest-box details
-popup monospace"); | 71 this._detailsPopup = this._container.createChild('div', 'suggest-box details
-popup monospace'); |
73 this._detailsPopup.classList.add("hidden"); | 72 this._detailsPopup.classList.add('hidden'); |
74 this._asyncDetailsCallback = null; | 73 this._asyncDetailsCallback = null; |
75 /** @type {!Map<number, !Promise<{detail: string, description: string}>>} */ | 74 /** @type {!Map<number, !Promise<{detail: string, description: string}>>} */ |
76 this._asyncDetailsPromises = new Map(); | 75 this._asyncDetailsPromises = new Map(); |
77 this._userInteracted = false; | 76 this._userInteracted = false; |
78 this._captureEnter = captureEnter; | 77 this._captureEnter = captureEnter; |
79 /** @type {!Array<!Element>} */ | 78 /** @type {!Array<!Element>} */ |
80 this._elementList = []; | 79 this._elementList = []; |
81 this._rowHeight = 17; | 80 this._rowHeight = 17; |
82 this._viewportWidth = "100vw"; | 81 this._viewportWidth = '100vw'; |
83 this._hasVerticalScroll = false; | 82 this._hasVerticalScroll = false; |
84 this._userEnteredText = ""; | 83 this._userEnteredText = ''; |
85 /** @type {!WebInspector.SuggestBox.Suggestions} */ | 84 /** @type {!WebInspector.SuggestBox.Suggestions} */ |
86 this._items = []; | 85 this._items = []; |
| 86 } |
| 87 |
| 88 /** |
| 89 * @return {boolean} |
| 90 */ |
| 91 visible() { |
| 92 return !!this._container.parentElement; |
| 93 } |
| 94 |
| 95 /** |
| 96 * @param {!AnchorBox} anchorBox |
| 97 */ |
| 98 setPosition(anchorBox) { |
| 99 this._updateBoxPosition(anchorBox); |
| 100 } |
| 101 |
| 102 /** |
| 103 * @param {!AnchorBox} anchorBox |
| 104 */ |
| 105 _updateBoxPosition(anchorBox) { |
| 106 console.assert(this._overlay); |
| 107 if (this._lastAnchorBox && this._lastAnchorBox.equals(anchorBox) && this._la
stItemCount === this.itemCount()) |
| 108 return; |
| 109 this._lastItemCount = this.itemCount(); |
| 110 this._lastAnchorBox = anchorBox; |
| 111 |
| 112 // Position relative to main DevTools element. |
| 113 var container = WebInspector.Dialog.modalHostView().element; |
| 114 anchorBox = anchorBox.relativeToElement(container); |
| 115 var totalHeight = container.offsetHeight; |
| 116 var aboveHeight = anchorBox.y; |
| 117 var underHeight = totalHeight - anchorBox.y - anchorBox.height; |
| 118 |
| 119 this._overlay.setLeftOffset(anchorBox.x); |
| 120 |
| 121 var under = underHeight >= aboveHeight; |
| 122 if (under) |
| 123 this._overlay.setVerticalOffset(anchorBox.y + anchorBox.height, true); |
| 124 else |
| 125 this._overlay.setVerticalOffset(totalHeight - anchorBox.y, false); |
| 126 |
| 127 var spacer = 6; |
| 128 var maxHeight = Math.min( |
| 129 Math.max(underHeight, aboveHeight) - spacer, this._maxItemsHeight ? this
._maxItemsHeight * this._rowHeight : 0); |
| 130 var height = this._rowHeight * this._items.length; |
| 131 this._hasVerticalScroll = height > maxHeight; |
| 132 this._element.style.height = Math.min(maxHeight, height) + 'px'; |
| 133 } |
| 134 |
| 135 _updateWidth() { |
| 136 if (this._hasVerticalScroll) { |
| 137 this._element.style.width = '100vw'; |
| 138 return; |
| 139 } |
| 140 // If there are no scrollbars, set the width to the width of the largest row
. |
| 141 var maxIndex = 0; |
| 142 for (var i = 0; i < this._items.length; i++) { |
| 143 if (this._items[i].title.length > this._items[maxIndex].title.length) |
| 144 maxIndex = i; |
| 145 } |
| 146 var element = /** @type {!Element} */ (this.itemElement(maxIndex)); |
| 147 this._element.style.width = WebInspector.measurePreferredSize(element, this.
_element).width + 'px'; |
| 148 } |
| 149 |
| 150 /** |
| 151 * @param {!Event} event |
| 152 */ |
| 153 _onBoxMouseDown(event) { |
| 154 if (this._hideTimeoutId) { |
| 155 window.clearTimeout(this._hideTimeoutId); |
| 156 delete this._hideTimeoutId; |
| 157 } |
| 158 event.preventDefault(); |
| 159 } |
| 160 |
| 161 _maybeHide() { |
| 162 if (!this._hideTimeoutId) |
| 163 this._hideTimeoutId = window.setTimeout(this.hide.bind(this), 0); |
| 164 } |
| 165 |
| 166 /** |
| 167 * // FIXME: make SuggestBox work for multiple documents. |
| 168 * @suppressGlobalPropertiesCheck |
| 169 */ |
| 170 _show() { |
| 171 if (this.visible()) |
| 172 return; |
| 173 this._bodyElement = document.body; |
| 174 this._bodyElement.addEventListener('mousedown', this._maybeHideBound, true); |
| 175 this._overlay = new WebInspector.SuggestBox.Overlay(); |
| 176 this._overlay.setContentElement(this._container); |
| 177 var measuringElement = this._createItemElement('1', '12'); |
| 178 this._viewport.element.appendChild(measuringElement); |
| 179 this._rowHeight = measuringElement.getBoundingClientRect().height; |
| 180 measuringElement.remove(); |
| 181 } |
| 182 |
| 183 hide() { |
| 184 if (!this.visible()) |
| 185 return; |
| 186 |
| 187 this._userInteracted = false; |
| 188 this._bodyElement.removeEventListener('mousedown', this._maybeHideBound, tru
e); |
| 189 delete this._bodyElement; |
| 190 this._container.remove(); |
| 191 this._overlay.dispose(); |
| 192 delete this._overlay; |
| 193 delete this._selectedElement; |
| 194 this._selectedIndex = -1; |
| 195 delete this._lastAnchorBox; |
| 196 } |
| 197 |
| 198 removeFromElement() { |
| 199 this.hide(); |
| 200 } |
| 201 |
| 202 /** |
| 203 * @param {boolean=} isIntermediateSuggestion |
| 204 * @return {boolean} |
| 205 */ |
| 206 _applySuggestion(isIntermediateSuggestion) { |
| 207 if (this._onlyCompletion) { |
| 208 this._suggestBoxDelegate.applySuggestion(this._onlyCompletion, isIntermedi
ateSuggestion); |
| 209 return true; |
| 210 } |
| 211 |
| 212 if (!this.visible() || !this._selectedElement) |
| 213 return false; |
| 214 |
| 215 var suggestion = this._selectedElement.__fullValue; |
| 216 if (!suggestion) |
| 217 return false; |
| 218 |
| 219 this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestio
n); |
| 220 return true; |
| 221 } |
| 222 |
| 223 /** |
| 224 * @return {boolean} |
| 225 */ |
| 226 acceptSuggestion() { |
| 227 var result = this._applySuggestion(); |
| 228 this.hide(); |
| 229 if (!result) |
| 230 return false; |
| 231 |
| 232 this._suggestBoxDelegate.acceptSuggestion(); |
| 233 |
| 234 return true; |
| 235 } |
| 236 |
| 237 /** |
| 238 * @param {number} shift |
| 239 * @param {boolean=} isCircular |
| 240 * @return {boolean} is changed |
| 241 */ |
| 242 _selectClosest(shift, isCircular) { |
| 243 if (!this._length) |
| 244 return false; |
| 245 |
| 246 this._userInteracted = true; |
| 247 |
| 248 if (this._selectedIndex === -1 && shift < 0) |
| 249 shift += 1; |
| 250 |
| 251 var index = this._selectedIndex + shift; |
| 252 |
| 253 if (isCircular) |
| 254 index = (this._length + index) % this._length; |
| 255 else |
| 256 index = Number.constrain(index, 0, this._length - 1); |
| 257 |
| 258 this._selectItem(index, true); |
| 259 this._applySuggestion(true); |
| 260 return true; |
| 261 } |
| 262 |
| 263 /** |
| 264 * @param {!Event} event |
| 265 */ |
| 266 _onItemMouseDown(event) { |
| 267 this._selectedElement = event.currentTarget; |
| 268 this.acceptSuggestion(); |
| 269 event.consume(true); |
| 270 } |
| 271 |
| 272 /** |
| 273 * @param {string} prefix |
| 274 * @param {string} text |
| 275 * @param {string=} className |
| 276 * @return {!Element} |
| 277 */ |
| 278 _createItemElement(prefix, text, className) { |
| 279 var element = createElementWithClass('div', 'suggest-box-content-item source
-code ' + (className || '')); |
| 280 element.tabIndex = -1; |
| 281 if (prefix && prefix.length && !text.indexOf(prefix)) { |
| 282 element.createChild('span', 'prefix').textContent = prefix; |
| 283 element.createChild('span', 'suffix').textContent = text.substring(prefix.
length).trimEnd(50); |
| 284 } else { |
| 285 element.createChild('span', 'suffix').textContent = text.trimEnd(50); |
| 286 } |
| 287 element.__fullValue = text; |
| 288 element.createChild('span', 'spacer'); |
| 289 element.addEventListener('mousedown', this._onItemMouseDown.bind(this), fals
e); |
| 290 return element; |
| 291 } |
| 292 |
| 293 /** |
| 294 * @param {!WebInspector.SuggestBox.Suggestions} items |
| 295 * @param {string} userEnteredText |
| 296 * @param {function(number): !Promise<{detail:string, description:string}>=} a
syncDetails |
| 297 */ |
| 298 _updateItems(items, userEnteredText, asyncDetails) { |
| 299 this._length = items.length; |
| 300 this._asyncDetailsPromises.clear(); |
| 301 this._asyncDetailsCallback = asyncDetails; |
| 302 this._elementList = []; |
| 303 delete this._selectedElement; |
| 304 |
| 305 this._userEnteredText = userEnteredText; |
| 306 this._items = items; |
| 307 } |
| 308 |
| 309 /** |
| 310 * @param {number} index |
| 311 * @return {!Promise<?{detail: string, description: string}>} |
| 312 */ |
| 313 _asyncDetails(index) { |
| 314 if (!this._asyncDetailsCallback) |
| 315 return Promise.resolve(/** @type {?{description: string, detail: string}}
*/ (null)); |
| 316 if (!this._asyncDetailsPromises.has(index)) |
| 317 this._asyncDetailsPromises.set(index, this._asyncDetailsCallback(index)); |
| 318 return /** @type {!Promise<?{detail: string, description: string}>} */ (this
._asyncDetailsPromises.get(index)); |
| 319 } |
| 320 |
| 321 /** |
| 322 * @param {?{detail: string, description: string}} details |
| 323 */ |
| 324 _showDetailsPopup(details) { |
| 325 this._detailsPopup.removeChildren(); |
| 326 if (!details) |
| 327 return; |
| 328 this._detailsPopup.createChild('section', 'detail').createTextChild(details.
detail); |
| 329 this._detailsPopup.createChild('section', 'description').createTextChild(det
ails.description); |
| 330 this._detailsPopup.classList.remove('hidden'); |
| 331 } |
| 332 |
| 333 /** |
| 334 * @param {number} index |
| 335 * @param {boolean} scrollIntoView |
| 336 */ |
| 337 _selectItem(index, scrollIntoView) { |
| 338 if (this._selectedElement) |
| 339 this._selectedElement.classList.remove('selected'); |
| 340 |
| 341 this._selectedIndex = index; |
| 342 if (index < 0) |
| 343 return; |
| 344 |
| 345 this._selectedElement = this.itemElement(index); |
| 346 this._selectedElement.classList.add('selected'); |
| 347 this._detailsPopup.classList.add('hidden'); |
| 348 var elem = this._selectedElement; |
| 349 this._asyncDetails(index).then(showDetails.bind(this), function() {}); |
| 350 |
| 351 if (scrollIntoView) |
| 352 this._viewport.scrollItemIntoView(index); |
| 353 |
| 354 /** |
| 355 * @param {?{detail: string, description: string}} details |
| 356 * @this {WebInspector.SuggestBox} |
| 357 */ |
| 358 function showDetails(details) { |
| 359 if (elem === this._selectedElement) |
| 360 this._showDetailsPopup(details); |
| 361 } |
| 362 } |
| 363 |
| 364 /** |
| 365 * @param {!WebInspector.SuggestBox.Suggestions} completions |
| 366 * @param {boolean} canShowForSingleItem |
| 367 * @param {string} userEnteredText |
| 368 * @return {boolean} |
| 369 */ |
| 370 _canShowBox(completions, canShowForSingleItem, userEnteredText) { |
| 371 if (!completions || !completions.length) |
| 372 return false; |
| 373 |
| 374 if (completions.length > 1) |
| 375 return true; |
| 376 |
| 377 // Do not show a single suggestion if it is the same as user-entered prefix,
even if allowed to show single-item suggest boxes. |
| 378 return canShowForSingleItem && completions[0].title !== userEnteredText; |
| 379 } |
| 380 |
| 381 _ensureRowCountPerViewport() { |
| 382 if (this._rowCountPerViewport) |
| 383 return; |
| 384 if (!this._items.length) |
| 385 return; |
| 386 |
| 387 this._rowCountPerViewport = Math.floor(this._element.getBoundingClientRect()
.height / this._rowHeight); |
| 388 } |
| 389 |
| 390 /** |
| 391 * @param {!AnchorBox} anchorBox |
| 392 * @param {!WebInspector.SuggestBox.Suggestions} completions |
| 393 * @param {number} selectedIndex |
| 394 * @param {boolean} canShowForSingleItem |
| 395 * @param {string} userEnteredText |
| 396 * @param {function(number): !Promise<{detail:string, description:string}>=} a
syncDetails |
| 397 */ |
| 398 updateSuggestions(anchorBox, completions, selectedIndex, canShowForSingleItem,
userEnteredText, asyncDetails) { |
| 399 delete this._onlyCompletion; |
| 400 if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) { |
| 401 this._updateItems(completions, userEnteredText, asyncDetails); |
| 402 this._show(); |
| 403 this._updateBoxPosition(anchorBox); |
| 404 this._updateWidth(); |
| 405 this._viewport.refresh(); |
| 406 this._selectItem(selectedIndex, selectedIndex > 0); |
| 407 delete this._rowCountPerViewport; |
| 408 } else { |
| 409 if (completions.length === 1) |
| 410 this._onlyCompletion = completions[0].title; |
| 411 this.hide(); |
| 412 } |
| 413 } |
| 414 |
| 415 /** |
| 416 * @param {!KeyboardEvent} event |
| 417 * @return {boolean} |
| 418 */ |
| 419 keyPressed(event) { |
| 420 switch (event.key) { |
| 421 case 'ArrowUp': |
| 422 return this.upKeyPressed(); |
| 423 case 'ArrowDown': |
| 424 return this.downKeyPressed(); |
| 425 case 'PageUp': |
| 426 return this.pageUpKeyPressed(); |
| 427 case 'PageDown': |
| 428 return this.pageDownKeyPressed(); |
| 429 case 'Enter': |
| 430 return this.enterKeyPressed(); |
| 431 } |
| 432 return false; |
| 433 } |
| 434 |
| 435 /** |
| 436 * @return {boolean} |
| 437 */ |
| 438 upKeyPressed() { |
| 439 return this._selectClosest(-1, true); |
| 440 } |
| 441 |
| 442 /** |
| 443 * @return {boolean} |
| 444 */ |
| 445 downKeyPressed() { |
| 446 return this._selectClosest(1, true); |
| 447 } |
| 448 |
| 449 /** |
| 450 * @return {boolean} |
| 451 */ |
| 452 pageUpKeyPressed() { |
| 453 this._ensureRowCountPerViewport(); |
| 454 return this._selectClosest(-this._rowCountPerViewport, false); |
| 455 } |
| 456 |
| 457 /** |
| 458 * @return {boolean} |
| 459 */ |
| 460 pageDownKeyPressed() { |
| 461 this._ensureRowCountPerViewport(); |
| 462 return this._selectClosest(this._rowCountPerViewport, false); |
| 463 } |
| 464 |
| 465 /** |
| 466 * @return {boolean} |
| 467 */ |
| 468 enterKeyPressed() { |
| 469 if (!this._userInteracted && this._captureEnter) |
| 470 return false; |
| 471 |
| 472 var hasSelectedItem = !!this._selectedElement || this._onlyCompletion; |
| 473 this.acceptSuggestion(); |
| 474 |
| 475 // Report the event as non-handled if there is no selected item, |
| 476 // to commit the input or handle it otherwise. |
| 477 return hasSelectedItem; |
| 478 } |
| 479 |
| 480 /** |
| 481 * @override |
| 482 * @param {number} index |
| 483 * @return {number} |
| 484 */ |
| 485 fastItemHeight(index) { |
| 486 return this._rowHeight; |
| 487 } |
| 488 |
| 489 /** |
| 490 * @override |
| 491 * @return {number} |
| 492 */ |
| 493 itemCount() { |
| 494 return this._items.length; |
| 495 } |
| 496 |
| 497 /** |
| 498 * @override |
| 499 * @param {number} index |
| 500 * @return {?Element} |
| 501 */ |
| 502 itemElement(index) { |
| 503 if (!this._elementList[index]) |
| 504 this._elementList[index] = |
| 505 this._createItemElement(this._userEnteredText, this._items[index].titl
e, this._items[index].className); |
| 506 return this._elementList[index]; |
| 507 } |
87 }; | 508 }; |
88 | 509 |
89 /** | 510 /** |
90 * @typedef {!Array.<{title: string, className: (string|undefined)}>} | 511 * @typedef {!Array.<{title: string, className: (string|undefined)}>} |
91 */ | 512 */ |
92 WebInspector.SuggestBox.Suggestions; | 513 WebInspector.SuggestBox.Suggestions; |
93 | 514 |
94 WebInspector.SuggestBox.prototype = { | |
95 /** | |
96 * @return {boolean} | |
97 */ | |
98 visible: function() | |
99 { | |
100 return !!this._container.parentElement; | |
101 }, | |
102 | |
103 /** | |
104 * @param {!AnchorBox} anchorBox | |
105 */ | |
106 setPosition: function(anchorBox) | |
107 { | |
108 this._updateBoxPosition(anchorBox); | |
109 }, | |
110 | |
111 /** | |
112 * @param {!AnchorBox} anchorBox | |
113 */ | |
114 _updateBoxPosition: function(anchorBox) | |
115 { | |
116 console.assert(this._overlay); | |
117 if (this._lastAnchorBox && this._lastAnchorBox.equals(anchorBox) && this
._lastItemCount === this.itemCount()) | |
118 return; | |
119 this._lastItemCount = this.itemCount(); | |
120 this._lastAnchorBox = anchorBox; | |
121 | |
122 // Position relative to main DevTools element. | |
123 var container = WebInspector.Dialog.modalHostView().element; | |
124 anchorBox = anchorBox.relativeToElement(container); | |
125 var totalHeight = container.offsetHeight; | |
126 var aboveHeight = anchorBox.y; | |
127 var underHeight = totalHeight - anchorBox.y - anchorBox.height; | |
128 | |
129 this._overlay.setLeftOffset(anchorBox.x); | |
130 | |
131 var under = underHeight >= aboveHeight; | |
132 if (under) | |
133 this._overlay.setVerticalOffset(anchorBox.y + anchorBox.height, true
); | |
134 else | |
135 this._overlay.setVerticalOffset(totalHeight - anchorBox.y, false); | |
136 | |
137 var spacer = 6; | |
138 var maxHeight = Math.min(Math.max(underHeight, aboveHeight) - spacer, th
is._maxItemsHeight ? this._maxItemsHeight * this._rowHeight : 0); | |
139 var height = this._rowHeight * this._items.length; | |
140 this._hasVerticalScroll = height > maxHeight; | |
141 this._element.style.height = Math.min(maxHeight, height) + "px"; | |
142 }, | |
143 | |
144 _updateWidth: function() | |
145 { | |
146 if (this._hasVerticalScroll) { | |
147 this._element.style.width = "100vw"; | |
148 return; | |
149 } | |
150 // If there are no scrollbars, set the width to the width of the largest
row. | |
151 var maxIndex = 0; | |
152 for (var i = 0; i < this._items.length; i++) { | |
153 if (this._items[i].title.length > this._items[maxIndex].title.length
) | |
154 maxIndex = i; | |
155 } | |
156 var element = /** @type {!Element} */ (this.itemElement(maxIndex)); | |
157 this._element.style.width = WebInspector.measurePreferredSize(element, t
his._element).width + "px"; | |
158 }, | |
159 | |
160 /** | |
161 * @param {!Event} event | |
162 */ | |
163 _onBoxMouseDown: function(event) | |
164 { | |
165 if (this._hideTimeoutId) { | |
166 window.clearTimeout(this._hideTimeoutId); | |
167 delete this._hideTimeoutId; | |
168 } | |
169 event.preventDefault(); | |
170 }, | |
171 | |
172 _maybeHide: function() | |
173 { | |
174 if (!this._hideTimeoutId) | |
175 this._hideTimeoutId = window.setTimeout(this.hide.bind(this), 0); | |
176 }, | |
177 | |
178 /** | |
179 * // FIXME: make SuggestBox work for multiple documents. | |
180 * @suppressGlobalPropertiesCheck | |
181 */ | |
182 _show: function() | |
183 { | |
184 if (this.visible()) | |
185 return; | |
186 this._bodyElement = document.body; | |
187 this._bodyElement.addEventListener("mousedown", this._maybeHideBound, tr
ue); | |
188 this._overlay = new WebInspector.SuggestBox.Overlay(); | |
189 this._overlay.setContentElement(this._container); | |
190 var measuringElement = this._createItemElement("1", "12"); | |
191 this._viewport.element.appendChild(measuringElement); | |
192 this._rowHeight = measuringElement.getBoundingClientRect().height; | |
193 measuringElement.remove(); | |
194 }, | |
195 | |
196 hide: function() | |
197 { | |
198 if (!this.visible()) | |
199 return; | |
200 | |
201 this._userInteracted = false; | |
202 this._bodyElement.removeEventListener("mousedown", this._maybeHideBound,
true); | |
203 delete this._bodyElement; | |
204 this._container.remove(); | |
205 this._overlay.dispose(); | |
206 delete this._overlay; | |
207 delete this._selectedElement; | |
208 this._selectedIndex = -1; | |
209 delete this._lastAnchorBox; | |
210 }, | |
211 | |
212 removeFromElement: function() | |
213 { | |
214 this.hide(); | |
215 }, | |
216 | |
217 /** | |
218 * @param {boolean=} isIntermediateSuggestion | |
219 * @return {boolean} | |
220 */ | |
221 _applySuggestion: function(isIntermediateSuggestion) | |
222 { | |
223 if (this._onlyCompletion) { | |
224 this._suggestBoxDelegate.applySuggestion(this._onlyCompletion, isInt
ermediateSuggestion); | |
225 return true; | |
226 } | |
227 | |
228 if (!this.visible() || !this._selectedElement) | |
229 return false; | |
230 | |
231 var suggestion = this._selectedElement.__fullValue; | |
232 if (!suggestion) | |
233 return false; | |
234 | |
235 this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSugge
stion); | |
236 return true; | |
237 }, | |
238 | |
239 /** | |
240 * @return {boolean} | |
241 */ | |
242 acceptSuggestion: function() | |
243 { | |
244 var result = this._applySuggestion(); | |
245 this.hide(); | |
246 if (!result) | |
247 return false; | |
248 | |
249 this._suggestBoxDelegate.acceptSuggestion(); | |
250 | |
251 return true; | |
252 }, | |
253 | |
254 /** | |
255 * @param {number} shift | |
256 * @param {boolean=} isCircular | |
257 * @return {boolean} is changed | |
258 */ | |
259 _selectClosest: function(shift, isCircular) | |
260 { | |
261 if (!this._length) | |
262 return false; | |
263 | |
264 this._userInteracted = true; | |
265 | |
266 if (this._selectedIndex === -1 && shift < 0) | |
267 shift += 1; | |
268 | |
269 var index = this._selectedIndex + shift; | |
270 | |
271 if (isCircular) | |
272 index = (this._length + index) % this._length; | |
273 else | |
274 index = Number.constrain(index, 0, this._length - 1); | |
275 | |
276 this._selectItem(index, true); | |
277 this._applySuggestion(true); | |
278 return true; | |
279 }, | |
280 | |
281 /** | |
282 * @param {!Event} event | |
283 */ | |
284 _onItemMouseDown: function(event) | |
285 { | |
286 this._selectedElement = event.currentTarget; | |
287 this.acceptSuggestion(); | |
288 event.consume(true); | |
289 }, | |
290 | |
291 /** | |
292 * @param {string} prefix | |
293 * @param {string} text | |
294 * @param {string=} className | |
295 * @return {!Element} | |
296 */ | |
297 _createItemElement: function(prefix, text, className) | |
298 { | |
299 var element = createElementWithClass("div", "suggest-box-content-item so
urce-code " + (className || "")); | |
300 element.tabIndex = -1; | |
301 if (prefix && prefix.length && !text.indexOf(prefix)) { | |
302 element.createChild("span", "prefix").textContent = prefix; | |
303 element.createChild("span", "suffix").textContent = text.substring(p
refix.length).trimEnd(50); | |
304 } else { | |
305 element.createChild("span", "suffix").textContent = text.trimEnd(50)
; | |
306 } | |
307 element.__fullValue = text; | |
308 element.createChild("span", "spacer"); | |
309 element.addEventListener("mousedown", this._onItemMouseDown.bind(this),
false); | |
310 return element; | |
311 }, | |
312 | |
313 /** | |
314 * @param {!WebInspector.SuggestBox.Suggestions} items | |
315 * @param {string} userEnteredText | |
316 * @param {function(number): !Promise<{detail:string, description:string}>=}
asyncDetails | |
317 */ | |
318 _updateItems: function(items, userEnteredText, asyncDetails) | |
319 { | |
320 this._length = items.length; | |
321 this._asyncDetailsPromises.clear(); | |
322 this._asyncDetailsCallback = asyncDetails; | |
323 this._elementList = []; | |
324 delete this._selectedElement; | |
325 | |
326 this._userEnteredText = userEnteredText; | |
327 this._items = items; | |
328 }, | |
329 | |
330 /** | |
331 * @param {number} index | |
332 * @return {!Promise<?{detail: string, description: string}>} | |
333 */ | |
334 _asyncDetails: function(index) | |
335 { | |
336 if (!this._asyncDetailsCallback) | |
337 return Promise.resolve(/** @type {?{description: string, detail: str
ing}} */(null)); | |
338 if (!this._asyncDetailsPromises.has(index)) | |
339 this._asyncDetailsPromises.set(index, this._asyncDetailsCallback(ind
ex)); | |
340 return /** @type {!Promise<?{detail: string, description: string}>} */(t
his._asyncDetailsPromises.get(index)); | |
341 }, | |
342 | |
343 /** | |
344 * @param {?{detail: string, description: string}} details | |
345 */ | |
346 _showDetailsPopup: function(details) | |
347 { | |
348 this._detailsPopup.removeChildren(); | |
349 if (!details) | |
350 return; | |
351 this._detailsPopup.createChild("section", "detail").createTextChild(deta
ils.detail); | |
352 this._detailsPopup.createChild("section", "description").createTextChild
(details.description); | |
353 this._detailsPopup.classList.remove("hidden"); | |
354 }, | |
355 | |
356 /** | |
357 * @param {number} index | |
358 * @param {boolean} scrollIntoView | |
359 */ | |
360 _selectItem: function(index, scrollIntoView) | |
361 { | |
362 if (this._selectedElement) | |
363 this._selectedElement.classList.remove("selected"); | |
364 | |
365 this._selectedIndex = index; | |
366 if (index < 0) | |
367 return; | |
368 | |
369 this._selectedElement = this.itemElement(index); | |
370 this._selectedElement.classList.add("selected"); | |
371 this._detailsPopup.classList.add("hidden"); | |
372 var elem = this._selectedElement; | |
373 this._asyncDetails(index).then(showDetails.bind(this), function(){}); | |
374 | |
375 if (scrollIntoView) | |
376 this._viewport.scrollItemIntoView(index); | |
377 | |
378 /** | |
379 * @param {?{detail: string, description: string}} details | |
380 * @this {WebInspector.SuggestBox} | |
381 */ | |
382 function showDetails(details) | |
383 { | |
384 if (elem === this._selectedElement) | |
385 this._showDetailsPopup(details); | |
386 } | |
387 }, | |
388 | |
389 /** | |
390 * @param {!WebInspector.SuggestBox.Suggestions} completions | |
391 * @param {boolean} canShowForSingleItem | |
392 * @param {string} userEnteredText | |
393 * @return {boolean} | |
394 */ | |
395 _canShowBox: function(completions, canShowForSingleItem, userEnteredText) | |
396 { | |
397 if (!completions || !completions.length) | |
398 return false; | |
399 | |
400 if (completions.length > 1) | |
401 return true; | |
402 | |
403 // Do not show a single suggestion if it is the same as user-entered pre
fix, even if allowed to show single-item suggest boxes. | |
404 return canShowForSingleItem && completions[0].title !== userEnteredText; | |
405 }, | |
406 | |
407 _ensureRowCountPerViewport: function() | |
408 { | |
409 if (this._rowCountPerViewport) | |
410 return; | |
411 if (!this._items.length) | |
412 return; | |
413 | |
414 this._rowCountPerViewport = Math.floor(this._element.getBoundingClientRe
ct().height / this._rowHeight); | |
415 }, | |
416 | |
417 /** | |
418 * @param {!AnchorBox} anchorBox | |
419 * @param {!WebInspector.SuggestBox.Suggestions} completions | |
420 * @param {number} selectedIndex | |
421 * @param {boolean} canShowForSingleItem | |
422 * @param {string} userEnteredText | |
423 * @param {function(number): !Promise<{detail:string, description:string}>=}
asyncDetails | |
424 */ | |
425 updateSuggestions: function(anchorBox, completions, selectedIndex, canShowFo
rSingleItem, userEnteredText, asyncDetails) | |
426 { | |
427 delete this._onlyCompletion; | |
428 if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)
) { | |
429 this._updateItems(completions, userEnteredText, asyncDetails); | |
430 this._show(); | |
431 this._updateBoxPosition(anchorBox); | |
432 this._updateWidth(); | |
433 this._viewport.refresh(); | |
434 this._selectItem(selectedIndex, selectedIndex > 0); | |
435 delete this._rowCountPerViewport; | |
436 } else { | |
437 if (completions.length === 1) | |
438 this._onlyCompletion = completions[0].title; | |
439 this.hide(); | |
440 } | |
441 }, | |
442 | |
443 /** | |
444 * @param {!KeyboardEvent} event | |
445 * @return {boolean} | |
446 */ | |
447 keyPressed: function(event) | |
448 { | |
449 switch (event.key) { | |
450 case "ArrowUp": | |
451 return this.upKeyPressed(); | |
452 case "ArrowDown": | |
453 return this.downKeyPressed(); | |
454 case "PageUp": | |
455 return this.pageUpKeyPressed(); | |
456 case "PageDown": | |
457 return this.pageDownKeyPressed(); | |
458 case "Enter": | |
459 return this.enterKeyPressed(); | |
460 } | |
461 return false; | |
462 }, | |
463 | |
464 /** | |
465 * @return {boolean} | |
466 */ | |
467 upKeyPressed: function() | |
468 { | |
469 return this._selectClosest(-1, true); | |
470 }, | |
471 | |
472 /** | |
473 * @return {boolean} | |
474 */ | |
475 downKeyPressed: function() | |
476 { | |
477 return this._selectClosest(1, true); | |
478 }, | |
479 | |
480 /** | |
481 * @return {boolean} | |
482 */ | |
483 pageUpKeyPressed: function() | |
484 { | |
485 this._ensureRowCountPerViewport(); | |
486 return this._selectClosest(-this._rowCountPerViewport, false); | |
487 }, | |
488 | |
489 /** | |
490 * @return {boolean} | |
491 */ | |
492 pageDownKeyPressed: function() | |
493 { | |
494 this._ensureRowCountPerViewport(); | |
495 return this._selectClosest(this._rowCountPerViewport, false); | |
496 }, | |
497 | |
498 /** | |
499 * @return {boolean} | |
500 */ | |
501 enterKeyPressed: function() | |
502 { | |
503 if (!this._userInteracted && this._captureEnter) | |
504 return false; | |
505 | |
506 var hasSelectedItem = !!this._selectedElement || this._onlyCompletion; | |
507 this.acceptSuggestion(); | |
508 | |
509 // Report the event as non-handled if there is no selected item, | |
510 // to commit the input or handle it otherwise. | |
511 return hasSelectedItem; | |
512 }, | |
513 | |
514 /** | |
515 * @override | |
516 * @param {number} index | |
517 * @return {number} | |
518 */ | |
519 fastItemHeight: function(index) | |
520 { | |
521 return this._rowHeight; | |
522 }, | |
523 | |
524 /** | |
525 * @override | |
526 * @return {number} | |
527 */ | |
528 itemCount: function() | |
529 { | |
530 return this._items.length; | |
531 }, | |
532 | |
533 /** | |
534 * @override | |
535 * @param {number} index | |
536 * @return {?Element} | |
537 */ | |
538 itemElement: function(index) | |
539 { | |
540 if (!this._elementList[index]) | |
541 this._elementList[index] = this._createItemElement(this._userEntered
Text, this._items[index].title, this._items[index].className); | |
542 return this._elementList[index]; | |
543 } | |
544 }; | |
545 | |
546 /** | 515 /** |
547 * @constructor | 516 * @unrestricted |
548 * // FIXME: make SuggestBox work for multiple documents. | |
549 * @suppressGlobalPropertiesCheck | |
550 */ | 517 */ |
551 WebInspector.SuggestBox.Overlay = function() | 518 WebInspector.SuggestBox.Overlay = class { |
552 { | 519 /** |
553 this.element = createElementWithClass("div", "suggest-box-overlay"); | 520 * // FIXME: make SuggestBox work for multiple documents. |
554 var root = WebInspector.createShadowRootWithCoreStyles(this.element, "ui/sug
gestBox.css"); | 521 * @suppressGlobalPropertiesCheck |
555 this._leftSpacerElement = root.createChild("div", "suggest-box-left-spacer")
; | 522 */ |
556 this._horizontalElement = root.createChild("div", "suggest-box-horizontal"); | 523 constructor() { |
557 this._topSpacerElement = this._horizontalElement.createChild("div", "suggest
-box-top-spacer"); | 524 this.element = createElementWithClass('div', 'suggest-box-overlay'); |
558 this._bottomSpacerElement = this._horizontalElement.createChild("div", "sugg
est-box-bottom-spacer"); | 525 var root = WebInspector.createShadowRootWithCoreStyles(this.element, 'ui/sug
gestBox.css'); |
| 526 this._leftSpacerElement = root.createChild('div', 'suggest-box-left-spacer')
; |
| 527 this._horizontalElement = root.createChild('div', 'suggest-box-horizontal'); |
| 528 this._topSpacerElement = this._horizontalElement.createChild('div', 'suggest
-box-top-spacer'); |
| 529 this._bottomSpacerElement = this._horizontalElement.createChild('div', 'sugg
est-box-bottom-spacer'); |
559 this._resize(); | 530 this._resize(); |
560 document.body.appendChild(this.element); | 531 document.body.appendChild(this.element); |
| 532 } |
| 533 |
| 534 /** |
| 535 * @param {number} offset |
| 536 */ |
| 537 setLeftOffset(offset) { |
| 538 this._leftSpacerElement.style.flexBasis = offset + 'px'; |
| 539 } |
| 540 |
| 541 /** |
| 542 * @param {number} offset |
| 543 * @param {boolean} isTopOffset |
| 544 */ |
| 545 setVerticalOffset(offset, isTopOffset) { |
| 546 this.element.classList.toggle('under-anchor', isTopOffset); |
| 547 |
| 548 if (isTopOffset) { |
| 549 this._bottomSpacerElement.style.flexBasis = 'auto'; |
| 550 this._topSpacerElement.style.flexBasis = offset + 'px'; |
| 551 } else { |
| 552 this._bottomSpacerElement.style.flexBasis = offset + 'px'; |
| 553 this._topSpacerElement.style.flexBasis = 'auto'; |
| 554 } |
| 555 } |
| 556 |
| 557 /** |
| 558 * @param {!Element} element |
| 559 */ |
| 560 setContentElement(element) { |
| 561 this._horizontalElement.insertBefore(element, this._bottomSpacerElement); |
| 562 } |
| 563 |
| 564 _resize() { |
| 565 var container = WebInspector.Dialog.modalHostView().element; |
| 566 var containerBox = container.boxInWindow(container.ownerDocument.defaultView
); |
| 567 |
| 568 this.element.style.left = containerBox.x + 'px'; |
| 569 this.element.style.top = containerBox.y + 'px'; |
| 570 this.element.style.height = containerBox.height + 'px'; |
| 571 this.element.style.width = containerBox.width + 'px'; |
| 572 } |
| 573 |
| 574 dispose() { |
| 575 this.element.remove(); |
| 576 } |
561 }; | 577 }; |
562 | |
563 WebInspector.SuggestBox.Overlay.prototype = { | |
564 /** | |
565 * @param {number} offset | |
566 */ | |
567 setLeftOffset: function(offset) | |
568 { | |
569 this._leftSpacerElement.style.flexBasis = offset + "px"; | |
570 }, | |
571 | |
572 /** | |
573 * @param {number} offset | |
574 * @param {boolean} isTopOffset | |
575 */ | |
576 setVerticalOffset: function(offset, isTopOffset) | |
577 { | |
578 this.element.classList.toggle("under-anchor", isTopOffset); | |
579 | |
580 if (isTopOffset) { | |
581 this._bottomSpacerElement.style.flexBasis = "auto"; | |
582 this._topSpacerElement.style.flexBasis = offset + "px"; | |
583 } else { | |
584 this._bottomSpacerElement.style.flexBasis = offset + "px"; | |
585 this._topSpacerElement.style.flexBasis = "auto"; | |
586 } | |
587 }, | |
588 | |
589 /** | |
590 * @param {!Element} element | |
591 */ | |
592 setContentElement: function(element) | |
593 { | |
594 this._horizontalElement.insertBefore(element, this._bottomSpacerElement)
; | |
595 }, | |
596 | |
597 _resize: function() | |
598 { | |
599 var container = WebInspector.Dialog.modalHostView().element; | |
600 var containerBox = container.boxInWindow(container.ownerDocument.default
View); | |
601 | |
602 this.element.style.left = containerBox.x + "px"; | |
603 this.element.style.top = containerBox.y + "px"; | |
604 this.element.style.height = containerBox.height + "px"; | |
605 this.element.style.width = containerBox.width + "px"; | |
606 }, | |
607 | |
608 dispose: function() | |
609 { | |
610 this.element.remove(); | |
611 } | |
612 }; | |
OLD | NEW |