Chromium Code Reviews| Index: third_party/WebKit/Source/devtools/front_end/ui/ListControl.js |
| diff --git a/third_party/WebKit/Source/devtools/front_end/ui/ListControl.js b/third_party/WebKit/Source/devtools/front_end/ui/ListControl.js |
| index a76022f03598a1dae20f6b5baac5182dd216d20a..2afc2ee30016eb697569b0567eb26306e626e2a7 100644 |
| --- a/third_party/WebKit/Source/devtools/front_end/ui/ListControl.js |
| +++ b/third_party/WebKit/Source/devtools/front_end/ui/ListControl.js |
| @@ -37,10 +37,11 @@ UI.ListDelegate.prototype = { |
| }; |
| /** @enum {symbol} */ |
| -UI.ListHeightMode = { |
| - Fixed: Symbol('UI.ListHeightMode.Fixed'), |
| - Measured: Symbol('UI.ListHeightMode.Measured'), |
| - Variable: Symbol('UI.ListHeightMode.Variable') |
| +UI.ListMode = { |
| + Grow: Symbol('UI.ListMode.Grow'), |
| + ViewportFixedItems: Symbol('UI.ListMode.ViewportFixedItems'), |
| + ViewportFixedItemsMeasured: Symbol('UI.ListMode.ViewportFixedItemsMeasured'), |
| + ViewportVariableItems: Symbol('UI.ListMode.ViewportVariableItems') |
| }; |
| /** |
| @@ -60,7 +61,6 @@ UI.ListControl = class { |
| this._renderedHeight = 0; |
| this._topHeight = 0; |
| this._bottomHeight = 0; |
| - this._clearViewport(); |
| /** @type {!Array<T>} */ |
| this._items = []; |
| @@ -76,24 +76,42 @@ UI.ListControl = class { |
| if (this.onClick(event)) |
| event.consume(true); |
| }; |
| + this._boundScroll = event => { |
| + this._updateViewport(this.element.scrollTop, this.element.offsetHeight); |
| + }; |
| this._delegate = delegate; |
| - this._heightMode = UI.ListHeightMode.Measured; |
| + this._mode = UI.ListMode.ViewportFixedItemsMeasured; |
| this._fixedHeight = 0; |
| this._variableOffsets = new Int32Array(0); |
| - |
| - this.element.addEventListener('scroll', this._onScroll.bind(this), false); |
| + this._clearViewport(); |
| + this.element.addEventListener('scroll', this._boundScroll, false); |
| } |
| /** |
| - * @param {!UI.ListHeightMode} mode |
| + * @param {!UI.ListMode} mode |
| */ |
| - setHeightMode(mode) { |
| - this._heightMode = mode; |
| - this._fixedHeight = 0; |
| - if (this._items.length) { |
| - this._itemToElement.clear(); |
| - this._invalidate(0, this._items.length, this._items.length); |
| + setMode(mode) { |
|
caseq
2016/12/29 19:25:31
Should we even allow changing mode on the fly? Per
dgozman
2016/12/29 20:38:44
Done.
|
| + if (mode === UI.ListMode.Grow) { |
| + if (this._mode !== UI.ListMode.Grow) |
| + this.element.removeEventListener('scroll', this._boundScroll, false); |
|
caseq
2016/12/29 19:25:31
do it unconditionally just in case?
|
| + this._mode = mode; |
|
caseq
2016/12/29 19:25:30
this would go before if then
|
| + if (this._items.length) { |
| + this._itemToElement.clear(); |
| + this._clearContents(); |
| + this._invalidateGrowMode(0, 0, this._items.length); |
| + } |
| + } else { |
| + if (this._mode === UI.ListMode.Grow) |
| + this.element.addEventListener('scroll', this._boundScroll, false); |
| + this._mode = mode; |
| + this._fixedHeight = 0; |
| + if (this._items.length) { |
| + this._itemToElement.clear(); |
| + if (this._mode === UI.ListMode.Grow) |
|
caseq
2016/12/29 19:25:30
this can't be true :-)
|
| + this._clearViewport(); |
| + this._invalidate(0, this._items.length, this._items.length); |
| + } |
| } |
| } |
| @@ -181,9 +199,9 @@ UI.ListControl = class { |
| if (this._selectedIndex >= to) { |
| this._selectedIndex += items.length - (to - from); |
| } else if (this._selectedIndex >= from) { |
| - var index = this._findClosestSelectable(from + items.length, +1, 0, false); |
| + var index = this._findFirstSelectable(from + items.length, +1, false); |
| if (index === -1) |
| - index = this._findClosestSelectable(from - 1, -1, 0, false); |
| + index = this._findFirstSelectable(from - 1, -1, false); |
| this._select(index, oldSelectedItem, oldSelectedElement); |
| } |
| } |
| @@ -204,8 +222,9 @@ UI.ListControl = class { |
| } |
| viewportResized() { |
| - // TODO(dgozman): try to keep the visible scrollTop the same |
| - // when invalidating after firstIndex but before first visible element. |
| + if (this._mode === UI.ListMode.Grow) |
| + return; |
| + // TODO(dgozman): try to keep visible scrollTop the same. |
| var scrollTop = this.element.scrollTop; |
| var viewportHeight = this.element.offsetHeight; |
| this._clearViewport(); |
| @@ -216,6 +235,10 @@ UI.ListControl = class { |
| * @param {number} index |
| */ |
| scrollItemAtIndexIntoView(index) { |
| + if (this._mode === UI.ListMode.Grow) { |
| + this._elementAtIndex(index).scrollIntoViewIfNeeded(false); |
| + return; |
| + } |
| var top = this._offsetAtIndex(index); |
| var bottom = this._offsetAtIndex(index + 1); |
| var scrollTop = this.element.scrollTop; |
| @@ -260,22 +283,24 @@ UI.ListControl = class { |
| var index = -1; |
| switch (event.key) { |
| case 'ArrowUp': |
| - index = this._selectedIndex === -1 ? this._findClosestSelectable(this._items.length - 1, -1, 0, true) : |
| - this._findClosestSelectable(this._selectedIndex, -1, 1, true); |
| + index = this._selectedIndex === -1 ? this._items.length - 1 : this._selectedIndex - 1; |
| + index = this._findFirstSelectable(index, -1, true); |
| break; |
| case 'ArrowDown': |
| - index = this._selectedIndex === -1 ? this._findClosestSelectable(0, +1, 0, true) : |
| - this._findClosestSelectable(this._selectedIndex, +1, 1, true); |
| + index = this._selectedIndex === -1 ? 0 : this._selectedIndex + 1; |
| + index = this._findFirstSelectable(index, +1, true); |
| break; |
| case 'PageUp': |
| + if (this._mode === UI.ListMode.Grow) |
| + return false; |
| index = this._selectedIndex === -1 ? this._items.length - 1 : this._selectedIndex; |
| - // Compensate for zoom rounding errors with -1. |
| - index = this._findClosestSelectable(index, -1, this.element.offsetHeight - 1, false); |
| + index = this._findPageSelectable(index, -1); |
| break; |
| case 'PageDown': |
| + if (this._mode === UI.ListMode.Grow) |
| + return false; |
| index = this._selectedIndex === -1 ? 0 : this._selectedIndex; |
| - // Compensate for zoom rounding errors with -1. |
| - index = this._findClosestSelectable(index, +1, this.element.offsetHeight - 1, false); |
| + index = this._findPageSelectable(index, +1); |
| break; |
| default: |
| return false; |
| @@ -298,9 +323,19 @@ UI.ListControl = class { |
| node = node.parentNodeOrShadowHost(); |
| if (!node || node.nodeType !== Node.ELEMENT_NODE) |
| return false; |
| - var offset = /** @type {!Element} */ (node).getBoundingClientRect().top; |
| - offset -= this.element.getBoundingClientRect().top; |
| - var index = this._indexAtOffset(offset + this.element.scrollTop); |
| + var index = -1; |
| + if (this._mode === UI.ListMode.Grow) { |
| + for (var i = 0; i < this._items.length; i++) { |
|
caseq
2016/12/29 19:25:31
use findIndex()?
dgozman
2016/12/29 20:38:44
Done.
|
| + if (this._itemToElement.get(this._items[i]) === node) { |
| + index = i; |
| + break; |
| + } |
| + } |
| + } else { |
| + var offset = /** @type {!Element} */ (node).getBoundingClientRect().top; |
| + offset -= this.element.getBoundingClientRect().top; |
| + index = this._indexAtOffset(offset + this.element.scrollTop); |
| + } |
| if (index === -1 || !this._delegate.isItemSelectable(this._items[index])) |
| return false; |
| this._select(index); |
| @@ -319,9 +354,11 @@ UI.ListControl = class { |
| * @return {number} |
| */ |
| _indexAtOffset(offset) { |
| + if (this._mode === UI.ListMode.Grow) |
| + throw 'There should be no offset conversions in grow mode'; |
| if (!this._items.length || offset < 0) |
| return 0; |
| - if (this._heightMode === UI.ListHeightMode.Variable) { |
| + if (this._mode === UI.ListMode.ViewportVariableItems) { |
| return Math.min( |
| this._items.length - 1, this._variableOffsets.lowerBound(offset, undefined, 0, this._items.length)); |
| } |
| @@ -349,9 +386,11 @@ UI.ListControl = class { |
| * @return {number} |
| */ |
| _offsetAtIndex(index) { |
| + if (this._mode === UI.ListMode.Grow) |
| + throw 'There should be no offset conversions in grow mode'; |
| if (!this._items.length) |
| return 0; |
| - if (this._heightMode === UI.ListHeightMode.Variable) |
| + if (this._mode === UI.ListMode.ViewportVariableItems) |
| return this._variableOffsets[index]; |
| if (!this._fixedHeight) |
| this._measureHeight(); |
| @@ -359,7 +398,7 @@ UI.ListControl = class { |
| } |
| _measureHeight() { |
| - if (this._heightMode === UI.ListHeightMode.Measured) |
| + if (this._mode === UI.ListMode.ViewportFixedItemsMeasured) |
| this._fixedHeight = UI.measurePreferredSize(this._elementAtIndex(0), this.element).height; |
| else |
| this._fixedHeight = this._delegate.heightForItem(this._items[0]); |
| @@ -384,41 +423,45 @@ UI.ListControl = class { |
| /** |
| * @param {number} index |
| * @param {number} direction |
| - * @param {number} minSkippedHeight |
| * @param {boolean} canWrap |
| * @return {number} |
| */ |
| - _findClosestSelectable(index, direction, minSkippedHeight, canWrap) { |
| + _findFirstSelectable(index, direction, canWrap) { |
| var length = this._items.length; |
| if (!length) |
| return -1; |
| - |
| - var lastSelectable = -1; |
| - var start = -1; |
| - var startOffset = this._offsetAtIndex(index); |
| - while (true) { |
| + for (var step = 0; step <= length; step++) { |
| if (index < 0 || index >= length) { |
| if (!canWrap) |
| - return lastSelectable; |
| + return -1; |
| index = (index + length) % length; |
| } |
| + if (this._delegate.isItemSelectable(this._items[index])) |
| + return index; |
| + index += direction; |
| + } |
| + return -1; |
| + } |
| - // Handle full wrap-around. |
| - if (index === start) |
| - return lastSelectable; |
| - if (start === -1) { |
| - start = index; |
| - startOffset = this._offsetAtIndex(index); |
| - } |
| - |
| + /** |
| + * @param {number} index |
| + * @param {number} direction |
| + * @return {number} |
| + */ |
| + _findPageSelectable(index, direction) { |
| + var lastSelectable = -1; |
| + var startOffset = this._offsetAtIndex(index); |
| + // Compensate for zoom rounding errors with -1. |
| + var viewportHeight = this.element.offsetHeight - 1; |
| + while (index >= 0 && index < this._items.length) { |
| if (this._delegate.isItemSelectable(this._items[index])) { |
| - if (Math.abs(this._offsetAtIndex(index) - startOffset) >= minSkippedHeight) |
| + if (Math.abs(this._offsetAtIndex(index) - startOffset) >= viewportHeight) |
| return index; |
| lastSelectable = index; |
| } |
| - |
| index += direction; |
| } |
| + return lastSelectable; |
| } |
| /** |
| @@ -443,12 +486,17 @@ UI.ListControl = class { |
| * @param {number} inserted |
| */ |
| _invalidate(from, to, inserted) { |
| - if (this._heightMode === UI.ListHeightMode.Variable) { |
| + if (this._mode === UI.ListMode.ViewportVariableItems) { |
| this._reallocateVariableOffsets(this._items.length + 1, from + 1); |
| for (var i = from + 1; i <= this._items.length; i++) |
| this._variableOffsets[i] = this._variableOffsets[i - 1] + this._delegate.heightForItem(this._items[i - 1]); |
| } |
| + if (this._mode === UI.ListMode.Grow) { |
|
caseq
2016/12/29 19:25:31
nit: move up
dgozman
2016/12/29 20:38:44
Done.
|
| + this._invalidateGrowMode(from, to - from, inserted); |
| + return; |
| + } |
| + |
| var viewportHeight = this.element.offsetHeight; |
| var totalHeight = this._totalHeight(); |
| var scrollTop = this.element.scrollTop; |
| @@ -480,18 +528,42 @@ UI.ListControl = class { |
| return; |
| } |
| - // TODO(dgozman): try to keep the visible scrollTop the same |
| + // TODO(dgozman): try to keep visible scrollTop the same |
| // when invalidating after firstIndex but before first visible element. |
| this._clearViewport(); |
| this._updateViewport(Number.constrain(scrollTop, 0, totalHeight - viewportHeight), viewportHeight); |
| } |
| + /** |
| + * @param {number} start |
| + * @param {number} remove |
| + * @param {number} add |
| + */ |
| + _invalidateGrowMode(start, remove, add) { |
| + var startElement = this._topElement; |
| + for (var index = 0; index < start; index++) |
| + startElement = startElement.nextSibling; |
|
caseq
2016/12/29 19:25:31
nextElementSibling
dgozman
2016/12/29 20:38:44
Done.
|
| + for (var index = 0; index < remove; index++) |
|
caseq
2016/12/29 19:25:30
nit: while (remove--)
dgozman
2016/12/29 20:38:44
Done.
|
| + startElement.nextSibling.remove(); |
|
caseq
2016/12/29 19:25:31
ditto.
|
| + for (var index = add - 1; index >= 0; index--) { |
| + var element = this._elementAtIndex(start + index); |
| + this.element.insertBefore(element, startElement.nextSibling); |
| + } |
| + } |
| + |
| _clearViewport() { |
| + if (this._mode === UI.ListMode.Grow) |
| + throw 'There should be no viewport updates in grow mode'; |
| this._firstIndex = 0; |
| this._lastIndex = 0; |
| this._renderedHeight = 0; |
| this._topHeight = 0; |
| this._bottomHeight = 0; |
| + this._clearContents(); |
| + } |
| + |
| + _clearContents() { |
| + // Note: this method should not force layout. Be careful. |
| this._topElement.style.height = '0'; |
| this._bottomElement.style.height = '0'; |
| this.element.removeChildren(); |
| @@ -499,16 +571,14 @@ UI.ListControl = class { |
| this.element.appendChild(this._bottomElement); |
| } |
| - _onScroll() { |
| - this._updateViewport(this.element.scrollTop, this.element.offsetHeight); |
| - } |
| - |
| /** |
| * @param {number} scrollTop |
| * @param {number} viewportHeight |
| */ |
| _updateViewport(scrollTop, viewportHeight) { |
| // Note: this method should not force layout. Be careful. |
| + if (this._mode === UI.ListMode.Grow) |
| + throw 'There should be no viewport updates in grow mode'; |
| var totalHeight = this._totalHeight(); |
| if (!totalHeight) { |