blob: b94dceb67e92da61dc9f07eefc593d61546eae60 [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @unrestricted
*/
export default class ConsoleViewport {
/**
* @param {!Console.ConsoleViewportProvider} 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';
UI.ARIAUtils.markAsHidden(this._topGapElement);
UI.ARIAUtils.markAsHidden(this._bottomGapElement);
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._contentElement.addEventListener('focusin', this._onFocusIn.bind(this), false);
this._contentElement.addEventListener('focusout', this._onFocusOut.bind(this), false);
this._contentElement.addEventListener('keydown', this._onKeyDown.bind(this), false);
this._virtualSelectedIndex = -1;
this._contentElement.tabIndex = -1;
this._firstActiveIndex = -1;
this._lastActiveIndex = -1;
this._renderedItems = [];
this._anchorSelection = null;
this._headSelection = null;
this._itemCount = 0;
this._cumulativeHeights = new Int32Array(0);
this._muteCopyHandler = false;
// Listen for any changes to descendants and trigger a refresh. This ensures
// 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};
}
/**
* @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();
}
}
/**
* @return {boolean}
*/
hasVirtualSelection() {
return this._virtualSelectedIndex !== -1;
}
copyWithStyles() {
this._muteCopyHandler = true;
this.element.ownerDocument.execCommand('copy');
this._muteCopyHandler = false;
}
/**
* @param {!Event} event
*/
_onCopy(event) {
if (this._muteCopyHandler) {
return;
}
const text = this._selectedText();
if (!text) {
return;
}
event.preventDefault();
event.clipboardData.setData('text/plain', text);
}
/**
* @param {!Event} event
*/
_onFocusIn(event) {
const renderedIndex = this._renderedItems.findIndex(item => item.element().isSelfOrAncestor(event.target));
if (renderedIndex !== -1) {
this._virtualSelectedIndex = this._firstActiveIndex + renderedIndex;
}
let focusLastChild = false;
// Make default selection when moving from external (e.g. prompt) to the container.
if (this._virtualSelectedIndex === -1 && this._isOutsideViewport(/** @type {?Element} */ (event.relatedTarget)) &&
event.target === this._contentElement && this._itemCount) {
focusLastChild = true;
this._virtualSelectedIndex = this._itemCount - 1;
// Update stick to bottom before scrolling into view.
this.refresh();
this.scrollItemIntoView(this._virtualSelectedIndex);
}
this._updateFocusedItem(focusLastChild);
}
/**
* @param {!Event} event
*/
_onFocusOut(event) {
// Remove selection when focus moves to external location (e.g. prompt).
if (this._isOutsideViewport(/** @type {?Element} */ (event.relatedTarget))) {
this._virtualSelectedIndex = -1;
}
this._updateFocusedItem();
}
/**
* @param {?Element} element
* @return {boolean}
*/
_isOutsideViewport(element) {
return !!element && !element.isSelfOrDescendant(this._contentElement);
}
/**
* @param {!Event} event
*/
_onDragStart(event) {
const text = this._selectedText();
if (!text) {
return false;
}
event.dataTransfer.clearData();
event.dataTransfer.setData('text/plain', text);
event.dataTransfer.effectAllowed = 'copy';
return true;
}
/**
* @param {!Event} event
*/
_onKeyDown(event) {
if (UI.isEditing() || !this._itemCount || event.shiftKey) {
return;
}
let isArrowUp = false;
switch (event.key) {
case 'ArrowUp':
if (this._virtualSelectedIndex > 0) {
isArrowUp = true;
this._virtualSelectedIndex--;
} else {
return;
}
break;
case 'ArrowDown':
if (this._virtualSelectedIndex < this._itemCount - 1) {
this._virtualSelectedIndex++;
} else {
return;
}
break;
case 'Home':
this._virtualSelectedIndex = 0;
break;
case 'End':
this._virtualSelectedIndex = this._itemCount - 1;
break;
default:
return;
}
event.consume(true);
this.scrollItemIntoView(this._virtualSelectedIndex);
this._updateFocusedItem(isArrowUp);
}
/**
* @param {boolean=} focusLastChild
*/
_updateFocusedItem(focusLastChild) {
const selectedElement = this.renderedElementAt(this._virtualSelectedIndex);
const changed = this._lastSelectedElement !== selectedElement;
const containerHasFocus = this._contentElement === this.element.ownerDocument.deepActiveElement();
if (this._lastSelectedElement && changed) {
this._lastSelectedElement.classList.remove('console-selected');
}
if (selectedElement && (focusLastChild || changed || containerHasFocus) && this.element.hasFocus()) {
selectedElement.classList.add('console-selected');
// Do not focus the message if something within holds focus (e.g. object).
if (focusLastChild) {
this.setStickToBottom(false);
this._renderedItems[this._virtualSelectedIndex - this._firstActiveIndex].focusLastChildOrSelf();
} else if (!selectedElement.hasFocus()) {
focusWithoutScroll(selectedElement);
}
}
if (this._itemCount && !this._contentElement.hasFocus()) {
this._contentElement.tabIndex = 0;
} else {
this._contentElement.tabIndex = -1;
}
this._lastSelectedElement = selectedElement;
/**
* @suppress {checkTypes}
* @param {!Element} element
*/
function focusWithoutScroll(element) {
// TODO(luoe): Closure has an outdated typedef for Element.prototype.focus.
element.focus({preventScroll: true});
}
}
/**
* @return {!Element}
*/
contentElement() {
return this._contentElement;
}
invalidate() {
delete this._cachedProviderElements;
this._itemCount = this._provider.itemCount();
if (this._virtualSelectedIndex > this._itemCount - 1) {
this._virtualSelectedIndex = this._itemCount - 1;
}
this._rebuildCumulativeHeights();
this.refresh();
}
/**
* @param {number} index
* @return {?Console.ConsoleViewportElement}
*/
_providerElement(index) {
if (!this._cachedProviderElements) {
this._cachedProviderElements = new Array(this._itemCount);
}
let element = this._cachedProviderElements[index];
if (!element) {
element = this._provider.itemElement(index);
this._cachedProviderElements[index] = element;
}
return element;
}
_rebuildCumulativeHeights() {
const firstActiveIndex = this._firstActiveIndex;
const lastActiveIndex = this._lastActiveIndex;
let height = 0;
this._cumulativeHeights = new Int32Array(this._itemCount);
for (let i = 0; i < this._itemCount; ++i) {
if (firstActiveIndex <= i && i - firstActiveIndex < this._renderedItems.length && i <= lastActiveIndex) {
height += this._renderedItems[i - firstActiveIndex].element().offsetHeight;
} else {
height += this._provider.fastHeight(i);
}
this._cumulativeHeights[i] = height;
}
}
_rebuildCumulativeHeightsIfNeeded() {
let totalCachedHeight = 0;
let totalMeasuredHeight = 0;
// Check whether current items in DOM have changed heights. Tolerate 1-pixel
// error due to double-to-integer rounding errors.
for (let i = 0; i < this._renderedItems.length; ++i) {
const cachedItemHeight = this._cachedItemHeight(this._firstActiveIndex + i);
const measuredHeight = this._renderedItems[i].element().offsetHeight;
if (Math.abs(cachedItemHeight - measuredHeight) > 1) {
this._rebuildCumulativeHeights();
return;
}
totalMeasuredHeight += measuredHeight;
totalCachedHeight += cachedItemHeight;
if (Math.abs(totalCachedHeight - totalMeasuredHeight) > 1) {
this._rebuildCumulativeHeights();
return;
}
}
}
/**
* @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;
}
const 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) {
const range = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
if (!range || selection.isCollapsed || !this.element.hasSelection()) {
this._headSelection = null;
this._anchorSelection = null;
return false;
}
let firstSelected = Number.MAX_VALUE;
let lastSelected = -1;
let hasVisibleSelection = false;
for (let i = 0; i < this._renderedItems.length; ++i) {
if (range.intersectsNode(this._renderedItems[i].element())) {
const 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);
}
const topOverlap = range.intersectsNode(this._topGapElement) && this._topGapElement._active;
const 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;
}
const isBackward = this._isSelectionBackwards(selection);
const startSelection = this._selectionIsBackward ? this._headSelection : this._anchorSelection;
const 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) {
let anchorElement = null;
let 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;
}
let headElement = null;
let 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() {
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 (let 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;
this._updateFocusedItem();
return;
}
const selection = this.element.getComponentSelection();
const shouldRestoreSelection = this._updateSelectionModel(selection);
const visibleFrom = this.element.scrollTop;
const visibleHeight = this._visibleHeight();
const activeHeight = visibleHeight * 2;
this._rebuildCumulativeHeightsIfNeeded();
// 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(this._cumulativeHeights.lowerBound(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);
}
const topGapHeight = this._cumulativeHeights[this._firstActiveIndex - 1] || 0;
const bottomGapHeight =
this._cumulativeHeights[this._cumulativeHeights.length - 1] - this._cumulativeHeights[this._lastActiveIndex];
/**
* @this {Console.ConsoleViewport}
*/
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');
}
this._partialViewportUpdate(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
*/
_partialViewportUpdate(prepare) {
const itemsToRender = new Set();
for (let i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) {
itemsToRender.add(this._providerElement(i));
}
const willBeHidden = this._renderedItems.filter(item => !itemsToRender.has(item));
for (let i = 0; i < willBeHidden.length; ++i) {
willBeHidden[i].willHide();
}
prepare();
let hadFocus = false;
for (let i = 0; i < willBeHidden.length; ++i) {
hadFocus = hadFocus || willBeHidden[i].element().hasFocus();
willBeHidden[i].element().remove();
}
const wasShown = [];
let anchor = this._contentElement.firstChild;
for (const viewportElement of itemsToRender) {
const element = viewportElement.element();
if (element !== anchor) {
const shouldCallWasShown = !element.parentElement;
if (shouldCallWasShown) {
wasShown.push(viewportElement);
}
this._contentElement.insertBefore(element, anchor);
} else {
anchor = anchor.nextSibling;
}
}
for (let i = 0; i < wasShown.length; ++i) {
wasShown[i].wasShown();
}
this._renderedItems = Array.from(itemsToRender);
if (hadFocus) {
this._contentElement.focus();
}
this._updateFocusedItem();
}
/**
* @return {?string}
*/
_selectedText() {
this._updateSelectionModel(this.element.getComponentSelection());
if (!this._headSelection || !this._anchorSelection) {
return null;
}
let startSelection = null;
let endSelection = null;
if (this._selectionIsBackward) {
startSelection = this._headSelection;
endSelection = this._anchorSelection;
} else {
startSelection = this._anchorSelection;
endSelection = this._headSelection;
}
const textLines = [];
for (let i = startSelection.item; i <= endSelection.item; ++i) {
const element = this._providerElement(i).element();
const lineContent = element.childTextNodes().map(Components.Linkifier.untruncatedNodeText).join('');
textLines.push(lineContent);
}
const endSelectionElement = this._providerElement(endSelection.item).element();
if (endSelection.node && endSelection.node.isSelfOrDescendant(endSelectionElement)) {
const itemTextOffset = this._textOffsetInNode(endSelectionElement, endSelection.node, endSelection.offset);
textLines[textLines.length - 1] = textLines.peekLast().substring(0, itemTextOffset);
}
const startSelectionElement = this._providerElement(startSelection.item).element();
if (startSelection.node && startSelection.node.isSelfOrDescendant(startSelectionElement)) {
const itemTextOffset = this._textOffsetInNode(startSelectionElement, startSelection.node, startSelection.offset);
textLines[0] = textLines[0].substring(itemTextOffset);
}
return textLines.join('\n');
}
/**
* @param {!Element} itemElement
* @param {!Node} selectionNode
* @param {number} offset
* @return {number}
*/
_textOffsetInNode(itemElement, selectionNode, offset) {
// If the selectionNode is not a TextNode, we may need to convert a child offset into a character offset.
if (selectionNode.nodeType !== Node.TEXT_NODE) {
if (offset < selectionNode.childNodes.length) {
selectionNode = /** @type {!Node} */ (selectionNode.childNodes.item(offset));
offset = 0;
} else {
offset = selectionNode.textContent.length;
}
}
let chars = 0;
let node = itemElement;
while ((node = node.traverseNextNode(itemElement)) && node !== selectionNode) {
if (node.nodeType !== Node.TEXT_NODE || node.parentElement.nodeName === 'STYLE' ||
node.parentElement.nodeName === 'SCRIPT') {
continue;
}
chars += Components.Linkifier.untruncatedNodeText(node).length;
}
// If the selected node text was truncated, treat any non-zero offset as the full length.
const untruncatedContainerLength = Components.Linkifier.untruncatedNodeText(selectionNode).length;
if (offset > 0 && untruncatedContainerLength !== selectionNode.textContent.length) {
offset = untruncatedContainerLength;
}
return chars + offset;
}
/**
* @param {!Event} event
*/
_onScroll(event) {
this.refresh();
}
/**
* @return {number}
*/
firstVisibleIndex() {
if (!this._cumulativeHeights.length) {
return -1;
}
this._rebuildCumulativeHeightsIfNeeded();
return this._cumulativeHeights.lowerBound(this.element.scrollTop + 1);
}
/**
* @return {number}
*/
lastVisibleIndex() {
if (!this._cumulativeHeights.length) {
return -1;
}
this._rebuildCumulativeHeightsIfNeeded();
const scrollBottom = this.element.scrollTop + this.element.clientHeight;
const right = this._itemCount - 1;
return this._cumulativeHeights.lowerBound(scrollBottom, undefined, undefined, right);
}
/**
* @return {?Element}
*/
renderedElementAt(index) {
if (index === -1 || index < this._firstActiveIndex || index > this._lastActiveIndex) {
return null;
}
return this._renderedItems[index - this._firstActiveIndex].element();
}
/**
* @param {number} index
* @param {boolean=} makeLast
*/
scrollItemIntoView(index, makeLast) {
const firstVisibleIndex = this.firstVisibleIndex();
const lastVisibleIndex = this.lastVisibleIndex();
if (index > firstVisibleIndex && index < lastVisibleIndex) {
return;
}
// If the prompt is visible, then the last item must be fully on screen.
if (index === lastVisibleIndex &&
this._cumulativeHeights[index] <= this.element.scrollTop + this._visibleHeight()) {
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) {
console.assert(index >= 0 && index < this._itemCount, 'Cannot scroll item at invalid 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();
// After refresh, the item is in DOM, but may not be visible (items above were larger than expected).
this.renderedElementAt(index).scrollIntoView(true /* alignTop */);
}
/**
* @param {number} index
*/
forceScrollItemToBeLast(index) {
console.assert(index >= 0 && index < this._itemCount, 'Cannot scroll item at invalid index');
this.setStickToBottom(false);
this._rebuildCumulativeHeightsIfNeeded();
this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeight();
if (this.element.isScrolledToBottom()) {
this.setStickToBottom(true);
}
this.refresh();
// After refresh, the item is in DOM, but may not be visible (items above were larger than expected).
this.renderedElementAt(index).scrollIntoView(false /* alignTop */);
}
/**
* @return {number}
*/
_visibleHeight() {
// Use offsetHeight instead of clientHeight to avoid being affected by horizontal scroll.
return this.element.offsetHeight;
}
}
/**
* @interface
*/
class ConsoleViewportProvider {
/**
* @param {number} index
* @return {number}
*/
fastHeight(index) {
return 0;
}
/**
* @return {number}
*/
itemCount() {
return 0;
}
/**
* @return {number}
*/
minimumRowHeight() {
return 0;
}
/**
* @param {number} index
* @return {?Console.ConsoleViewportElement}
*/
itemElement(index) {
return null;
}
}
/**
* @interface
*/
export class ConsoleViewportElement {
willHide() {
}
wasShown() {
}
/**
* @return {!Element}
*/
element() {
}
}
/* Legacy exported object */
self.Console = self.Console || {};
/* Legacy exported object */
Console = Console || {};
/**
* @constructor
*/
Console.ConsoleViewport = ConsoleViewport;
/**
* @interface
*/
Console.ConsoleViewportProvider = ConsoleViewportProvider;
/**
* @interface
*/
Console.ConsoleViewportElement = ConsoleViewportElement;