Chromium Code Reviews| Index: third_party/WebKit/Source/devtools/front_end/ui/ViewportControl.js |
| diff --git a/third_party/WebKit/Source/devtools/front_end/ui/ViewportControl.js b/third_party/WebKit/Source/devtools/front_end/ui/ViewportControl.js |
| index dbd36008c8a08dd7baf1be0f7679ec221d521a0e..f912ee040155a804373ab5de7df3cf1038d8cded 100644 |
| --- a/third_party/WebKit/Source/devtools/front_end/ui/ViewportControl.js |
| +++ b/third_party/WebKit/Source/devtools/front_end/ui/ViewportControl.js |
| @@ -3,167 +3,315 @@ |
| // found in the LICENSE file. |
| /** |
| * @unrestricted |
| + * @template T |
|
einbinder
2016/12/21 00:23:53
T needs to be some kind of object, otherwise you c
|
| */ |
| UI.ViewportControl = class { |
| /** |
| - * @param {!UI.ViewportControl.Provider} provider |
| + * @param {?function(T):!Element} renderer |
| */ |
| - constructor(provider) { |
| + constructor(renderer) { |
| this.element = createElement('div'); |
| this.element.style.overflow = 'auto'; |
| - this._innerElement = this.element.createChild('div'); |
| - this._innerElement.style.height = '0px'; |
| - this._innerElement.style.position = 'relative'; |
| - this._innerElement.style.overflow = 'hidden'; |
| - |
| - this._provider = provider; |
| - this.element.addEventListener('scroll', this._update.bind(this), false); |
| - this._itemCount = 0; |
| - this._indexSymbol = Symbol('UI.ViewportControl._indexSymbol'); |
| - } |
| - |
| - refresh() { |
| - this._itemCount = this._provider.itemCount(); |
| - this._innerElement.removeChildren(); |
| - |
| - var height = 0; |
| - this._cumulativeHeights = new Int32Array(this._itemCount); |
| - for (var i = 0; i < this._itemCount; ++i) { |
| - height += this._provider.fastItemHeight(i); |
| - this._cumulativeHeights[i] = height; |
| - } |
| - this._innerElement.style.height = height + 'px'; |
| + this._topElement = this.element.createChild('div'); |
| + this._bottomElement = this.element.createChild('div'); |
| + |
| + this._firstIndex = 0; |
| + this._lastIndex = 0; |
| + this._renderedHeight = 0; |
| + |
| + this._fixedHeight = 0; |
| + /** @type {?function(T):!Element} */ |
| + this._renderer = renderer; |
| + /** @type {!Array<T>} */ |
| + this._items = []; |
| - this._update(); |
| + this._elementSymbol = Symbol('UI.ViewportControl.element'); |
|
einbinder
2016/12/21 00:23:53
It might make sense to use a WeakMap (or just a Ma
dgozman
2016/12/27 21:32:59
Done.
|
| + this.element.addEventListener('scroll', this._onScroll.bind(this), false); |
| + this._update(0, this.element.offsetHeight); |
| } |
| - _update() { |
| - if (!this._cumulativeHeights) { |
| - this.refresh(); |
| - return; |
| - } |
| + setVariableHeight() { |
| + // TODO(dgozman): support variable height. |
| + throw 'Not supported'; |
| + } |
| - var visibleHeight = this._visibleHeight(); |
| - var visibleFrom = this.element.scrollTop; |
| - var activeHeight = visibleHeight * 2; |
| - var firstActiveIndex = Math.max( |
| - Array.prototype.lowerBound.call(this._cumulativeHeights, visibleFrom + 1 - (activeHeight - visibleHeight) / 2), |
| - 0); |
| - var lastActiveIndex = Math.min( |
| - Array.prototype.lowerBound.call( |
| - this._cumulativeHeights, visibleFrom + visibleHeight + (activeHeight - visibleHeight) / 2), |
| - this._itemCount - 1); |
| - |
| - var children = this._innerElement.children; |
| - for (var i = children.length - 1; i >= 0; --i) { |
| - var element = children[i]; |
| - if (element[this._indexSymbol] < firstActiveIndex || element[this._indexSymbol] > lastActiveIndex) |
| - element.remove(); |
| - } |
| + /** |
| + * @param {number} elementHeight |
| + */ |
| + setFixedHeight(elementHeight) { |
| + this._fixedHeight = elementHeight; |
| + } |
| - for (var i = firstActiveIndex; i <= lastActiveIndex; ++i) |
| - this._insertElement(i); |
| + /** |
| + * @param {?function(T):!Element} renderer |
| + */ |
| + setRenderer(renderer) { |
| + this._renderer = renderer; |
| + for (var i = 0; i < this._items.length; i++) |
| + this._items[i][this._elementSymbol] = null; |
| + this._refresh(); |
| } |
| /** |
| - * @param {number} index |
| + * @param {number} from |
| + * @param {number} to |
| */ |
| - _insertElement(index) { |
| - var element = this._provider.itemElement(index); |
| - if (!element || element.parentElement === this._innerElement) |
| - return; |
| + refreshRange(from, to) { |
| + this._invalidate(from, to); |
| + } |
| - element.style.position = 'absolute'; |
| - element.style.top = (this._cumulativeHeights[index - 1] || 0) + 'px'; |
| - element.style.left = '0'; |
| - element.style.right = '0'; |
| - element[this._indexSymbol] = index; |
| - this._innerElement.appendChild(element); |
| + resized() { |
| + this._refresh(); |
| } |
| /** |
| - * @return {number} |
| + * @param {number} index |
| + */ |
| + scrollItemAtIndexIntoView(index) { |
| + var top = this._offsetAtIndex(index); |
| + var bottom = this._offsetAtIndex(index + 1); |
| + var scrollTop = this.element.scrollTop; |
| + var height = this.element.offsetHeight; |
| + if (top < scrollTop) |
| + this._update(top, height); |
| + else if (bottom > scrollTop + height) |
| + this._update(bottom - height, height); |
| + } |
| + |
| + /** |
| + * @param {number} index |
| + * @return {!Element} |
| */ |
| - firstVisibleIndex() { |
| - return Math.max(Array.prototype.lowerBound.call(this._cumulativeHeights, this.element.scrollTop + 1), 0); |
| + elementAtIndex(index) { |
| + return this._elementAtIndex(index); |
| } |
| /** |
| * @return {number} |
| */ |
| - lastVisibleIndex() { |
| - return Math.min( |
| - Array.prototype.lowerBound.call(this._cumulativeHeights, this.element.scrollTop + this._visibleHeight()), |
| - this._itemCount); |
| + length() { |
| + return this._items.length; |
| } |
| /** |
| * @param {number} index |
| - * @param {boolean=} makeLast |
| + * @return {T} |
| */ |
| - scrollItemIntoView(index, makeLast) { |
| - var firstVisibleIndex = this.firstVisibleIndex(); |
| - var lastVisibleIndex = this.lastVisibleIndex(); |
| - if (index > firstVisibleIndex && index < lastVisibleIndex) |
| - return; |
| - if (makeLast) |
| - this.forceScrollItemToBeLast(index); |
| - else if (index <= firstVisibleIndex) |
| - this.forceScrollItemToBeFirst(index); |
| - else if (index >= lastVisibleIndex) |
| - this.forceScrollItemToBeLast(index); |
| + itemAtIndex(index) { |
| + return this._items[index]; |
| + } |
| + |
| + /** |
| + * @param {T} item |
| + */ |
| + pushItem(item) { |
| + this._items.push(item); |
| + this._invalidate(this._items.length - 1, this._items.length - 1); |
| + } |
| + |
| + /** |
| + * @return {T} |
| + */ |
| + popItem() { |
| + var result = this._items.pop(); |
| + result[this._elementSymbol] = null; |
| + this._invalidate(this._items.length - 1, this._items.length); |
| + return result; |
| } |
| /** |
| * @param {number} index |
| + * @param {T} item |
| */ |
| - forceScrollItemToBeFirst(index) { |
| - this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0; |
| - this._update(); |
| + insertItemAtIndex(index, item) { |
| + this._items.splice(index, 0, item); |
| + this._invalidate(index, index); |
| } |
| /** |
| * @param {number} index |
| + * @return {T} |
| */ |
| - forceScrollItemToBeLast(index) { |
| - this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeight(); |
| - this._update(); |
| + removeItemAtIndex(index) { |
| + var result = this._items[index]; |
| + this._items.splice(index, 1); |
| + result[this._elementSymbol] = null; |
| + this._invalidate(index, index + 1); |
| + return result; |
| } |
| /** |
| - * @return {number} |
| + * @param {number} from |
| + * @param {number} to |
| + * @param {!Array<T>} items |
| */ |
| - _visibleHeight() { |
| - return this.element.offsetHeight; |
| + replaceItemsInRange(from, to, items) { |
| + for (var i = from; i < to; i++) |
| + this._items[i][this._elementSymbol] = null; |
| + this._items.splice.bind(this._items, from, to - from).apply(null, items); |
| + this._invalidate(from, to); |
| } |
| -}; |
| -/** |
| - * @interface |
| - */ |
| -UI.ViewportControl.Provider = function() {}; |
| + /** |
| + * @param {!Array<T>} items |
| + */ |
| + replaceAllItems(items) { |
| + var length = this._items.length; |
| + for (var i = 0; i < this._items.length; i++) |
| + this._items[i][this._elementSymbol] = null; |
| + this._items = items; |
| + this._invalidate(0, length); |
| + } |
| -UI.ViewportControl.Provider.prototype = { |
| /** |
| - * @param {number} index |
| * @return {number} |
| */ |
| - fastItemHeight(index) { |
| - return 0; |
| - }, |
| + _totalHeight() { |
| + return this._items.length * this._fixedHeight; |
| + } |
| /** |
| + * @param {number} offset |
| * @return {number} |
| */ |
| - itemCount() { |
| - return 0; |
| - }, |
| + _indexAtOffset(offset) { |
| + if (!this._items.length) |
| + return 0; |
| + var index = Math.floor(offset / this._fixedHeight); |
| + if (index >= this._items.length) |
| + return this._items.length - 1; |
| + return index; |
| + } |
| + |
| + /** |
| + * @param {number} index |
| + * @return {!Element} |
| + */ |
| + _elementAtIndex(index) { |
| + var item = this._items[index]; |
| + var element = item[this._elementSymbol]; |
| + if (!element) { |
| + if (this._renderer) { |
| + element = this._renderer.call(null, item); |
| + } else { |
| + element = createElement('div'); |
| + // TODO(dgozman): support variable height. |
| + element.style.height = this._fixedHeight + 'px'; |
| + } |
| + item[this._elementSymbol] = element; |
| + } |
| + return element; |
| + } |
| /** |
| * @param {number} index |
| - * @return {?Element} |
| + * @return {number} |
| + */ |
| + _offsetAtIndex(index) { |
| + return index * this._fixedHeight; |
| + } |
| + |
| + /** |
| + * @param {number} from |
| + * @param {number} to |
| */ |
| - itemElement(index) { |
| - return null; |
| + _invalidate(from, to) { |
| + var availableHeight = this.element.offsetHeight; |
| + var totalHeight = this._totalHeight(); |
| + if (this._renderedHeight < availableHeight || totalHeight < availableHeight) { |
| + this._refresh(); |
| + return; |
| + } |
| + |
| + var scrollTop = this.element.scrollTop; |
| + var heightDelta = totalHeight - this._renderedHeight; |
| + if (to <= this._firstIndex) { |
| + var topHeight = this._topHeight + heightDelta; |
| + this._topElement.style.height = topHeight + 'px'; |
| + this.element.scrollTop = scrollTop + heightDelta; |
| + this._topHeight = topHeight; |
| + this._renderedHeight = totalHeight; |
| + return; |
| + } |
| + |
| + if (from >= this._lastIndex) { |
| + var bottomHeight = this._bottomHeight + heightDelta; |
| + this._bottomElement.style.height = bottomHeight + 'px'; |
| + this.element.scrollTop = scrollTop + heightDelta; |
| + this._bottomHeight = bottomHeight; |
| + this._renderedHeight = totalHeight; |
| + return; |
| + } |
| + |
| + this._refresh(); |
| + } |
| + |
| + _refresh() { |
| + var height = this.element.offsetHeight; |
| + var scrollTop = Math.max(0, Math.min(this.element.scrollTop, this._totalHeight() - height)); |
| + this._firstIndex = 0; |
| + this._lastIndex = 0; |
| + this._renderedHeight = 0; |
| + this.element.removeChildren(); |
| + this.element.appendChild(this._topElement); |
| + this.element.appendChild(this._bottomElement); |
| + this._update(scrollTop, height); |
| + } |
| + |
| + _onScroll() { |
| + this._update(this.element.scrollTop, this.element.offsetHeight); |
| + } |
| + |
| + /** |
| + * @param {number} scrollTop |
| + * @param {number} height |
| + */ |
| + _update(scrollTop, height) { |
| + // Note: this method should not force layout. Be careful. |
| + |
| + var totalHeight = this._totalHeight(); |
| + if (!totalHeight) { |
| + this._firstIndex = 0; |
| + this._lastIndex = 0; |
| + this._topHeight = 0; |
| + this._bottomHeight = 0; |
| + this._renderedHeight = 0; |
| + this._topElement.style.height = '0px'; |
| + this._bottomElement.style.height = '0px'; |
| + return; |
| + } |
| + |
| + var firstIndex = this._indexAtOffset(Math.max(0, scrollTop - height)); |
| + var lastIndex = this._indexAtOffset(Math.min(totalHeight, scrollTop + 2 * height)) + 1; |
| + |
| + for (var index = this._firstIndex; index < firstIndex; index++) { |
| + var element = this._elementAtIndex(index); |
| + element.remove(); |
| + this._firstIndex++; |
| + } |
| + for (var index = this._lastIndex - 1; index >= lastIndex; index--) { |
| + var element = this._elementAtIndex(index); |
| + element.remove(); |
| + this._lastIndex--; |
| + } |
| + this._firstIndex = Math.min(this._firstIndex, lastIndex); |
| + this._lastIndex = Math.max(this._lastIndex, firstIndex); |
| + for (var index = this._firstIndex - 1; index >= firstIndex; index--) { |
| + var element = this._elementAtIndex(index); |
| + this.element.insertBefore(element, this._topElement.nextSibling); |
| + } |
| + for (var index = this._lastIndex; index < lastIndex; index++) { |
| + var element = this._elementAtIndex(index); |
| + this.element.insertBefore(element, this._bottomElement); |
| + } |
| + |
| + this._firstIndex = firstIndex; |
| + this._lastIndex = lastIndex; |
| + this._topHeight = this._offsetAtIndex(firstIndex); |
| + this._topElement.style.height = this._topHeight + 'px'; |
| + this._bottomHeight = (totalHeight - this._offsetAtIndex(lastIndex)); |
| + this._bottomElement.style.height = this._bottomHeight + 'px'; |
| + this._renderedHeight = totalHeight; |
| + this.element.scrollTop = scrollTop; |
| } |
| }; |