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 f8586e755e4db9f881812696ee9da2e67cc7e491..57bb46df5c027133149af537450d626402f50da9 100644 |
--- a/third_party/WebKit/Source/devtools/front_end/ui/ViewportControl.js |
+++ b/third_party/WebKit/Source/devtools/front_end/ui/ViewportControl.js |
@@ -27,32 +27,33 @@ |
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
*/ |
- |
/** |
- * @constructor |
- * @param {!WebInspector.ViewportControl.Provider} provider |
+ * @unrestricted |
*/ |
-WebInspector.ViewportControl = function(provider) |
-{ |
- this.element = createElement("div"); |
- this.element.style.overflow = "auto"; |
- this._topGapElement = this.element.createChild("div"); |
- this._topGapElement.style.height = "0px"; |
- this._topGapElement.style.color = "transparent"; |
- this._contentElement = this.element.createChild("div"); |
- this._bottomGapElement = this.element.createChild("div"); |
- this._bottomGapElement.style.height = "0px"; |
- this._bottomGapElement.style.color = "transparent"; |
+WebInspector.ViewportControl = class { |
+ /** |
+ * @param {!WebInspector.ViewportControl.Provider} provider |
+ */ |
+ constructor(provider) { |
+ this.element = createElement('div'); |
+ this.element.style.overflow = 'auto'; |
+ this._topGapElement = this.element.createChild('div'); |
+ this._topGapElement.style.height = '0px'; |
+ this._topGapElement.style.color = 'transparent'; |
+ this._contentElement = this.element.createChild('div'); |
+ this._bottomGapElement = this.element.createChild('div'); |
+ this._bottomGapElement.style.height = '0px'; |
+ this._bottomGapElement.style.color = 'transparent'; |
// Text content needed for range intersection checks in _updateSelectionModel. |
// Use Unicode ZERO WIDTH NO-BREAK SPACE, which avoids contributing any height to the element's layout overflow. |
- this._topGapElement.textContent = "\uFEFF"; |
- this._bottomGapElement.textContent = "\uFEFF"; |
+ this._topGapElement.textContent = '\uFEFF'; |
+ this._bottomGapElement.textContent = '\uFEFF'; |
this._provider = provider; |
- this.element.addEventListener("scroll", this._onScroll.bind(this), false); |
- this.element.addEventListener("copy", this._onCopy.bind(this), false); |
- this.element.addEventListener("dragstart", this._onDragStart.bind(this), false); |
+ this.element.addEventListener('scroll', this._onScroll.bind(this), false); |
+ this.element.addEventListener('copy', this._onCopy.bind(this), false); |
+ this.element.addEventListener('dragstart', this._onDragStart.bind(this), false); |
this._firstActiveIndex = 0; |
this._lastActiveIndex = -1; |
@@ -65,605 +66,592 @@ WebInspector.ViewportControl = function(provider) |
// that items updated asynchronously will not break stick-to-bottom behavior |
// if they change the scroll height. |
this._observer = new MutationObserver(this.refresh.bind(this)); |
- this._observerConfig = { childList: true, subtree: true }; |
-}; |
- |
-/** |
- * @interface |
- */ |
-WebInspector.ViewportControl.Provider = function() |
-{ |
-}; |
- |
-WebInspector.ViewportControl.Provider.prototype = { |
- /** |
- * @param {number} index |
- * @return {number} |
- */ |
- fastHeight: function(index) { return 0; }, |
- |
- /** |
- * @return {number} |
- */ |
- itemCount: function() { return 0; }, |
- |
- /** |
- * @return {number} |
- */ |
- minimumRowHeight: function() { return 0; }, |
- |
- /** |
- * @param {number} index |
- * @return {?WebInspector.ViewportElement} |
- */ |
- itemElement: function(index) { return null; } |
-}; |
- |
-/** |
- * @interface |
- */ |
-WebInspector.ViewportElement = function() { }; |
-WebInspector.ViewportElement.prototype = { |
- willHide: function() { }, |
- |
- wasShown: function() { }, |
- |
- /** |
- * @return {!Element} |
- */ |
- element: function() { }, |
-}; |
- |
-/** |
- * @constructor |
- * @implements {WebInspector.ViewportElement} |
- * @param {!Element} element |
- */ |
-WebInspector.StaticViewportElement = function(element) |
-{ |
- this._element = element; |
-}; |
- |
-WebInspector.StaticViewportElement.prototype = { |
- /** |
- * @override |
- */ |
- willHide: function() { }, |
+ this._observerConfig = {childList: true, subtree: true}; |
+ } |
+ |
+ /** |
+ * @return {boolean} |
+ */ |
+ stickToBottom() { |
+ return this._stickToBottom; |
+ } |
+ |
+ /** |
+ * @param {boolean} value |
+ */ |
+ setStickToBottom(value) { |
+ this._stickToBottom = value; |
+ if (this._stickToBottom) |
+ this._observer.observe(this._contentElement, this._observerConfig); |
+ else |
+ this._observer.disconnect(); |
+ } |
+ |
+ /** |
+ * @param {!Event} event |
+ */ |
+ _onCopy(event) { |
+ var text = this._selectedText(); |
+ if (!text) |
+ return; |
+ event.preventDefault(); |
+ event.clipboardData.setData('text/plain', text); |
+ } |
+ |
+ /** |
+ * @param {!Event} event |
+ */ |
+ _onDragStart(event) { |
+ var text = this._selectedText(); |
+ if (!text) |
+ return false; |
+ event.dataTransfer.clearData(); |
+ event.dataTransfer.setData('text/plain', text); |
+ event.dataTransfer.effectAllowed = 'copy'; |
+ return true; |
+ } |
+ |
+ /** |
+ * @return {!Element} |
+ */ |
+ contentElement() { |
+ return this._contentElement; |
+ } |
+ |
+ invalidate() { |
+ delete this._cumulativeHeights; |
+ delete this._cachedProviderElements; |
+ this._itemCount = this._provider.itemCount(); |
+ this.refresh(); |
+ } |
+ |
+ /** |
+ * @param {number} index |
+ * @return {?WebInspector.ViewportElement} |
+ */ |
+ _providerElement(index) { |
+ if (!this._cachedProviderElements) |
+ this._cachedProviderElements = new Array(this._itemCount); |
+ var element = this._cachedProviderElements[index]; |
+ if (!element) { |
+ element = this._provider.itemElement(index); |
+ this._cachedProviderElements[index] = element; |
+ } |
+ return element; |
+ } |
+ |
+ _rebuildCumulativeHeightsIfNeeded() { |
+ if (this._cumulativeHeights) |
+ return; |
+ if (!this._itemCount) |
+ return; |
+ var firstActiveIndex = this._firstActiveIndex; |
+ var lastActiveIndex = this._lastActiveIndex; |
+ var height = 0; |
+ this._cumulativeHeights = new Int32Array(this._itemCount); |
+ for (var i = 0; i < this._itemCount; ++i) { |
+ if (firstActiveIndex <= i && i <= lastActiveIndex) |
+ height += this._renderedItems[i - firstActiveIndex].element().offsetHeight; |
+ else |
+ height += this._provider.fastHeight(i); |
+ this._cumulativeHeights[i] = height; |
+ } |
+ } |
+ |
+ /** |
+ * @param {number} index |
+ * @return {number} |
+ */ |
+ _cachedItemHeight(index) { |
+ return index === 0 ? this._cumulativeHeights[0] : |
+ this._cumulativeHeights[index] - this._cumulativeHeights[index - 1]; |
+ } |
+ |
+ /** |
+ * @param {?Selection} selection |
+ * @suppressGlobalPropertiesCheck |
+ */ |
+ _isSelectionBackwards(selection) { |
+ if (!selection || !selection.rangeCount) |
+ return false; |
+ var range = document.createRange(); |
+ range.setStart(selection.anchorNode, selection.anchorOffset); |
+ range.setEnd(selection.focusNode, selection.focusOffset); |
+ return range.collapsed; |
+ } |
+ |
+ /** |
+ * @param {number} itemIndex |
+ * @param {!Node} node |
+ * @param {number} offset |
+ * @return {!{item: number, node: !Node, offset: number}} |
+ */ |
+ _createSelectionModel(itemIndex, node, offset) { |
+ return {item: itemIndex, node: node, offset: offset}; |
+ } |
+ |
+ /** |
+ * @param {?Selection} selection |
+ */ |
+ _updateSelectionModel(selection) { |
+ var range = selection && selection.rangeCount ? selection.getRangeAt(0) : null; |
+ if (!range || selection.isCollapsed || !this.element.hasSelection()) { |
+ this._headSelection = null; |
+ this._anchorSelection = null; |
+ return false; |
+ } |
- /** |
- * @override |
- */ |
- wasShown: function() { }, |
+ var firstSelected = Number.MAX_VALUE; |
+ var lastSelected = -1; |
+ |
+ var hasVisibleSelection = false; |
+ for (var i = 0; i < this._renderedItems.length; ++i) { |
+ if (range.intersectsNode(this._renderedItems[i].element())) { |
+ var index = i + this._firstActiveIndex; |
+ firstSelected = Math.min(firstSelected, index); |
+ lastSelected = Math.max(lastSelected, index); |
+ hasVisibleSelection = true; |
+ } |
+ } |
+ if (hasVisibleSelection) { |
+ firstSelected = |
+ this._createSelectionModel(firstSelected, /** @type {!Node} */ (range.startContainer), range.startOffset); |
+ lastSelected = |
+ this._createSelectionModel(lastSelected, /** @type {!Node} */ (range.endContainer), range.endOffset); |
+ } |
+ var topOverlap = range.intersectsNode(this._topGapElement) && this._topGapElement._active; |
+ var bottomOverlap = range.intersectsNode(this._bottomGapElement) && this._bottomGapElement._active; |
+ if (!topOverlap && !bottomOverlap && !hasVisibleSelection) { |
+ this._headSelection = null; |
+ this._anchorSelection = null; |
+ return false; |
+ } |
- /** |
- * @override |
- * @return {!Element} |
- */ |
- element: function() |
- { |
- return this._element; |
- }, |
-}; |
+ if (!this._anchorSelection || !this._headSelection) { |
+ this._anchorSelection = this._createSelectionModel(0, this.element, 0); |
+ this._headSelection = this._createSelectionModel(this._itemCount - 1, this.element, this.element.children.length); |
+ this._selectionIsBackward = false; |
+ } |
-WebInspector.ViewportControl.prototype = { |
- /** |
- * @return {boolean} |
- */ |
- stickToBottom: function() |
- { |
- return this._stickToBottom; |
- }, |
+ var isBackward = this._isSelectionBackwards(selection); |
+ var startSelection = this._selectionIsBackward ? this._headSelection : this._anchorSelection; |
+ var endSelection = this._selectionIsBackward ? this._anchorSelection : this._headSelection; |
+ if (topOverlap && bottomOverlap && hasVisibleSelection) { |
+ firstSelected = firstSelected.item < startSelection.item ? firstSelected : startSelection; |
+ lastSelected = lastSelected.item > endSelection.item ? lastSelected : endSelection; |
+ } else if (!hasVisibleSelection) { |
+ firstSelected = startSelection; |
+ lastSelected = endSelection; |
+ } else if (topOverlap) |
+ firstSelected = isBackward ? this._headSelection : this._anchorSelection; |
+ else if (bottomOverlap) |
+ lastSelected = isBackward ? this._anchorSelection : this._headSelection; |
+ |
+ if (isBackward) { |
+ this._anchorSelection = lastSelected; |
+ this._headSelection = firstSelected; |
+ } else { |
+ this._anchorSelection = firstSelected; |
+ this._headSelection = lastSelected; |
+ } |
+ this._selectionIsBackward = isBackward; |
+ return true; |
+ } |
+ |
+ /** |
+ * @param {?Selection} selection |
+ */ |
+ _restoreSelection(selection) { |
+ var anchorElement = null; |
+ var anchorOffset; |
+ if (this._firstActiveIndex <= this._anchorSelection.item && this._anchorSelection.item <= this._lastActiveIndex) { |
+ anchorElement = this._anchorSelection.node; |
+ anchorOffset = this._anchorSelection.offset; |
+ } else { |
+ if (this._anchorSelection.item < this._firstActiveIndex) |
+ anchorElement = this._topGapElement; |
+ else if (this._anchorSelection.item > this._lastActiveIndex) |
+ anchorElement = this._bottomGapElement; |
+ anchorOffset = this._selectionIsBackward ? 1 : 0; |
+ } |
- /** |
- * @param {boolean} value |
- */ |
- setStickToBottom: function(value) |
- { |
- this._stickToBottom = value; |
- if (this._stickToBottom) |
- this._observer.observe(this._contentElement, this._observerConfig); |
- else |
- this._observer.disconnect(); |
- }, |
+ var headElement = null; |
+ var headOffset; |
+ if (this._firstActiveIndex <= this._headSelection.item && this._headSelection.item <= this._lastActiveIndex) { |
+ headElement = this._headSelection.node; |
+ headOffset = this._headSelection.offset; |
+ } else { |
+ if (this._headSelection.item < this._firstActiveIndex) |
+ headElement = this._topGapElement; |
+ else if (this._headSelection.item > this._lastActiveIndex) |
+ headElement = this._bottomGapElement; |
+ headOffset = this._selectionIsBackward ? 0 : 1; |
+ } |
- /** |
- * @param {!Event} event |
- */ |
- _onCopy: function(event) |
- { |
- var text = this._selectedText(); |
- if (!text) |
- return; |
- event.preventDefault(); |
- event.clipboardData.setData("text/plain", text); |
- }, |
+ selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, headOffset); |
+ } |
+ |
+ refresh() { |
+ this._observer.disconnect(); |
+ this._innerRefresh(); |
+ if (this._stickToBottom) |
+ this._observer.observe(this._contentElement, this._observerConfig); |
+ } |
+ |
+ _innerRefresh() { |
+ if (!this._visibleHeight()) |
+ return; // Do nothing for invisible controls. |
+ |
+ if (!this._itemCount) { |
+ for (var i = 0; i < this._renderedItems.length; ++i) |
+ this._renderedItems[i].willHide(); |
+ this._renderedItems = []; |
+ this._contentElement.removeChildren(); |
+ this._topGapElement.style.height = '0px'; |
+ this._bottomGapElement.style.height = '0px'; |
+ this._firstActiveIndex = -1; |
+ this._lastActiveIndex = -1; |
+ return; |
+ } |
- /** |
- * @param {!Event} event |
- */ |
- _onDragStart: function(event) |
- { |
- var text = this._selectedText(); |
- if (!text) |
- return false; |
- event.dataTransfer.clearData(); |
- event.dataTransfer.setData("text/plain", text); |
- event.dataTransfer.effectAllowed = "copy"; |
- return true; |
- }, |
+ var selection = this.element.getComponentSelection(); |
+ var shouldRestoreSelection = this._updateSelectionModel(selection); |
- /** |
- * @return {!Element} |
- */ |
- contentElement: function() |
- { |
- return this._contentElement; |
- }, |
+ var visibleFrom = this.element.scrollTop; |
+ var visibleHeight = this._visibleHeight(); |
+ var isInvalidating = !this._cumulativeHeights; |
- invalidate: function() |
- { |
+ for (var i = 0; i < this._renderedItems.length; ++i) { |
+ // Tolerate 1-pixel error due to double-to-integer rounding errors. |
+ if (this._cumulativeHeights && |
+ Math.abs(this._cachedItemHeight(this._firstActiveIndex + i) - this._renderedItems[i].element().offsetHeight) > |
+ 1) |
delete this._cumulativeHeights; |
- delete this._cachedProviderElements; |
- this._itemCount = this._provider.itemCount(); |
- this.refresh(); |
- }, |
- |
- /** |
- * @param {number} index |
- * @return {?WebInspector.ViewportElement} |
- */ |
- _providerElement: function(index) |
- { |
- if (!this._cachedProviderElements) |
- this._cachedProviderElements = new Array(this._itemCount); |
- var element = this._cachedProviderElements[index]; |
- if (!element) { |
- element = this._provider.itemElement(index); |
- this._cachedProviderElements[index] = element; |
- } |
- return element; |
- }, |
- |
- _rebuildCumulativeHeightsIfNeeded: function() |
- { |
- if (this._cumulativeHeights) |
- return; |
- if (!this._itemCount) |
- return; |
- var firstActiveIndex = this._firstActiveIndex; |
- var lastActiveIndex = this._lastActiveIndex; |
- var height = 0; |
- this._cumulativeHeights = new Int32Array(this._itemCount); |
- for (var i = 0; i < this._itemCount; ++i) { |
- if (firstActiveIndex <= i && i <= lastActiveIndex) |
- height += this._renderedItems[i - firstActiveIndex].element().offsetHeight; |
- else |
- height += this._provider.fastHeight(i); |
- this._cumulativeHeights[i] = height; |
- } |
- }, |
- |
- /** |
- * @param {number} index |
- * @return {number} |
- */ |
- _cachedItemHeight: function(index) |
- { |
- return index === 0 ? this._cumulativeHeights[0] : this._cumulativeHeights[index] - this._cumulativeHeights[index - 1]; |
- }, |
- |
- /** |
- * @param {?Selection} selection |
- * @suppressGlobalPropertiesCheck |
- */ |
- _isSelectionBackwards: function(selection) |
- { |
- if (!selection || !selection.rangeCount) |
- return false; |
- var range = document.createRange(); |
- range.setStart(selection.anchorNode, selection.anchorOffset); |
- range.setEnd(selection.focusNode, selection.focusOffset); |
- return range.collapsed; |
- }, |
- |
- /** |
- * @param {number} itemIndex |
- * @param {!Node} node |
- * @param {number} offset |
- * @return {!{item: number, node: !Node, offset: number}} |
- */ |
- _createSelectionModel: function(itemIndex, node, offset) |
- { |
- return { |
- item: itemIndex, |
- node: node, |
- offset: offset |
- }; |
- }, |
+ } |
+ this._rebuildCumulativeHeightsIfNeeded(); |
+ var oldFirstActiveIndex = this._firstActiveIndex; |
+ var oldLastActiveIndex = this._lastActiveIndex; |
+ var activeHeight = visibleHeight * 2; |
+ // When the viewport is scrolled to the bottom, using the cumulative heights estimate is not |
+ // precise enough to determine next visible indices. This stickToBottom check avoids extra |
+ // calls to refresh in those cases. |
+ if (this._stickToBottom) { |
+ this._firstActiveIndex = |
+ Math.max(this._itemCount - Math.ceil(activeHeight / this._provider.minimumRowHeight()), 0); |
+ this._lastActiveIndex = this._itemCount - 1; |
+ } else { |
+ this._firstActiveIndex = Math.max( |
+ Array.prototype.lowerBound.call( |
+ this._cumulativeHeights, visibleFrom + 1 - (activeHeight - visibleHeight) / 2), |
+ 0); |
+ // Proactively render more rows in case some of them will be collapsed without triggering refresh. @see crbug.com/390169 |
+ this._lastActiveIndex = this._firstActiveIndex + Math.ceil(activeHeight / this._provider.minimumRowHeight()) - 1; |
+ this._lastActiveIndex = Math.min(this._lastActiveIndex, this._itemCount - 1); |
+ } |
- /** |
- * @param {?Selection} selection |
- */ |
- _updateSelectionModel: function(selection) |
- { |
- var range = selection && selection.rangeCount ? selection.getRangeAt(0) : null; |
- if (!range || selection.isCollapsed || !this.element.hasSelection()) { |
- this._headSelection = null; |
- this._anchorSelection = null; |
- return false; |
- } |
- |
- var firstSelected = Number.MAX_VALUE; |
- var lastSelected = -1; |
- |
- var hasVisibleSelection = false; |
- for (var i = 0; i < this._renderedItems.length; ++i) { |
- if (range.intersectsNode(this._renderedItems[i].element())) { |
- var index = i + this._firstActiveIndex; |
- firstSelected = Math.min(firstSelected, index); |
- lastSelected = Math.max(lastSelected, index); |
- hasVisibleSelection = true; |
- } |
- } |
- if (hasVisibleSelection) { |
- firstSelected = this._createSelectionModel(firstSelected, /** @type {!Node} */(range.startContainer), range.startOffset); |
- lastSelected = this._createSelectionModel(lastSelected, /** @type {!Node} */(range.endContainer), range.endOffset); |
- } |
- var topOverlap = range.intersectsNode(this._topGapElement) && this._topGapElement._active; |
- var bottomOverlap = range.intersectsNode(this._bottomGapElement) && this._bottomGapElement._active; |
- if (!topOverlap && !bottomOverlap && !hasVisibleSelection) { |
- this._headSelection = null; |
- this._anchorSelection = null; |
- return false; |
- } |
- |
- if (!this._anchorSelection || !this._headSelection) { |
- this._anchorSelection = this._createSelectionModel(0, this.element, 0); |
- this._headSelection = this._createSelectionModel(this._itemCount - 1, this.element, this.element.children.length); |
- this._selectionIsBackward = false; |
- } |
- |
- var isBackward = this._isSelectionBackwards(selection); |
- var startSelection = this._selectionIsBackward ? this._headSelection : this._anchorSelection; |
- var endSelection = this._selectionIsBackward ? this._anchorSelection : this._headSelection; |
- if (topOverlap && bottomOverlap && hasVisibleSelection) { |
- firstSelected = firstSelected.item < startSelection.item ? firstSelected : startSelection; |
- lastSelected = lastSelected.item > endSelection.item ? lastSelected : endSelection; |
- } else if (!hasVisibleSelection) { |
- firstSelected = startSelection; |
- lastSelected = endSelection; |
- } else if (topOverlap) |
- firstSelected = isBackward ? this._headSelection : this._anchorSelection; |
- else if (bottomOverlap) |
- lastSelected = isBackward ? this._anchorSelection : this._headSelection; |
- |
- if (isBackward) { |
- this._anchorSelection = lastSelected; |
- this._headSelection = firstSelected; |
- } else { |
- this._anchorSelection = firstSelected; |
- this._headSelection = lastSelected; |
- } |
- this._selectionIsBackward = isBackward; |
- return true; |
- }, |
+ var topGapHeight = this._cumulativeHeights[this._firstActiveIndex - 1] || 0; |
+ var bottomGapHeight = |
+ this._cumulativeHeights[this._cumulativeHeights.length - 1] - this._cumulativeHeights[this._lastActiveIndex]; |
/** |
- * @param {?Selection} selection |
+ * @this {WebInspector.ViewportControl} |
*/ |
- _restoreSelection: function(selection) |
- { |
- var anchorElement = null; |
- var anchorOffset; |
- if (this._firstActiveIndex <= this._anchorSelection.item && this._anchorSelection.item <= this._lastActiveIndex) { |
- anchorElement = this._anchorSelection.node; |
- anchorOffset = this._anchorSelection.offset; |
- } else { |
- if (this._anchorSelection.item < this._firstActiveIndex) |
- anchorElement = this._topGapElement; |
- else if (this._anchorSelection.item > this._lastActiveIndex) |
- anchorElement = this._bottomGapElement; |
- anchorOffset = this._selectionIsBackward ? 1 : 0; |
- } |
- |
- var headElement = null; |
- var headOffset; |
- if (this._firstActiveIndex <= this._headSelection.item && this._headSelection.item <= this._lastActiveIndex) { |
- headElement = this._headSelection.node; |
- headOffset = this._headSelection.offset; |
- } else { |
- if (this._headSelection.item < this._firstActiveIndex) |
- headElement = this._topGapElement; |
- else if (this._headSelection.item > this._lastActiveIndex) |
- headElement = this._bottomGapElement; |
- headOffset = this._selectionIsBackward ? 0 : 1; |
- } |
- |
- selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, headOffset); |
- }, |
- |
- refresh: function() |
- { |
- this._observer.disconnect(); |
- this._innerRefresh(); |
- if (this._stickToBottom) |
- this._observer.observe(this._contentElement, this._observerConfig); |
- }, |
- |
- _innerRefresh: function() |
- { |
- if (!this._visibleHeight()) |
- return; // Do nothing for invisible controls. |
- |
- if (!this._itemCount) { |
- for (var i = 0; i < this._renderedItems.length; ++i) |
- this._renderedItems[i].willHide(); |
- this._renderedItems = []; |
- this._contentElement.removeChildren(); |
- this._topGapElement.style.height = "0px"; |
- this._bottomGapElement.style.height = "0px"; |
- this._firstActiveIndex = -1; |
- this._lastActiveIndex = -1; |
- return; |
- } |
- |
- var selection = this.element.getComponentSelection(); |
- var shouldRestoreSelection = this._updateSelectionModel(selection); |
- |
- var visibleFrom = this.element.scrollTop; |
- var visibleHeight = this._visibleHeight(); |
- var isInvalidating = !this._cumulativeHeights; |
- |
- for (var i = 0; i < this._renderedItems.length; ++i) { |
- // Tolerate 1-pixel error due to double-to-integer rounding errors. |
- if (this._cumulativeHeights && Math.abs(this._cachedItemHeight(this._firstActiveIndex + i) - this._renderedItems[i].element().offsetHeight) > 1) |
- delete this._cumulativeHeights; |
- } |
- this._rebuildCumulativeHeightsIfNeeded(); |
- var oldFirstActiveIndex = this._firstActiveIndex; |
- var oldLastActiveIndex = this._lastActiveIndex; |
- var activeHeight = visibleHeight * 2; |
- // When the viewport is scrolled to the bottom, using the cumulative heights estimate is not |
- // precise enough to determine next visible indices. This stickToBottom check avoids extra |
- // calls to refresh in those cases. |
- if (this._stickToBottom) { |
- this._firstActiveIndex = Math.max(this._itemCount - Math.ceil(activeHeight / this._provider.minimumRowHeight()), 0); |
- this._lastActiveIndex = this._itemCount - 1; |
- } else { |
- this._firstActiveIndex = Math.max(Array.prototype.lowerBound.call(this._cumulativeHeights, visibleFrom + 1 - (activeHeight - visibleHeight) / 2), 0); |
- // Proactively render more rows in case some of them will be collapsed without triggering refresh. @see crbug.com/390169 |
- this._lastActiveIndex = this._firstActiveIndex + Math.ceil(activeHeight / this._provider.minimumRowHeight()) - 1; |
- this._lastActiveIndex = Math.min(this._lastActiveIndex, this._itemCount - 1); |
- } |
- |
- var topGapHeight = this._cumulativeHeights[this._firstActiveIndex - 1] || 0; |
- var bottomGapHeight = this._cumulativeHeights[this._cumulativeHeights.length - 1] - this._cumulativeHeights[this._lastActiveIndex]; |
- |
- /** |
- * @this {WebInspector.ViewportControl} |
- */ |
- function prepare() |
- { |
- this._topGapElement.style.height = topGapHeight + "px"; |
- this._bottomGapElement.style.height = bottomGapHeight + "px"; |
- this._topGapElement._active = !!topGapHeight; |
- this._bottomGapElement._active = !!bottomGapHeight; |
- this._contentElement.style.setProperty("height", "10000000px"); |
- } |
- |
- if (isInvalidating) |
- this._fullViewportUpdate(prepare.bind(this)); |
- else |
- this._partialViewportUpdate(oldFirstActiveIndex, oldLastActiveIndex, prepare.bind(this)); |
- this._contentElement.style.removeProperty("height"); |
- // Should be the last call in the method as it might force layout. |
- if (shouldRestoreSelection) |
- this._restoreSelection(selection); |
- if (this._stickToBottom) |
- this.element.scrollTop = 10000000; |
- }, |
+ function prepare() { |
+ this._topGapElement.style.height = topGapHeight + 'px'; |
+ this._bottomGapElement.style.height = bottomGapHeight + 'px'; |
+ this._topGapElement._active = !!topGapHeight; |
+ this._bottomGapElement._active = !!bottomGapHeight; |
+ this._contentElement.style.setProperty('height', '10000000px'); |
+ } |
- /** |
- * @param {function()} prepare |
- */ |
- _fullViewportUpdate: function(prepare) |
- { |
- for (var i = 0; i < this._renderedItems.length; ++i) |
- this._renderedItems[i].willHide(); |
- prepare(); |
- this._renderedItems = []; |
- this._contentElement.removeChildren(); |
- for (var i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) { |
- var viewportElement = this._providerElement(i); |
- this._contentElement.appendChild(viewportElement.element()); |
- this._renderedItems.push(viewportElement); |
- } |
- for (var i = 0; i < this._renderedItems.length; ++i) |
- this._renderedItems[i].wasShown(); |
- }, |
+ if (isInvalidating) |
+ this._fullViewportUpdate(prepare.bind(this)); |
+ else |
+ this._partialViewportUpdate(oldFirstActiveIndex, oldLastActiveIndex, prepare.bind(this)); |
+ this._contentElement.style.removeProperty('height'); |
+ // Should be the last call in the method as it might force layout. |
+ if (shouldRestoreSelection) |
+ this._restoreSelection(selection); |
+ if (this._stickToBottom) |
+ this.element.scrollTop = 10000000; |
+ } |
+ |
+ /** |
+ * @param {function()} prepare |
+ */ |
+ _fullViewportUpdate(prepare) { |
+ for (var i = 0; i < this._renderedItems.length; ++i) |
+ this._renderedItems[i].willHide(); |
+ prepare(); |
+ this._renderedItems = []; |
+ this._contentElement.removeChildren(); |
+ for (var i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) { |
+ var viewportElement = this._providerElement(i); |
+ this._contentElement.appendChild(viewportElement.element()); |
+ this._renderedItems.push(viewportElement); |
+ } |
+ for (var i = 0; i < this._renderedItems.length; ++i) |
+ this._renderedItems[i].wasShown(); |
+ } |
+ |
+ /** |
+ * @param {number} oldFirstActiveIndex |
+ * @param {number} oldLastActiveIndex |
+ * @param {function()} prepare |
+ */ |
+ _partialViewportUpdate(oldFirstActiveIndex, oldLastActiveIndex, prepare) { |
+ var willBeHidden = []; |
+ for (var i = 0; i < this._renderedItems.length; ++i) { |
+ var index = oldFirstActiveIndex + i; |
+ if (index < this._firstActiveIndex || this._lastActiveIndex < index) |
+ willBeHidden.push(this._renderedItems[i]); |
+ } |
+ for (var i = 0; i < willBeHidden.length; ++i) |
+ willBeHidden[i].willHide(); |
+ prepare(); |
+ for (var i = 0; i < willBeHidden.length; ++i) |
+ willBeHidden[i].element().remove(); |
- /** |
- * @param {number} oldFirstActiveIndex |
- * @param {number} oldLastActiveIndex |
- * @param {function()} prepare |
- */ |
- _partialViewportUpdate: function(oldFirstActiveIndex, oldLastActiveIndex, prepare) |
- { |
- var willBeHidden = []; |
- for (var i = 0; i < this._renderedItems.length; ++i) { |
- var index = oldFirstActiveIndex + i; |
- if (index < this._firstActiveIndex || this._lastActiveIndex < index) |
- willBeHidden.push(this._renderedItems[i]); |
- } |
- for (var i = 0; i < willBeHidden.length; ++i) |
- willBeHidden[i].willHide(); |
- prepare(); |
- for (var i = 0; i < willBeHidden.length; ++i) |
- willBeHidden[i].element().remove(); |
- |
- this._renderedItems = []; |
- var anchor = this._contentElement.firstChild; |
- var wasShown = []; |
- for (var i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) { |
- var viewportElement = this._providerElement(i); |
- var element = viewportElement.element(); |
- if (element !== anchor) { |
- this._contentElement.insertBefore(element, anchor); |
- wasShown.push(viewportElement); |
- } else { |
- anchor = anchor.nextSibling; |
- } |
- this._renderedItems.push(viewportElement); |
- } |
- for (var i = 0; i < wasShown.length; ++i) |
- wasShown[i].wasShown(); |
- }, |
+ this._renderedItems = []; |
+ var anchor = this._contentElement.firstChild; |
+ var wasShown = []; |
+ for (var i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) { |
+ var viewportElement = this._providerElement(i); |
+ var element = viewportElement.element(); |
+ if (element !== anchor) { |
+ this._contentElement.insertBefore(element, anchor); |
+ wasShown.push(viewportElement); |
+ } else { |
+ anchor = anchor.nextSibling; |
+ } |
+ this._renderedItems.push(viewportElement); |
+ } |
+ for (var i = 0; i < wasShown.length; ++i) |
+ wasShown[i].wasShown(); |
+ } |
+ |
+ /** |
+ * @return {?string} |
+ */ |
+ _selectedText() { |
+ this._updateSelectionModel(this.element.getComponentSelection()); |
+ if (!this._headSelection || !this._anchorSelection) |
+ return null; |
+ |
+ var startSelection = null; |
+ var endSelection = null; |
+ if (this._selectionIsBackward) { |
+ startSelection = this._headSelection; |
+ endSelection = this._anchorSelection; |
+ } else { |
+ startSelection = this._anchorSelection; |
+ endSelection = this._headSelection; |
+ } |
- /** |
- * @return {?string} |
- */ |
- _selectedText: function() |
- { |
- this._updateSelectionModel(this.element.getComponentSelection()); |
- if (!this._headSelection || !this._anchorSelection) |
- return null; |
- |
- var startSelection = null; |
- var endSelection = null; |
- if (this._selectionIsBackward) { |
- startSelection = this._headSelection; |
- endSelection = this._anchorSelection; |
- } else { |
- startSelection = this._anchorSelection; |
- endSelection = this._headSelection; |
- } |
- |
- var textLines = []; |
- for (var i = startSelection.item; i <= endSelection.item; ++i) |
- textLines.push(this._providerElement(i).element().deepTextContent()); |
- |
- var endSelectionElement = this._providerElement(endSelection.item).element(); |
- if (endSelection.node && endSelection.node.isSelfOrDescendant(endSelectionElement)) { |
- var itemTextOffset = this._textOffsetInNode(endSelectionElement, endSelection.node, endSelection.offset); |
- textLines[textLines.length - 1] = textLines.peekLast().substring(0, itemTextOffset); |
- } |
- |
- var startSelectionElement = this._providerElement(startSelection.item).element(); |
- if (startSelection.node && startSelection.node.isSelfOrDescendant(startSelectionElement)) { |
- var itemTextOffset = this._textOffsetInNode(startSelectionElement, startSelection.node, startSelection.offset); |
- textLines[0] = textLines[0].substring(itemTextOffset); |
- } |
- |
- return textLines.join("\n"); |
- }, |
+ var textLines = []; |
+ for (var i = startSelection.item; i <= endSelection.item; ++i) |
+ textLines.push(this._providerElement(i).element().deepTextContent()); |
- /** |
- * @param {!Element} itemElement |
- * @param {!Node} container |
- * @param {number} offset |
- * @return {number} |
- */ |
- _textOffsetInNode: function(itemElement, container, offset) |
- { |
- var chars = 0; |
- var node = itemElement; |
- while ((node = node.traverseNextTextNode()) && !node.isSelfOrDescendant(container)) |
- chars += node.textContent.length; |
- return chars + offset; |
- }, |
+ var endSelectionElement = this._providerElement(endSelection.item).element(); |
+ if (endSelection.node && endSelection.node.isSelfOrDescendant(endSelectionElement)) { |
+ var itemTextOffset = this._textOffsetInNode(endSelectionElement, endSelection.node, endSelection.offset); |
+ textLines[textLines.length - 1] = textLines.peekLast().substring(0, itemTextOffset); |
+ } |
- /** |
- * @param {!Event} event |
- */ |
- _onScroll: function(event) |
- { |
- this.refresh(); |
- }, |
+ var startSelectionElement = this._providerElement(startSelection.item).element(); |
+ if (startSelection.node && startSelection.node.isSelfOrDescendant(startSelectionElement)) { |
+ var itemTextOffset = this._textOffsetInNode(startSelectionElement, startSelection.node, startSelection.offset); |
+ textLines[0] = textLines[0].substring(itemTextOffset); |
+ } |
- /** |
- * @return {number} |
- */ |
- firstVisibleIndex: function() |
- { |
- var firstVisibleIndex = Math.max(Array.prototype.lowerBound.call(this._cumulativeHeights, this.element.scrollTop + 1), 0); |
- return Math.max(firstVisibleIndex, this._firstActiveIndex); |
- }, |
+ return textLines.join('\n'); |
+ } |
+ |
+ /** |
+ * @param {!Element} itemElement |
+ * @param {!Node} container |
+ * @param {number} offset |
+ * @return {number} |
+ */ |
+ _textOffsetInNode(itemElement, container, offset) { |
+ var chars = 0; |
+ var node = itemElement; |
+ while ((node = node.traverseNextTextNode()) && !node.isSelfOrDescendant(container)) |
+ chars += node.textContent.length; |
+ return chars + offset; |
+ } |
+ |
+ /** |
+ * @param {!Event} event |
+ */ |
+ _onScroll(event) { |
+ this.refresh(); |
+ } |
+ |
+ /** |
+ * @return {number} |
+ */ |
+ firstVisibleIndex() { |
+ var firstVisibleIndex = |
+ Math.max(Array.prototype.lowerBound.call(this._cumulativeHeights, this.element.scrollTop + 1), 0); |
+ return Math.max(firstVisibleIndex, this._firstActiveIndex); |
+ } |
+ |
+ /** |
+ * @return {number} |
+ */ |
+ lastVisibleIndex() { |
+ var lastVisibleIndex; |
+ if (this._stickToBottom) |
+ lastVisibleIndex = this._itemCount - 1; |
+ else |
+ lastVisibleIndex = |
+ this.firstVisibleIndex() + Math.ceil(this._visibleHeight() / this._provider.minimumRowHeight()) - 1; |
+ return Math.min(lastVisibleIndex, this._lastActiveIndex); |
+ } |
+ |
+ /** |
+ * @return {?Element} |
+ */ |
+ renderedElementAt(index) { |
+ if (index < this._firstActiveIndex) |
+ return null; |
+ if (index > this._lastActiveIndex) |
+ return null; |
+ return this._renderedItems[index - this._firstActiveIndex].element(); |
+ } |
+ |
+ /** |
+ * @param {number} index |
+ * @param {boolean=} makeLast |
+ */ |
+ 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); |
+ } |
+ |
+ /** |
+ * @param {number} index |
+ */ |
+ forceScrollItemToBeFirst(index) { |
+ this.setStickToBottom(false); |
+ this._rebuildCumulativeHeightsIfNeeded(); |
+ this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0; |
+ if (this.element.isScrolledToBottom()) |
+ this.setStickToBottom(true); |
+ this.refresh(); |
+ } |
+ |
+ /** |
+ * @param {number} index |
+ */ |
+ forceScrollItemToBeLast(index) { |
+ this.setStickToBottom(false); |
+ this._rebuildCumulativeHeightsIfNeeded(); |
+ this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeight(); |
+ if (this.element.isScrolledToBottom()) |
+ this.setStickToBottom(true); |
+ this.refresh(); |
+ } |
+ |
+ /** |
+ * @return {number} |
+ */ |
+ _visibleHeight() { |
+ // Use offsetHeight instead of clientHeight to avoid being affected by horizontal scroll. |
+ return this.element.offsetHeight; |
+ } |
+}; |
- /** |
- * @return {number} |
- */ |
- lastVisibleIndex: function() |
- { |
- var lastVisibleIndex; |
- if (this._stickToBottom) |
- lastVisibleIndex = this._itemCount - 1; |
- else |
- lastVisibleIndex = this.firstVisibleIndex() + Math.ceil(this._visibleHeight() / this._provider.minimumRowHeight()) - 1; |
- return Math.min(lastVisibleIndex, this._lastActiveIndex); |
- }, |
+/** |
+ * @interface |
+ */ |
+WebInspector.ViewportControl.Provider = function() {}; |
- /** |
- * @return {?Element} |
- */ |
- renderedElementAt: function(index) |
- { |
- if (index < this._firstActiveIndex) |
- return null; |
- if (index > this._lastActiveIndex) |
- return null; |
- return this._renderedItems[index - this._firstActiveIndex].element(); |
- }, |
+WebInspector.ViewportControl.Provider.prototype = { |
+ /** |
+ * @param {number} index |
+ * @return {number} |
+ */ |
+ fastHeight: function(index) { |
+ return 0; |
+ }, |
+ |
+ /** |
+ * @return {number} |
+ */ |
+ itemCount: function() { |
+ return 0; |
+ }, |
+ |
+ /** |
+ * @return {number} |
+ */ |
+ minimumRowHeight: function() { |
+ return 0; |
+ }, |
+ |
+ /** |
+ * @param {number} index |
+ * @return {?WebInspector.ViewportElement} |
+ */ |
+ itemElement: function(index) { |
+ return null; |
+ } |
+}; |
- /** |
- * @param {number} index |
- * @param {boolean=} makeLast |
- */ |
- scrollItemIntoView: function(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); |
- }, |
+/** |
+ * @interface |
+ */ |
+WebInspector.ViewportElement = function() {}; |
+WebInspector.ViewportElement.prototype = { |
+ willHide: function() {}, |
- /** |
- * @param {number} index |
- */ |
- forceScrollItemToBeFirst: function(index) |
- { |
- this.setStickToBottom(false); |
- this._rebuildCumulativeHeightsIfNeeded(); |
- this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0; |
- if (this.element.isScrolledToBottom()) |
- this.setStickToBottom(true); |
- this.refresh(); |
- }, |
+ wasShown: function() {}, |
- /** |
- * @param {number} index |
- */ |
- forceScrollItemToBeLast: function(index) |
- { |
- this.setStickToBottom(false); |
- this._rebuildCumulativeHeightsIfNeeded(); |
- this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeight(); |
- if (this.element.isScrolledToBottom()) |
- this.setStickToBottom(true); |
- this.refresh(); |
- }, |
+ /** |
+ * @return {!Element} |
+ */ |
+ element: function() {}, |
+}; |
- /** |
- * @return {number} |
- */ |
- _visibleHeight: function() |
- { |
- // Use offsetHeight instead of clientHeight to avoid being affected by horizontal scroll. |
- return this.element.offsetHeight; |
- } |
+/** |
+ * @implements {WebInspector.ViewportElement} |
+ * @unrestricted |
+ */ |
+WebInspector.StaticViewportElement = class { |
+ /** |
+ * @param {!Element} element |
+ */ |
+ constructor(element) { |
+ this._element = element; |
+ } |
+ |
+ /** |
+ * @override |
+ */ |
+ willHide() { |
+ } |
+ |
+ /** |
+ * @override |
+ */ |
+ wasShown() { |
+ } |
+ |
+ /** |
+ * @override |
+ * @return {!Element} |
+ */ |
+ element() { |
+ return this._element; |
+ } |
}; |