| 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..e868af22f6501ff876f7318c4b7a420758ebe606 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,330 @@
|
| // found in the LICENSE file.
|
| /**
|
| * @unrestricted
|
| + * @template T
|
| */
|
| UI.ViewportControl = class {
|
| - /**
|
| - * @param {!UI.ViewportControl.Provider} provider
|
| - */
|
| - constructor(provider) {
|
| + constructor() {
|
| 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._update();
|
| + this._topElement = this.element.createChild('div');
|
| + this._bottomElement = this.element.createChild('div');
|
| + this._firstIndex = 0;
|
| + this._lastIndex = 0;
|
| + this._fixedHeight = true;
|
| + this._measuredHeight = 0;
|
| + /** @type {?function(T):!Element} */
|
| + this._renderer = null;
|
| + /** @type {!Array<T>} */
|
| + this._items = [];
|
| + this._elementSymbol = Symbol('UI.ViewportControl.element');
|
| + this.element.addEventListener('scroll', this._onScroll.bind(this), false);
|
| + this._update(0);
|
| }
|
|
|
| - _update() {
|
| - if (!this._cumulativeHeights) {
|
| - this.refresh();
|
| - return;
|
| - }
|
| + setVariableHeights() {
|
| + // 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
|
| + */
|
| + setFixedHeights(elementHeight) {
|
| + this._fixedHeight = true;
|
| + this._measuredHeight = elementHeight;
|
| + }
|
|
|
| - for (var i = firstActiveIndex; i <= lastActiveIndex; ++i)
|
| - this._insertElement(i);
|
| + // !!!!! Move this to clients.
|
| + setFixedMeasuredHeights() {
|
| + this._fixedHeight = true;
|
| + this._measuredHeight = 0;
|
| }
|
|
|
| /**
|
| - * @param {number} index
|
| + * @param {?function(T):!Element} renderer
|
| */
|
| - _insertElement(index) {
|
| - var element = this._provider.itemElement(index);
|
| - if (!element || element.parentElement === this._innerElement)
|
| - return;
|
| + setRenderer(renderer) {
|
| + this._renderer = renderer;
|
| + for (var i = 0; i < this._items; i++)
|
| + this._items[i][this._elementSymbol] = null;
|
| + this._refresh();
|
| + }
|
|
|
| - 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);
|
| + slowRefresh() {
|
| + this._refresh();
|
| }
|
|
|
| /**
|
| * @return {number}
|
| */
|
| - firstVisibleIndex() {
|
| - return Math.max(Array.prototype.lowerBound.call(this._cumulativeHeights, this.element.scrollTop + 1), 0);
|
| + measuredElementHeight() {
|
| + return this._elementHeight();
|
| }
|
|
|
| /**
|
| * @return {number}
|
| */
|
| - lastVisibleIndex() {
|
| - return Math.min(
|
| - Array.prototype.lowerBound.call(this._cumulativeHeights, this.element.scrollTop + this._visibleHeight()),
|
| - this._itemCount);
|
| + elementsPerViewport() {
|
| + if (!this._fixedHeight)
|
| + throw 'Only supported in fixed-height scenario';
|
| + var elementHeight = this._elementHeight();
|
| + return elementHeight ? Math.floor(this.element.offsetHeight / elementHeight) : 0;
|
| }
|
|
|
| /**
|
| * @param {number} index
|
| - * @param {boolean=} makeLast
|
| + * @return {!Element}
|
| */
|
| - 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);
|
| + elementAtIndex(index) {
|
| + return this._elementAtIndex(index);
|
| + }
|
| +
|
| + /**
|
| + * @return {number}
|
| + */
|
| + length() {
|
| + return this._items.length;
|
| }
|
|
|
| /**
|
| * @param {number} index
|
| + * @return {T}
|
| */
|
| - forceScrollItemToBeFirst(index) {
|
| - this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0;
|
| - this._update();
|
| + itemAtIndex(index) {
|
| + return this._items[index];
|
| + }
|
| +
|
| + /**
|
| + * @param {T} item
|
| + */
|
| + pushItem(item) {
|
| + var totalHeight = this._totalHeight();
|
| + this._items.push(item);
|
| + this._invalidate(totalHeight, this._items.length - 1, this._items.length - 1);
|
| + }
|
| +
|
| + /**
|
| + * @return {T}
|
| + */
|
| + popItem() {
|
| + var totalHeight = this._totalHeight();
|
| + var result = this._items.pop();
|
| + result[this._elementSymbol] = null;
|
| + this._invalidate(totalHeight, this._items.length - 1, this._items.length);
|
| + return result;
|
| }
|
|
|
| /**
|
| * @param {number} index
|
| + * @param {T} item
|
| */
|
| - forceScrollItemToBeLast(index) {
|
| - this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeight();
|
| - this._update();
|
| + insertItemAtIndex(index, item) {
|
| + var totalHeight = this._totalHeight();
|
| + this._items.splice(index, 0, item);
|
| + this._invalidate(totalHeight, index, index);
|
| }
|
|
|
| /**
|
| - * @return {number}
|
| + * @param {number} index
|
| + * @return {T}
|
| */
|
| - _visibleHeight() {
|
| - return this.element.offsetHeight;
|
| + removeItemAtIndex(index) {
|
| + var totalHeight = this._totalHeight();
|
| + var result = this._items[index];
|
| + this._items.splice(index, 1);
|
| + result[this._elementSymbol] = null;
|
| + this._invalidate(totalHeight, index, index + 1);
|
| + return result;
|
| }
|
| -};
|
|
|
| -/**
|
| - * @interface
|
| - */
|
| -UI.ViewportControl.Provider = function() {};
|
| + /**
|
| + * @param {number} from
|
| + * @param {number} to
|
| + * @param {!Array<T>} items
|
| + */
|
| + replaceItemsInRange(from, to, items) {
|
| + var totalHeight = this._totalHeight();
|
| + 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(totalHeight, from, to);
|
| + }
|
| +
|
| + /**
|
| + * @param {!Array<T>} items
|
| + */
|
| + replaceAllItems(items) {
|
| + var totalHeight = this._totalHeight();
|
| + var length = this._items.length;
|
| + for (var i = 0; i < this._items; i++)
|
| + this._items[i][this._elementSymbol] = null;
|
| + this._items = items;
|
| + this._invalidate(totalHeight, 0, length);
|
| + }
|
|
|
| -UI.ViewportControl.Provider.prototype = {
|
| /**
|
| * @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);
|
| + else if (bottom > scrollTop + height)
|
| + this._update(bottom - height);
|
| + }
|
| +
|
| + /**
|
| * @return {number}
|
| */
|
| - fastItemHeight(index) {
|
| - return 0;
|
| - },
|
| + _elementHeight() {
|
| + if (!this._fixedHeight)
|
| + throw 'Only measure in fixed-height scenario';
|
| + if (!this._measuredHeight && this._items.length)
|
| + this._measuredHeight = UI.measurePreferredSize(this._elementAtIndex(0), this.element).height;
|
| + return this._measuredHeight;
|
| + }
|
|
|
| /**
|
| * @return {number}
|
| */
|
| - itemCount() {
|
| - return 0;
|
| - },
|
| + _totalHeight() {
|
| + return this._items.length ? this._items.length * this._elementHeight() : 0;
|
| + }
|
| +
|
| + /**
|
| + * @param {number} offset
|
| + * @return {number}
|
| + */
|
| + _indexAtOffset(offset) {
|
| + if (!this._items.length)
|
| + return 0;
|
| + var index = Math.floor(offset / this._elementHeight());
|
| + if (index >= this._items.length)
|
| + return this._items.length - 1;
|
| + return index;
|
| + }
|
|
|
| /**
|
| * @param {number} index
|
| - * @return {?Element}
|
| + * @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._measuredHeight + 'px';
|
| + }
|
| + item[this._elementSymbol] = element;
|
| + }
|
| + return element;
|
| + }
|
| +
|
| + /**
|
| + * @param {number} index
|
| + * @return {number}
|
| + */
|
| + _offsetAtIndex(index) {
|
| + return this._items.length ? index * this._elementHeight() : 0;
|
| + }
|
| +
|
| + /**
|
| + * @param {number} totalHeight
|
| + * @param {number} from
|
| + * @param {number} to
|
| + */
|
| + _invalidate(totalHeight, from, to) {
|
| + var scrollTop = this.element.scrollTop;
|
| + var availableHeight = this.element.offsetHeight;
|
| + var heightDelta = this._totalHeight() - totalHeight;
|
| + if (totalHeight < availableHeight || this._totalHeight() < availableHeight) {
|
| + this._refresh();
|
| + return;
|
| + }
|
| + if (to <= this._firstIndex) {
|
| + var topHeight = this._topHeight + heightDelta;
|
| + this._topElement.style.height = topHeight + 'px';
|
| + this.element.scrollTop = scrollTop + heightDelta;
|
| + this._topHeight = topHeight;
|
| + return;
|
| + }
|
| + if (from >= this._lastIndex) {
|
| + var bottomHeight = this._bottomHeight + heightDelta;
|
| + this._bottomElement.style.height = bottomHeight + 'px';
|
| + this.element.scrollTop = scrollTop + heightDelta;
|
| + this._bottomHeight = bottomHeight;
|
| + return;
|
| + }
|
| + this._refresh();
|
| + }
|
| +
|
| + _refresh() {
|
| + this._firstIndex = 0;
|
| + this._lastIndex = 0;
|
| + this.element.removeChildren();
|
| + this.element.appendChild(this._topElement);
|
| + this.element.appendChild(this._bottomElement);
|
| + this._update(0);
|
| + }
|
| +
|
| + _onScroll() {
|
| + this._update(this.element.scrollTop);
|
| + }
|
| +
|
| + /**
|
| + * @param {number} scrollTop
|
| */
|
| - itemElement(index) {
|
| - return null;
|
| + _update(scrollTop) {
|
| + var totalHeight = this._totalHeight();
|
| + if (!totalHeight) {
|
| + this._firstIndex = 0;
|
| + this._lastIndex = 0;
|
| + this._topHeight = 0;
|
| + this._bottomHeight = 0;
|
| + this._topElement.style.height = '0px';
|
| + this._bottomElement.style.height = '0px';
|
| + return;
|
| + }
|
| +
|
| + var height = this.element.offsetHeight;
|
| + 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.element.scrollTop = scrollTop;
|
| }
|
| };
|
|
|