Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(585)

Unified Diff: third_party/WebKit/Source/devtools/front_end/ui/ListControl.js

Issue 2592433003: [DevTools] Replace ViewportControl with ListControl. (Closed)
Patch Set: small fix Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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
new file mode 100644
index 0000000000000000000000000000000000000000..0e2d855dcd97e227d04f39e555006635f172ab31
--- /dev/null
+++ b/third_party/WebKit/Source/devtools/front_end/ui/ListControl.js
@@ -0,0 +1,492 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @template T
+ * @interface
+ */
+UI.ListDelegate = function() {};
+
+UI.ListDelegate.prototype = {
+ /**
+ * @return {?number}
+ */
+ fixedHeight() {},
+
+ /**
+ * @param {T} item
+ * @return {!Element}
+ */
+ createElementForItem(item) {},
+
+ /**
+ * @param {T} item
+ * @return {number}
+ */
+ heightForItem(item) {},
+
+ /**
+ * @param {T} item
+ * @return {boolean}
+ */
+ isItemSelectable(item) {},
+
+ /**
+ * @param {?T} from
+ * @param {?T} to
+ * @param {?Element} fromElement
+ * @param {?Element} toElement
+ */
+ selectedItemChanged(from, to, fromElement, toElement) {},
+};
+
+/**
+ * @unrestricted
+ * @template T
+ */
+UI.ListControl = class {
+ /**
+ * @param {!UI.ListDelegate<T>} delegate
+ * @param {boolean} handleInput
+ */
+ constructor(delegate, handleInput) {
+ this.element = createElement('div');
+ this.element.style.overflow = 'auto';
+ this._topElement = this.element.createChild('div');
+ this._bottomElement = this.element.createChild('div');
+ this._firstIndex = 0;
+ this._lastIndex = 0;
+ this._renderedHeight = 0;
+
+ /** @type {!Array<T>} */
+ this._items = [];
+ /** @type {!Map<T, !Element>} */
+ this._itemToElement = new Map();
+ /** @type {!WeakMap<!Element, T>} */
+ this._elementToItem = new WeakMap();
+ this._selectedIndex = -1;
+
+ this._boundKeyDown = (event) => {
alph 2016/12/27 23:38:33 drop ()
dgozman 2016/12/28 06:15:21 Done.
+ if (this.onKeyDown(event))
+ event.consume(true);
+ };
+ this._boundClick = (event) => {
alph 2016/12/27 23:38:33 ditto
dgozman 2016/12/28 06:15:20 Done.
+ if (this.onClick(event))
+ event.consume(true);
+ };
+ this._setUp(delegate, handleInput);
+
+ this.element.addEventListener('scroll', this._onScroll.bind(this), false);
+ this._update(0, this.element.offsetHeight);
+ }
+
+ /**
+ * @return {number}
+ */
+ length() {
+ return this._items.length;
+ }
+
+ /**
+ * @param {number} index
+ * @return {T}
+ */
+ itemAtIndex(index) {
+ return this._items[index];
+ }
+
+ /**
+ * @param {T} item
+ */
+ pushItem(item) {
+ this.replaceItemsInRange(this._items.length, this._items.length, [item]);
+ }
+
+ /**
+ * @return {T}
+ */
+ popItem() {
alph 2016/12/27 23:38:33 return this.removeItemAtIndex(...)
dgozman 2016/12/28 06:15:21 Done.
+ var result = this._items[this._items.length - 1];
alph 2016/12/27 23:38:32 peekLast
+ this.replaceItemsInRange(this._items.length - 1, this._items.length, []);
+ return result;
+ }
+
+ /**
+ * @param {number} index
+ * @param {T} item
+ */
+ insertItemAtIndex(index, item) {
+ this.replaceItemsInRange(index, index, [item]);
+ }
+
+ /**
+ * @param {number} index
+ * @return {T}
+ */
+ removeItemAtIndex(index) {
+ var result = this._items[index];
+ this.replaceItemsInRange(index, index + 1, []);
+ return result;
+ }
+
+ /**
+ * @param {number} from
+ * @param {number} to
+ * @param {!Array<T>} items
+ */
+ replaceItemsInRange(from, to, items) {
+ var oldSelectedItem = this._selectedIndex !== -1 ? this._items[this._selectedIndex] : null;
+ var oldSelectedElement = oldSelectedItem ? (this._itemToElement.get(oldSelectedItem) || null) : null;
+
+ for (var i = from; i < to; i++)
+ this._itemToElement.delete(this._items[i]);
+ this._items.splice.bind(this._items, from, to - from).apply(null, items);
alph 2016/12/27 23:38:32 apply with too many items may fail.
dgozman 2016/12/28 06:15:21 Done.
+ this._invalidate(from, to);
+
+ if (this._selectedIndex >= to)
+ this._selectedIndex += items.length - (to - from);
+ else if (this._selectedIndex >= from)
+ this._select(this._selectClosest(from - 1, -1, false), oldSelectedItem, oldSelectedElement);
alph 2016/12/27 23:38:33 s/-1/+1/ Also put selection on n-1 if the last one
dgozman 2016/12/28 06:15:21 Done.
+ }
+
+ /**
+ * @param {!Array<T>} items
+ */
+ replaceAllItems(items) {
+ this.replaceItemsInRange(0, this._items.length, items);
+ }
+
+ /**
+ * @param {number} from
+ * @param {number} to
+ */
+ invalidateRange(from, to) {
+ this._invalidate(from, to);
alph 2016/12/27 23:38:32 you can merge these two.
dgozman 2016/12/28 06:15:20 Acknowledged.
+ }
+
+ viewportResized() {
+ this._refresh();
+ }
+
+ /**
+ * @param {!UI.ListDelegate<T>} delegate
+ * @param {boolean} handleInput
+ */
+ reset(delegate, handleInput) {
+ this._selectedIndex = -1;
+ this._items = [];
+ this._itemToElement.clear();
+ this._elementToItem = new WeakMap();
+ this._setUp(delegate, handleInput);
+ this._refresh();
+ }
+
+ /**
+ * @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
+ * @param {boolean} scrollIntoView
alph 2016/12/27 23:38:32 boolean=
dgozman 2016/12/28 06:15:21 Done.
+ */
+ selectItemAtIndex(index, scrollIntoView) {
+ if (index !== -1 && !this._delegate.isItemSelectable(this._items[index]))
+ throw 'Attempt to select non-selectable item';
+ this._select(index);
+ if (index !== -1 && scrollIntoView)
+ this.scrollItemAtIndexIntoView(index);
+ }
+
+ /**
+ * @return {number}
+ */
+ selectedIndex() {
+ return this._selectedIndex;
+ }
+
+ /**
+ * @return {?T}
+ */
+ selectedItem() {
+ return this._selectedIndex === -1 ? null : this._items[this._selectedIndex];
+ }
+
+ /**
+ * @param {!Event} event
+ * @return {boolean}
+ */
+ onKeyDown(event) {
+ var index = -1;
+ switch (event.key) {
+ case 'ArrowUp':
+ index = this._selectedIndex === -1 ? this._items.length - 1 : this._selectedIndex - 1;
+ index = this._selectClosest(index, -1, true);
+ break;
+ case 'ArrowDown':
+ index = this._selectedIndex === -1 ? 0 : this._selectedIndex + 1;
+ index = this._selectClosest(index, 1, true);
+ break;
+ case 'PageUp':
+ index = this._selectedIndex === -1 ?
+ this._firstIndex :
alph 2016/12/27 23:38:33 Use first index in viewport
dgozman 2016/12/28 06:15:21 Done.
+ this._indexAtOffset(this._offsetAtIndex(this._selectedIndex) - this.element.offsetHeight);
+ index = this._selectClosest(index, 1, false);
alph 2016/12/27 23:38:33 -1
dgozman 2016/12/28 06:15:20 Done.
+ break;
+ case 'PageDown':
+ index = this._selectedIndex === -1 ?
+ this._lastIndex :
+ this._indexAtOffset(this._offsetAtIndex(this._selectedIndex) + this.element.offsetHeight);
+ index = this._selectClosest(index, 1, false);
+ break;
+ default:
+ return false;
+ }
+ if (index !== -1) {
+ this.scrollItemAtIndexIntoView(index);
+ this._select(index);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @param {!Event} event
+ * @return {boolean}
+ */
+ onClick(event) {
+ var node = event.target;
+ while (node && node.parentNode !== this.element)
alph 2016/12/27 23:38:32 handle shadow DOM
dgozman 2016/12/28 06:15:21 Done.
+ node = node.parentNode;
+ if (!node)
+ return false;
+ var item = this._elementToItem.get(node);
+ if (!item)
+ return false;
+ this._select(item);
alph 2016/12/27 23:38:32 something's wrong here
dgozman 2016/12/28 06:15:21 Done.
+ return true;
+ }
+
+ /**
+ * @return {number}
+ */
+ _totalHeight() {
+ return this._items.length * this._fixedHeight;
+ }
+
+ /**
+ * @param {number} offset
+ * @return {number}
+ */
+ _indexAtOffset(offset) {
+ if (!this._items.length)
+ return 0;
+ if (offset < 0)
+ 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 = this._itemToElement.get(item);
+ if (!element) {
+ element = this._delegate.createElementForItem(item);
+ this._itemToElement.set(item, element);
+ this._elementToItem.set(element, item);
+ }
+ return element;
+ }
+
+ /**
+ * @param {number} index
+ * @return {number}
+ */
+ _offsetAtIndex(index) {
+ return index * this._fixedHeight;
+ }
+
+ /**
+ * @param {number} index
+ * @param {?T=} oldItem
+ * @param {?Element=} oldElement
+ */
+ _select(index, oldItem, oldElement) {
+ if (oldItem === undefined)
+ oldItem = this._selectedIndex !== -1 ? this._items[this._selectedIndex] : null;
+ if (oldElement === undefined)
+ oldElement = oldItem ? (this._itemToElement.get(oldItem) || null) : null;
alph 2016/12/27 23:38:32 you can drop ?:
dgozman 2016/12/28 06:15:21 Done.
+ this._selectedIndex = index;
+ var newItem = this._selectedIndex !== -1 ? this._items[this._selectedIndex] : null;
+ var newElement = newItem ? (this._itemToElement.get(newItem) || null) : null;
alph 2016/12/27 23:38:33 ditto
dgozman 2016/12/28 06:15:21 Done.
+ this._delegate.selectedItemChanged(oldItem, newItem, oldElement, newElement);
+ }
+
+ /**
+ * @param {number} index
+ * @param {number} direction
+ * @param {boolean} canWrap
+ * @return {number}
+ */
+ _selectClosest(index, direction, canWrap) {
+ var length = this._items.length;
+ if (!length)
+ return -1;
+ var start = -1;
+ while (true) {
+ if (index < 0 || index >= length) {
+ if (!canWrap)
+ return -1;
alph 2016/12/27 23:38:32 When reaching top, it should return first selectab
dgozman 2016/12/28 06:15:21 Done.
+ index = (index + length) % length;
+ }
+
+ // Handle full wrap-around.
+ if (index === start)
+ return -1;
+ if (start === -1)
+ start = index;
+
+ if (this._delegate.isItemSelectable(this._items[index]))
+ return index;
+ index += direction;
+ }
+ }
+
+ /**
+ * @param {!UI.ListDelegate<T>} delegate
+ * @param {boolean} handleInput
+ */
+ _setUp(delegate, handleInput) {
+ this._delegate = delegate;
+ if (handleInput) {
+ this.element.addEventListener('keydown', this._boundKeyDown, false);
+ this.element.addEventListener('click', this._boundClick, false);
+ } else {
+ this.element.removeEventListener('keydown', this._boundKeyDown, false);
+ this.element.removeEventListener('click', this._boundClick, false);
+ }
+ this._fixedHeight = this._delegate.fixedHeight();
+ if (!this._fixedHeight)
+ throw 'Variable height is not supported';
+ }
+
+ /**
+ * @param {number} from
+ * @param {number} to
+ */
+ _invalidate(from, to) {
+ var availableHeight = this.element.offsetHeight;
alph 2016/12/27 23:38:33 viewportHeight
dgozman 2016/12/28 06:15:21 Done.
+ 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;
alph 2016/12/27 23:38:32 update _firstIndex
dgozman 2016/12/28 06:15:21 Done.
+ return;
+ }
+
+ if (from >= this._lastIndex) {
+ var bottomHeight = this._bottomHeight + heightDelta;
+ this._bottomElement.style.height = bottomHeight + 'px';
+ this.element.scrollTop = scrollTop + heightDelta;
alph 2016/12/27 23:38:33 drop this
dgozman 2016/12/28 06:15:21 Done.
+ this._bottomHeight = bottomHeight;
+ this._renderedHeight = totalHeight;
+ return;
+ }
+
+ this._refresh();
+ }
+
+ _refresh() {
+ var height = this.element.offsetHeight;
alph 2016/12/27 23:38:33 viewportHeight
dgozman 2016/12/28 06:15:20 Done.
+ var scrollTop = Math.max(0, Math.min(this.element.scrollTop, this._totalHeight() - height));
alph 2016/12/27 23:38:32 If items are added before visible ones, I'd expect
dgozman 2016/12/28 06:15:21 Added TODO.
+ 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';
alph 2016/12/27 23:38:33 '0'
dgozman 2016/12/28 06:15:21 Done.
+ 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++) {
alph 2016/12/27 23:38:32 while (this._firstIndex < Math.min(firstIndex, thi
dgozman 2016/12/28 06:15:21 Done.
+ var element = this._elementAtIndex(index);
+ element.remove();
+ this._firstIndex++;
+ }
+ for (var index = this._lastIndex - 1; index >= lastIndex; index--) {
alph 2016/12/27 23:38:33 ditto
dgozman 2016/12/28 06:15:20 Done.
+ 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));
alph 2016/12/27 23:38:33 drop ()
dgozman 2016/12/28 06:15:20 Done.
+ this._bottomElement.style.height = this._bottomHeight + 'px';
+ this._renderedHeight = totalHeight;
+ this.element.scrollTop = scrollTop;
+ }
+};

Powered by Google App Engine
This is Rietveld 408576698