| // Copyright 2014 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. |
| /** |
| * @unrestricted |
| * @extends {DataGrid.DataGrid<!NODE_TYPE>} |
| * @template NODE_TYPE |
| */ |
| export default class ViewportDataGrid extends DataGrid.DataGrid { |
| /** |
| * @param {!Array.<!DataGrid.DataGrid.ColumnDescriptor>} columnsArray |
| * @param {function(!NODE_TYPE, string, string, string)=} editCallback |
| * @param {function(!NODE_TYPE)=} deleteCallback |
| * @param {function()=} refreshCallback |
| */ |
| constructor(columnsArray, editCallback, deleteCallback, refreshCallback) { |
| super(columnsArray, editCallback, deleteCallback, refreshCallback); |
| |
| this._onScrollBound = this._onScroll.bind(this); |
| this.scrollContainer.addEventListener('scroll', this._onScrollBound, true); |
| |
| /** @type {!Array.<!DataGrid.ViewportDataGridNode>} */ |
| this._visibleNodes = []; |
| /** |
| * @type {boolean} |
| */ |
| this._inline = false; |
| |
| this._stickToBottom = false; |
| this._updateIsFromUser = false; |
| this._lastScrollTop = 0; |
| this._firstVisibleIsStriped = false; |
| this._isStriped = false; |
| |
| this.setRootNode(new DataGrid.ViewportDataGridNode()); |
| } |
| |
| /** |
| * @param {boolean} striped |
| * @override |
| */ |
| setStriped(striped) { |
| this._isStriped = striped; |
| let startsWithOdd = true; |
| if (this._visibleNodes.length) { |
| const allChildren = this.rootNode().flatChildren(); |
| startsWithOdd = !!(allChildren.indexOf(this._visibleNodes[0])); |
| } |
| this._updateStripesClass(startsWithOdd); |
| } |
| |
| /** |
| * @param {boolean} startsWithOdd |
| */ |
| _updateStripesClass(startsWithOdd) { |
| this.element.classList.toggle('striped-data-grid', !startsWithOdd && this._isStriped); |
| this.element.classList.toggle('striped-data-grid-starts-with-odd', startsWithOdd && this._isStriped); |
| } |
| |
| /** |
| * @param {!Element} scrollContainer |
| */ |
| setScrollContainer(scrollContainer) { |
| this.scrollContainer.removeEventListener('scroll', this._onScrollBound, true); |
| /** |
| * @suppress {accessControls} |
| */ |
| this._scrollContainer = scrollContainer; |
| this.scrollContainer.addEventListener('scroll', this._onScrollBound, true); |
| } |
| |
| /** |
| * @override |
| */ |
| onResize() { |
| if (this._stickToBottom) { |
| this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight - this.scrollContainer.clientHeight; |
| } |
| this.scheduleUpdate(); |
| super.onResize(); |
| } |
| |
| /** |
| * @param {boolean} stick |
| */ |
| setStickToBottom(stick) { |
| this._stickToBottom = stick; |
| } |
| |
| /** |
| * @param {?Event} event |
| */ |
| _onScroll(event) { |
| this._stickToBottom = this.scrollContainer.isScrolledToBottom(); |
| if (this._lastScrollTop !== this.scrollContainer.scrollTop) { |
| this.scheduleUpdate(true); |
| } |
| } |
| |
| /** |
| * @protected |
| */ |
| scheduleUpdateStructure() { |
| this.scheduleUpdate(); |
| } |
| |
| /** |
| * @param {boolean=} isFromUser |
| */ |
| scheduleUpdate(isFromUser) { |
| if (this._stickToBottom && isFromUser) { |
| this._stickToBottom = this.scrollContainer.isScrolledToBottom(); |
| } |
| this._updateIsFromUser = this._updateIsFromUser || isFromUser; |
| if (this._updateAnimationFrameId) { |
| return; |
| } |
| this._updateAnimationFrameId = this.element.window().requestAnimationFrame(this._update.bind(this)); |
| } |
| |
| // TODO(allada) This should be fixed to never be needed. It is needed right now for network because removing |
| // elements happens followed by a scheduleRefresh() which causes white space to be visible, but the waterfall |
| // updates instantly. |
| updateInstantly() { |
| this._update(); |
| } |
| |
| /** |
| * @override |
| */ |
| renderInline() { |
| this._inline = true; |
| super.renderInline(); |
| this._update(); |
| } |
| |
| /** |
| * @param {number} clientHeight |
| * @param {number} scrollTop |
| * @return {{topPadding: number, bottomPadding: number, contentHeight: number, visibleNodes: !Array.<!DataGrid.ViewportDataGridNode>, offset: number}} |
| */ |
| _calculateVisibleNodes(clientHeight, scrollTop) { |
| const nodes = this.rootNode().flatChildren(); |
| if (this._inline) { |
| return {topPadding: 0, bottomPadding: 0, contentHeight: 0, visibleNodes: nodes, offset: 0}; |
| } |
| |
| const size = nodes.length; |
| let i = 0; |
| let y = 0; |
| |
| for (; i < size && y + nodes[i].nodeSelfHeight() < scrollTop; ++i) { |
| y += nodes[i].nodeSelfHeight(); |
| } |
| const start = i; |
| const topPadding = y; |
| |
| for (; i < size && y < scrollTop + clientHeight; ++i) { |
| y += nodes[i].nodeSelfHeight(); |
| } |
| const end = i; |
| |
| let bottomPadding = 0; |
| for (; i < size; ++i) { |
| bottomPadding += nodes[i].nodeSelfHeight(); |
| } |
| |
| return { |
| topPadding: topPadding, |
| bottomPadding: bottomPadding, |
| contentHeight: y - topPadding, |
| visibleNodes: nodes.slice(start, end), |
| offset: start |
| }; |
| } |
| |
| /** |
| * @return {number} |
| */ |
| _contentHeight() { |
| const nodes = this.rootNode().flatChildren(); |
| let result = 0; |
| for (let i = 0, size = nodes.length; i < size; ++i) { |
| result += nodes[i].nodeSelfHeight(); |
| } |
| return result; |
| } |
| |
| _update() { |
| if (this._updateAnimationFrameId) { |
| this.element.window().cancelAnimationFrame(this._updateAnimationFrameId); |
| delete this._updateAnimationFrameId; |
| } |
| |
| const clientHeight = this.scrollContainer.clientHeight; |
| let scrollTop = this.scrollContainer.scrollTop; |
| const currentScrollTop = scrollTop; |
| const maxScrollTop = Math.max(0, this._contentHeight() - clientHeight); |
| if (!this._updateIsFromUser && this._stickToBottom) { |
| scrollTop = maxScrollTop; |
| } |
| this._updateIsFromUser = false; |
| scrollTop = Math.min(maxScrollTop, scrollTop); |
| |
| const viewportState = this._calculateVisibleNodes(clientHeight, scrollTop); |
| const visibleNodes = viewportState.visibleNodes; |
| const visibleNodesSet = new Set(visibleNodes); |
| |
| for (let i = 0; i < this._visibleNodes.length; ++i) { |
| const oldNode = this._visibleNodes[i]; |
| if (!visibleNodesSet.has(oldNode) && oldNode.attached()) { |
| const element = oldNode.existingElement(); |
| element.remove(); |
| } |
| } |
| |
| let previousElement = this.topFillerRowElement(); |
| const tBody = this.dataTableBody; |
| let offset = viewportState.offset; |
| |
| if (visibleNodes.length) { |
| const nodes = this.rootNode().flatChildren(); |
| const index = nodes.indexOf(visibleNodes[0]); |
| this._updateStripesClass(!!(index % 2)); |
| if (this._stickToBottom && index !== -1 && !!(index % 2) !== this._firstVisibleIsStriped) { |
| offset += 1; |
| } |
| } |
| |
| this._firstVisibleIsStriped = !!(offset % 2); |
| |
| for (let i = 0; i < visibleNodes.length; ++i) { |
| const node = visibleNodes[i]; |
| const element = node.element(); |
| node.setStriped((offset + i) % 2 === 0); |
| if (element !== previousElement.nextSibling) { |
| tBody.insertBefore(element, previousElement.nextSibling); |
| } |
| node.revealed = true; |
| previousElement = element; |
| } |
| |
| this.setVerticalPadding(viewportState.topPadding, viewportState.bottomPadding); |
| this._lastScrollTop = scrollTop; |
| if (scrollTop !== currentScrollTop) { |
| this.scrollContainer.scrollTop = scrollTop; |
| } |
| const contentFits = |
| viewportState.contentHeight <= clientHeight && viewportState.topPadding + viewportState.bottomPadding === 0; |
| if (contentFits !== this.element.classList.contains('data-grid-fits-viewport')) { |
| this.element.classList.toggle('data-grid-fits-viewport', contentFits); |
| this.updateWidths(); |
| } |
| this._visibleNodes = visibleNodes; |
| this.dispatchEventToListeners(Events.ViewportCalculated); |
| } |
| |
| /** |
| * @param {!DataGrid.ViewportDataGridNode} node |
| */ |
| _revealViewportNode(node) { |
| const nodes = this.rootNode().flatChildren(); |
| const index = nodes.indexOf(node); |
| if (index === -1) { |
| return; |
| } |
| let fromY = 0; |
| for (let i = 0; i < index; ++i) { |
| fromY += nodes[i].nodeSelfHeight(); |
| } |
| const toY = fromY + node.nodeSelfHeight(); |
| |
| let scrollTop = this.scrollContainer.scrollTop; |
| if (scrollTop > fromY) { |
| scrollTop = fromY; |
| this._stickToBottom = false; |
| } else if (scrollTop + this.scrollContainer.offsetHeight < toY) { |
| scrollTop = toY - this.scrollContainer.offsetHeight; |
| } |
| this.scrollContainer.scrollTop = scrollTop; |
| } |
| } |
| |
| /** |
| * @override @suppress {checkPrototypalTypes} @enum {symbol} |
| */ |
| export const Events = { |
| ViewportCalculated: Symbol('ViewportCalculated') |
| }; |
| |
| /** |
| * @unrestricted |
| * @extends {DataGrid.DataGridNode<!NODE_TYPE>} |
| * @template NODE_TYPE |
| */ |
| export class ViewportDataGridNode extends DataGrid.DataGridNode { |
| /** |
| * @param {?Object.<string, *>=} data |
| * @param {boolean=} hasChildren |
| */ |
| constructor(data, hasChildren) { |
| super(data, hasChildren); |
| /** @type {boolean} */ |
| this._stale = false; |
| /** @type {?Array<!DataGrid.ViewportDataGridNode>} */ |
| this._flatNodes = null; |
| this._isStriped = false; |
| } |
| |
| /** |
| * @override |
| * @return {!Element} |
| */ |
| element() { |
| const existingElement = this.existingElement(); |
| const element = existingElement || this.createElement(); |
| if (!existingElement || this._stale) { |
| this.createCells(element); |
| this._stale = false; |
| } |
| return element; |
| } |
| |
| /** |
| * @param {boolean} isStriped |
| */ |
| setStriped(isStriped) { |
| this._isStriped = isStriped; |
| this.element().classList.toggle('odd', isStriped); |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| isStriped() { |
| return this._isStriped; |
| } |
| |
| /** |
| * @protected |
| */ |
| clearFlatNodes() { |
| this._flatNodes = null; |
| const parent = /** @type {!DataGrid.ViewportDataGridNode} */ (this.parent); |
| if (parent) { |
| parent.clearFlatNodes(); |
| } |
| } |
| |
| /** |
| * @return {!Array<!DataGrid.ViewportDataGridNode>} |
| */ |
| flatChildren() { |
| if (this._flatNodes) { |
| return this._flatNodes; |
| } |
| /** @type {!Array<!DataGrid.ViewportDataGridNode>} */ |
| const flatNodes = []; |
| /** @type {!Array<!Array<!DataGrid.ViewportDataGridNode>>} */ |
| const children = [this.children]; |
| /** @type {!Array<number>} */ |
| const counters = [0]; |
| let depth = 0; |
| while (depth >= 0) { |
| if (children[depth].length <= counters[depth]) { |
| depth--; |
| continue; |
| } |
| const node = children[depth][counters[depth]++]; |
| flatNodes.push(node); |
| if (node.expanded && node.children.length) { |
| depth++; |
| children[depth] = node.children; |
| counters[depth] = 0; |
| } |
| } |
| |
| this._flatNodes = flatNodes; |
| return flatNodes; |
| } |
| |
| /** |
| * @override |
| * @param {!NODE_TYPE} child |
| * @param {number} index |
| */ |
| insertChild(child, index) { |
| this.clearFlatNodes(); |
| if (child.parent === this) { |
| const currentIndex = this.children.indexOf(child); |
| if (currentIndex < 0) { |
| console.assert(false, 'Inconsistent DataGrid state'); |
| } |
| if (currentIndex === index) { |
| return; |
| } |
| if (currentIndex < index) { |
| --index; |
| } |
| } |
| child.remove(); |
| child.parent = this; |
| child.dataGrid = this.dataGrid; |
| if (!this.children.length) { |
| this.setHasChildren(true); |
| } |
| this.children.splice(index, 0, child); |
| child.recalculateSiblings(index); |
| if (this.expanded) { |
| this.dataGrid.scheduleUpdateStructure(); |
| } |
| } |
| |
| /** |
| * @override |
| * @param {!NODE_TYPE} child |
| */ |
| removeChild(child) { |
| this.clearFlatNodes(); |
| if (this.dataGrid) { |
| this.dataGrid.updateSelectionBeforeRemoval(child, false); |
| } |
| if (child.previousSibling) { |
| child.previousSibling.nextSibling = child.nextSibling; |
| } |
| if (child.nextSibling) { |
| child.nextSibling.previousSibling = child.previousSibling; |
| } |
| if (child.parent !== this) { |
| throw 'removeChild: Node is not a child of this node.'; |
| } |
| |
| this.children.remove(child, true); |
| child._unlink(); |
| |
| if (!this.children.length) { |
| this.setHasChildren(false); |
| } |
| if (this.expanded) { |
| this.dataGrid.scheduleUpdateStructure(); |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| removeChildren() { |
| this.clearFlatNodes(); |
| if (this.dataGrid) { |
| this.dataGrid.updateSelectionBeforeRemoval(this, true); |
| } |
| for (let i = 0; i < this.children.length; ++i) { |
| this.children[i]._unlink(); |
| } |
| this.children = []; |
| |
| if (this.expanded) { |
| this.dataGrid.scheduleUpdateStructure(); |
| } |
| } |
| |
| _unlink() { |
| if (this.attached()) { |
| this.existingElement().remove(); |
| } |
| this.resetNode(); |
| } |
| |
| /** |
| * @override |
| */ |
| collapse() { |
| if (!this.expanded) { |
| return; |
| } |
| this.clearFlatNodes(); |
| /** |
| * @suppress {accessControls} |
| */ |
| this._expanded = false; |
| if (this.existingElement()) { |
| this.existingElement().classList.remove('expanded'); |
| } |
| this.dataGrid.scheduleUpdateStructure(); |
| } |
| |
| /** |
| * @override |
| */ |
| expand() { |
| if (this.expanded) { |
| return; |
| } |
| this.dataGrid._stickToBottom = false; |
| this.clearFlatNodes(); |
| super.expand(); |
| this.dataGrid.scheduleUpdateStructure(); |
| } |
| |
| /** |
| * @protected |
| * @return {boolean} |
| */ |
| attached() { |
| return !!(this.dataGrid && this.existingElement() && this.existingElement().parentElement); |
| } |
| |
| /** |
| * @override |
| */ |
| refresh() { |
| if (this.attached()) { |
| this._stale = true; |
| this.dataGrid.scheduleUpdate(); |
| } else { |
| this.resetElement(); |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| reveal() { |
| this.dataGrid._revealViewportNode(this); |
| } |
| |
| /** |
| * @override |
| * @param {number} index |
| */ |
| recalculateSiblings(index) { |
| this.clearFlatNodes(); |
| super.recalculateSiblings(index); |
| } |
| } |
| |
| /* Legacy exported object */ |
| self.DataGrid = self.DataGrid || {}; |
| |
| /* Legacy exported object */ |
| DataGrid = DataGrid || {}; |
| |
| /** |
| * @unrestricted |
| * @extends {DataGrid.DataGrid<!NODE_TYPE>} |
| * @constructor |
| */ |
| DataGrid.ViewportDataGrid = ViewportDataGrid; |
| |
| /** |
| * @override @suppress {checkPrototypalTypes} @enum {symbol} |
| */ |
| DataGrid.ViewportDataGrid.Events = Events; |
| |
| /** |
| * @unrestricted |
| * @extends {DataGrid.DataGridNode<!NODE_TYPE>} |
| * @constructor |
| */ |
| DataGrid.ViewportDataGridNode = ViewportDataGridNode; |