Chromium Code Reviews| Index: third_party/WebKit/Source/devtools/front_end/ui/SuggestBox.js |
| diff --git a/third_party/WebKit/Source/devtools/front_end/ui/SuggestBox.js b/third_party/WebKit/Source/devtools/front_end/ui/SuggestBox.js |
| index 1b20de2e0162c3f1caf51a5295798dbd816e4dad..44b1d9f35f67347183532707da563953151c212a 100644 |
| --- a/third_party/WebKit/Source/devtools/front_end/ui/SuggestBox.js |
| +++ b/third_party/WebKit/Source/devtools/front_end/ui/SuggestBox.js |
| @@ -46,8 +46,8 @@ UI.SuggestBoxDelegate.prototype = { |
| }; |
| /** |
| - * @implements {UI.ViewportControl.Provider} |
| * @unrestricted |
| + * @implements {UI.ListDelegate} |
| */ |
| UI.SuggestBox = class { |
| /** |
| @@ -57,32 +57,26 @@ UI.SuggestBox = class { |
| */ |
| constructor(suggestBoxDelegate, maxItemsHeight, captureEnter) { |
| this._suggestBoxDelegate = suggestBoxDelegate; |
| - this._length = 0; |
| - this._selectedIndex = -1; |
| - this._selectedElement = null; |
| this._maxItemsHeight = maxItemsHeight; |
| this._maybeHideBound = this._maybeHide.bind(this); |
| this._container = createElementWithClass('div', 'suggest-box-container'); |
| - this._viewport = new UI.ViewportControl(this); |
| - this._element = this._viewport.element; |
| + this._rowHeight = 17; |
| + /** @type {!UI.ListControl<!UI.SuggestBox.Suggestion>} */ |
| + this._list = new UI.ListControl(this, false); |
| + this._element = this._list.element; |
| this._element.classList.add('suggest-box'); |
| this._container.appendChild(this._element); |
| this._element.addEventListener('mousedown', this._onBoxMouseDown.bind(this), true); |
| this._detailsPopup = this._container.createChild('div', 'suggest-box details-popup monospace'); |
| this._detailsPopup.classList.add('hidden'); |
| this._asyncDetailsCallback = null; |
| - /** @type {!Map<number, !Promise<{detail: string, description: string}>>} */ |
| + /** @type {!Map<!UI.SuggestBox.Suggestion, !Promise<{detail: string, description: string}>>} */ |
| this._asyncDetailsPromises = new Map(); |
| this._userInteracted = false; |
| this._captureEnter = captureEnter; |
| - /** @type {!Array<!Element>} */ |
| - this._elementList = []; |
| - this._rowHeight = 17; |
| this._viewportWidth = '100vw'; |
| this._hasVerticalScroll = false; |
| this._userEnteredText = ''; |
| - /** @type {!UI.SuggestBox.Suggestions} */ |
| - this._items = []; |
| } |
| /** |
| @@ -96,17 +90,18 @@ UI.SuggestBox = class { |
| * @param {!AnchorBox} anchorBox |
| */ |
| setPosition(anchorBox) { |
| - this._updateBoxPosition(anchorBox); |
| + this._updateBoxPosition(anchorBox, this._list.length()); |
| } |
| /** |
| * @param {!AnchorBox} anchorBox |
| + * @param {number} length |
| */ |
| - _updateBoxPosition(anchorBox) { |
| + _updateBoxPosition(anchorBox, length) { |
| console.assert(this._overlay); |
| - if (this._lastAnchorBox && this._lastAnchorBox.equals(anchorBox) && this._lastItemCount === this.itemCount()) |
| + if (this._lastAnchorBox && this._lastAnchorBox.equals(anchorBox) && this._lastItemCount === length) |
| return; |
| - this._lastItemCount = this.itemCount(); |
| + this._lastItemCount = length; |
| this._lastAnchorBox = anchorBox; |
| // Position relative to main DevTools element. |
| @@ -128,24 +123,28 @@ UI.SuggestBox = class { |
| var maxHeight = Math.min( |
| Math.max(underHeight, aboveHeight) - spacer, |
| this._maxItemsHeight ? this._maxItemsHeight * this._rowHeight : Infinity); |
| - var height = this._rowHeight * this._items.length; |
| + var height = this._rowHeight * length; |
| this._hasVerticalScroll = height > maxHeight; |
| this._element.style.height = Math.min(maxHeight, height) + 'px'; |
| } |
| - _updateWidth() { |
| + /** |
| + * @param {!UI.SuggestBox.Suggestions} items |
| + */ |
| + _updateWidth(items) { |
| if (this._hasVerticalScroll) { |
| this._element.style.width = '100vw'; |
| return; |
| } |
| + if (!items.length) |
| + return; |
| // If there are no scrollbars, set the width to the width of the largest row. |
| - var maxIndex = 0; |
| - for (var i = 0; i < this._items.length; i++) { |
| - if (this._items[i].title.length > this._items[maxIndex].title.length) |
| - maxIndex = i; |
| + var maxItem = items[0]; |
| + for (var i = 1; i < items.length; i++) { |
| + if (items[i].title.length > maxItem.title.length) |
| + maxItem = items[i]; |
| } |
| - var element = /** @type {!Element} */ (this.itemElement(maxIndex)); |
| - this._element.style.width = UI.measurePreferredSize(element, this._element).width + 'px'; |
| + this._element.style.width = UI.measurePreferredSize(this.createElementForItem(maxItem), this._element).width + 'px'; |
| } |
| /** |
| @@ -175,10 +174,8 @@ UI.SuggestBox = class { |
| this._bodyElement.addEventListener('mousedown', this._maybeHideBound, true); |
| this._overlay = new UI.SuggestBox.Overlay(); |
| this._overlay.setContentElement(this._container); |
| - var measuringElement = this._createItemElement('1', '12'); |
| - this._viewport.element.appendChild(measuringElement); |
| - this._rowHeight = measuringElement.getBoundingClientRect().height; |
| - measuringElement.remove(); |
| + this._rowHeight = |
| + UI.measurePreferredSize(this.createElementForItem({title: '1', subtitle: '12'}), this._element).height; |
| } |
| hide() { |
| @@ -191,8 +188,6 @@ UI.SuggestBox = class { |
| this._container.remove(); |
| this._overlay.dispose(); |
| delete this._overlay; |
| - delete this._selectedElement; |
| - this._selectedIndex = -1; |
| delete this._lastAnchorBox; |
| } |
| @@ -210,10 +205,10 @@ UI.SuggestBox = class { |
| return true; |
| } |
| - if (!this.visible() || !this._selectedElement) |
| + if (!this.visible() || !this._list.selectedItem()) |
| return false; |
| - var suggestion = this._selectedElement.__fullValue; |
| + var suggestion = this._list.selectedItem().title; |
| if (!suggestion) |
| return false; |
| @@ -236,57 +231,29 @@ UI.SuggestBox = class { |
| } |
| /** |
| - * @param {number} shift |
| - * @param {boolean=} isCircular |
| - * @return {boolean} is changed |
| - */ |
| - _selectClosest(shift, isCircular) { |
| - if (!this._length) |
| - return false; |
| - |
| - this._userInteracted = true; |
| - |
| - if (this._selectedIndex === -1 && shift < 0) |
| - shift += 1; |
| - |
| - var index = this._selectedIndex + shift; |
| - |
| - if (isCircular) |
| - index = (this._length + index) % this._length; |
| - else |
| - index = Number.constrain(index, 0, this._length - 1); |
| - |
| - this._selectItem(index); |
| - return true; |
| - } |
| - |
| - /** |
| - * @param {!Event} event |
| + * @override |
| + * @return {?number} |
| */ |
| - _onItemMouseDown(event) { |
| - this._selectedElement = event.currentTarget; |
| - this.acceptSuggestion(); |
| - event.consume(true); |
| + fixedHeight() { |
| + return this._rowHeight; |
| } |
| /** |
| - * @param {string} query |
| - * @param {string} title |
| - * @param {string=} subtitle |
| - * @param {string=} iconType |
| - * @param {boolean=} isSecondary |
| + * @override |
| + * @param {!UI.SuggestBox.Suggestion} item |
| * @return {!Element} |
| */ |
| - _createItemElement(query, title, subtitle, iconType, isSecondary) { |
| + createElementForItem(item) { |
| + var query = this._userEnteredText; |
| var element = createElementWithClass('div', 'suggest-box-content-item source-code'); |
| - if (iconType) { |
| - var icon = UI.Icon.create(iconType, 'suggestion-icon'); |
| + if (item.iconType) { |
| + var icon = UI.Icon.create(item.iconType, 'suggestion-icon'); |
| element.appendChild(icon); |
| } |
| - if (isSecondary) |
| + if (item.isSecondary) |
| element.classList.add('secondary'); |
| element.tabIndex = -1; |
| - var displayText = title.trimEnd(50 + query.length); |
| + var displayText = item.title.trimEnd(50 + query.length); |
| var titleElement = element.createChild('span', 'suggestion-title'); |
| var index = displayText.toLowerCase().indexOf(query.toLowerCase()); |
| @@ -296,41 +263,78 @@ UI.SuggestBox = class { |
| titleElement.createChild('span', 'query').textContent = displayText.substring(index, index + query.length); |
| titleElement.createChild('span').textContent = displayText.substring(index > -1 ? index + query.length : 0); |
| titleElement.createChild('span', 'spacer'); |
| - if (subtitle) { |
| + if (item.subtitle) { |
| var subtitleElement = element.createChild('span', 'suggestion-subtitle'); |
| - subtitleElement.textContent = subtitle.trimEnd(15); |
| + subtitleElement.textContent = item.subtitle.trimEnd(15); |
| } |
| - element.__fullValue = title; |
| element.addEventListener('mousedown', this._onItemMouseDown.bind(this), false); |
| return element; |
| } |
| /** |
| - * @param {!UI.SuggestBox.Suggestions} items |
| - * @param {string} userEnteredText |
| - * @param {function(number): !Promise<{detail:string, description:string}>=} asyncDetails |
| + * @override |
| + * @param {!UI.SuggestBox.Suggestion} item |
| + * @return {number} |
| + */ |
| + heightForItem(item) { |
| + return this._rowHeight; |
| + } |
| + |
| + /** |
| + * @override |
| + * @param {!UI.SuggestBox.Suggestion} item |
| + * @return {boolean} |
| + */ |
| + isItemSelectable(item) { |
| + return true; |
| + } |
| + |
| + /** |
| + * @override |
| + * @param {?UI.SuggestBox.Suggestion} from |
| + * @param {?UI.SuggestBox.Suggestion} to |
| + * @param {?Element} fromElement |
| + * @param {?Element} toElement |
| + */ |
| + selectedItemChanged(from, to, fromElement, toElement) { |
| + if (fromElement) { |
| + fromElement.classList.remove('selected'); |
|
alph
2016/12/27 23:38:33
remove( ..., ... )
dgozman
2016/12/28 06:15:21
Done.
|
| + fromElement.classList.remove('force-white-icons'); |
| + } |
| + if (toElement) { |
| + toElement.classList.add('selected'); |
| + toElement.classList.add('force-white-icons'); |
| + } |
| + if (to) { |
|
alph
2016/12/27 23:38:33
if (!to) return;
dgozman
2016/12/28 06:15:21
Done.
|
| + this._detailsPopup.classList.add('hidden'); |
| + this._asyncDetails(to).then((details) => { |
|
alph
2016/12/27 23:38:33
drop ()
dgozman
2016/12/28 06:15:21
Done.
|
| + if (this._list.selectedItem() === to) |
| + this._showDetailsPopup(details); |
| + }); |
| + this._applySuggestion(true); |
| + } |
| + } |
| + |
| + /** |
| + * @param {!Event} event |
| */ |
| - _updateItems(items, userEnteredText, asyncDetails) { |
| - this._length = items.length; |
| - this._asyncDetailsPromises.clear(); |
| - this._asyncDetailsCallback = asyncDetails; |
| - this._elementList = []; |
| - delete this._selectedElement; |
| - |
| - this._userEnteredText = userEnteredText; |
| - this._items = items; |
| + _onItemMouseDown(event) { |
| + if (this._list.onClick(event)) { |
|
alph
2016/12/27 23:38:33
if (!...) return
dgozman
2016/12/28 06:15:21
Done.
|
| + this.acceptSuggestion(); |
| + event.consume(true); |
| + } |
| } |
| /** |
| - * @param {number} index |
| + * @param {!UI.SuggestBox.Suggestion} item |
| * @return {!Promise<?{detail: string, description: string}>} |
| */ |
| - _asyncDetails(index) { |
| + _asyncDetails(item) { |
| if (!this._asyncDetailsCallback) |
| return Promise.resolve(/** @type {?{description: string, detail: string}} */ (null)); |
| - if (!this._asyncDetailsPromises.has(index)) |
| - this._asyncDetailsPromises.set(index, this._asyncDetailsCallback(index)); |
| - return /** @type {!Promise<?{detail: string, description: string}>} */ (this._asyncDetailsPromises.get(index)); |
| + if (!this._asyncDetailsPromises.has(item)) |
| + this._asyncDetailsPromises.set(item, this._asyncDetailsCallback(item)); |
| + return /** @type {!Promise<?{detail: string, description: string}>} */ (this._asyncDetailsPromises.get(item)); |
| } |
| /** |
| @@ -346,39 +350,6 @@ UI.SuggestBox = class { |
| } |
| /** |
| - * @param {number} index |
| - */ |
| - _selectItem(index) { |
| - if (this._selectedElement) { |
| - this._selectedElement.classList.remove('selected'); |
| - this._selectedElement.classList.remove('force-white-icons'); |
| - } |
| - |
| - this._selectedIndex = index; |
| - if (index < 0) |
| - return; |
| - |
| - this._selectedElement = this.itemElement(index); |
| - this._selectedElement.classList.add('selected'); |
| - this._selectedElement.classList.add('force-white-icons'); |
| - this._detailsPopup.classList.add('hidden'); |
| - var elem = this._selectedElement; |
| - this._asyncDetails(index).then(showDetails.bind(this), function() {}); |
| - |
| - this._viewport.scrollItemIntoView(index); |
| - this._applySuggestion(true); |
| - |
| - /** |
| - * @param {?{detail: string, description: string}} details |
| - * @this {UI.SuggestBox} |
| - */ |
| - function showDetails(details) { |
| - if (elem === this._selectedElement) |
| - this._showDetailsPopup(details); |
| - } |
| - } |
| - |
| - /** |
| * @param {!UI.SuggestBox.Suggestions} completions |
| * @param {boolean} canShowForSingleItem |
| * @param {string} userEnteredText |
| @@ -398,15 +369,6 @@ UI.SuggestBox = class { |
| return canShowForSingleItem && completions[0].title !== userEnteredText; |
| } |
| - _ensureRowCountPerViewport() { |
| - if (this._rowCountPerViewport) |
| - return; |
| - if (!this._items.length) |
| - return; |
| - |
| - this._rowCountPerViewport = Math.floor(this._element.getBoundingClientRect().height / this._rowHeight); |
| - } |
| - |
| /** |
| * @param {!AnchorBox} anchorBox |
| * @param {!UI.SuggestBox.Suggestions} completions |
| @@ -424,11 +386,20 @@ UI.SuggestBox = class { |
| asyncDetails) { |
| delete this._onlyCompletion; |
| if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) { |
| - this._updateItems(completions, userEnteredText, asyncDetails); |
| + this._asyncDetailsPromises.clear(); |
| + if (asyncDetails) |
| + this._asyncDetailsCallback = (item) => asyncDetails.call(null, completions.indexOf(item)); |
|
alph
2016/12/27 23:38:33
item => asyncDetails(completions.indexOf(item));
dgozman
2016/12/28 06:15:21
Done.
|
| + else |
| + this._asyncDetailsCallback = null; |
| + this._userEnteredText = userEnteredText; |
| + |
| this._show(); |
| - this._updateBoxPosition(anchorBox); |
| - this._updateWidth(); |
| - this._viewport.refresh(); |
| + this._updateBoxPosition(anchorBox, completions.length); |
| + this._updateWidth(completions); |
| + |
| + this._list.reset(this, false); |
| + this._list.replaceAllItems(completions); |
| + |
| var highestPriorityItem = -1; |
| if (selectHighestPriority) { |
| var highestPriority = -Infinity; |
| @@ -440,8 +411,7 @@ UI.SuggestBox = class { |
| } |
| } |
| } |
| - this._selectItem(highestPriorityItem); |
| - delete this._rowCountPerViewport; |
| + this._list.selectItemAtIndex(highestPriorityItem, true); |
| } else { |
| if (completions.length === 1) { |
| this._onlyCompletion = completions[0].title; |
| @@ -456,100 +426,38 @@ UI.SuggestBox = class { |
| * @return {boolean} |
| */ |
| keyPressed(event) { |
| - switch (event.key) { |
| - case 'ArrowUp': |
| - return this.upKeyPressed(); |
| - case 'ArrowDown': |
| - return this.downKeyPressed(); |
| - case 'PageUp': |
| - return this.pageUpKeyPressed(); |
| - case 'PageDown': |
| - return this.pageDownKeyPressed(); |
| - case 'Enter': |
| - return this.enterKeyPressed(); |
| + if (this._list.onKeyDown(event)) { |
| + this._userInteracted = true; |
| + return true; |
| } |
| + if (event.key === 'Enter') |
| + return this.enterKeyPressed(); |
| return false; |
| } |
| /** |
| * @return {boolean} |
| */ |
| - upKeyPressed() { |
| - return this._selectClosest(-1, true); |
| - } |
| - |
| - /** |
| - * @return {boolean} |
| - */ |
| - downKeyPressed() { |
| - return this._selectClosest(1, true); |
| - } |
| - |
| - /** |
| - * @return {boolean} |
| - */ |
| - pageUpKeyPressed() { |
| - this._ensureRowCountPerViewport(); |
| - return this._selectClosest(-this._rowCountPerViewport, false); |
| - } |
| - |
| - /** |
| - * @return {boolean} |
| - */ |
| - pageDownKeyPressed() { |
| - this._ensureRowCountPerViewport(); |
| - return this._selectClosest(this._rowCountPerViewport, false); |
| - } |
| - |
| - /** |
| - * @return {boolean} |
| - */ |
| enterKeyPressed() { |
| if (!this._userInteracted && this._captureEnter) |
| return false; |
| - var hasSelectedItem = !!this._selectedElement || this._onlyCompletion; |
| + var hasSelectedItem = !!this._list.selectedItem() || this._onlyCompletion; |
| this.acceptSuggestion(); |
| // Report the event as non-handled if there is no selected item, |
| // to commit the input or handle it otherwise. |
| return hasSelectedItem; |
| } |
| - |
| - /** |
| - * @override |
| - * @param {number} index |
| - * @return {number} |
| - */ |
| - fastItemHeight(index) { |
| - return this._rowHeight; |
| - } |
| - |
| - /** |
| - * @override |
| - * @return {number} |
| - */ |
| - itemCount() { |
| - return this._items.length; |
| - } |
| - |
| - /** |
| - * @override |
| - * @param {number} index |
| - * @return {?Element} |
| - */ |
| - itemElement(index) { |
| - if (!this._elementList[index]) { |
| - this._elementList[index] = this._createItemElement( |
| - this._userEnteredText, this._items[index].title, this._items[index].subtitle, this._items[index].iconType, |
| - this._items[index].isSecondary); |
| - } |
| - return this._elementList[index]; |
| - } |
| }; |
| /** |
| - * @typedef {!Array.<{title: string, subtitle: (string|undefined), iconType: (string|undefined), priority: (number|undefined), isSecondary: (boolean|undefined)}>} |
| + * @typedef {!{title: string, subtitle: (string|undefined), iconType: (string|undefined), priority: (number|undefined), isSecondary: (boolean|undefined)}} |
| + */ |
| +UI.SuggestBox.Suggestion; |
| + |
| +/** |
| + * @typedef {!Array<!UI.SuggestBox.Suggestion>} |
| */ |
| UI.SuggestBox.Suggestions; |