UNPKG

es-grid-template

Version:

es-grid-template

529 lines (515 loc) 17.8 kB
import cx from 'classnames'; import React from 'react'; import { BehaviorSubject, combineLatest, noop, Subscription } from 'rxjs'; import * as op from 'rxjs/operators'; import { calculateRenderInfo } from "./calculations"; import { EmptyHtmlTable } from "./empty"; import TableHeader from "./header"; import { getRichVisibleRectsStream } from "./helpers/getRichVisibleRectsStream"; import { getFullRenderRange, makeRowHeightManager } from "./helpers/rowHeightManager"; import { TableDOMHelper } from "./helpers/TableDOMUtils"; import { HtmlTable } from "./html-table"; import Loading from "./loading"; import { Classes, LOCK_SHADOW_PADDING, StyledArtTableWrapper } from "./styles"; import { // getScrollbarSize, // scroll mặc định thì bật lên OVERSCAN_SIZE, shallowEqual, STYLED_REF_PROP, sum, syncScrollLeft, throttledWindowResize$ } from "./utils"; let emptyContentDeprecatedWarned = false; function warnEmptyContentIsDeprecated() { if (!emptyContentDeprecatedWarned) { emptyContentDeprecatedWarned = true; console.warn('[ali-react-table] `BaseTable` props.emptyContent is deprecated. Please use `props.components.EmptyContent` to customize the table display when there is no data.'); } } export class BaseTable extends React.Component { static defaultProps = { showHeader: true, // theme: undefined, isStickyHeader: true, stickyTop: 0, footerDataSource: [], isStickyFooter: true, stickyBottom: 0, hasStickyScroll: true, stickyScrollHeight: 'auto', useVirtual: 'auto', estimatedRowHeight: 38, isLoading: false, components: {}, getRowProps: noop, dataSource: [], originData: [] }; // @ts-ignore rowHeightManager = makeRowHeightManager(this.props.dataSource.length, this.props.estimatedRowHeight); artTableWrapperRef = /*#__PURE__*/React.createRef(); // @ts-ignore domHelper; rootSubscription = new Subscription(); // Cache of last computed render result // @ts-ignore lastInfo; // @ts-ignore props$; /** @deprecated `BaseTable.getDoms()` is deprecated. Please do not call it. */ getDoms() { console.warn('[ali-react-table] BaseTable.getDoms() 已经过时'); return this.domHelper; } constructor(props) { super(props); this.state = { hasScroll: true, needRenderLock: true, offsetY: 0, offsetX: 0, // Because ResizeObserver invokes the provided callback initially, // set default values for maxRenderHeight/maxRenderWidth (they will be overwritten soon) // https://stackoverflow.com/questions/60026223/does-resizeobserver-invokes-initially-on-page-load maxRenderHeight: 600, maxRenderWidth: 800 }; } /** Customize the scrollbar width to match the table width, ensuring the scrollbar thumb has the same width */ updateStickyScroll() { const { stickyScroll, artTable, stickyScrollItem } = this.domHelper; if (!artTable) { return; } const tableBodyHtmlTable = this.domHelper.getTableBodyHtmlTable(); const innerTableWidth = tableBodyHtmlTable.offsetWidth; const artTableWidth = artTable.offsetWidth; const stickyScrollHeightProp = this.props.stickyScrollHeight; // scroll mặc định thì bật lên // const stickyScrollHeight = stickyScrollHeightProp === 'auto' ? getScrollbarSize().height : stickyScrollHeightProp const stickyScrollHeight = stickyScrollHeightProp === 'auto' ? 8 : stickyScrollHeightProp; // height customscrll bar = 8 stickyScroll.style.marginTop = `-${(stickyScrollHeight ?? 0) + 1}px`; if (artTableWidth >= innerTableWidth) { if (this.state.hasScroll) { this.setState({ hasScroll: false }); } } else { if (!this.state.hasScroll && stickyScrollHeight && stickyScrollHeight > 5) { // Consider the case of hidden scrollbars on macOS this.setState({ hasScroll: true }); } } // Set width of child element stickyScrollItem.style.width = `${innerTableWidth}px`; } renderTableHeader(info) { const { stickyTop, showHeader } = this.props; return /*#__PURE__*/React.createElement("div", { className: cx(Classes.tableHeader, 'no-scrollbar'), style: { top: stickyTop === 0 ? undefined : stickyTop, display: showHeader ? undefined : 'none' } }, /*#__PURE__*/React.createElement(TableHeader, { info: info })); } updateOffsetX(nextOffsetX) { if (this.lastInfo.useVirtual.horizontal) { if (Math.abs(nextOffsetX - this.state.offsetX) >= OVERSCAN_SIZE / 2) { this.setState({ offsetX: nextOffsetX }); } } } syncHorizontalScrollFromTableBody() { this.syncHorizontalScroll(this.domHelper.tableBody.scrollLeft); } /** Synchronize horizontal scroll offset */ syncHorizontalScroll(x) { this.updateOffsetX(x); const { tableBody } = this.domHelper; const { flat } = this.lastInfo; const leftLockShadow = this.domHelper.getLeftLockShadow(); if (leftLockShadow) { const shouldShowLeftLockShadow = flat.left.length > 0 && this.state.needRenderLock && x > 0; if (shouldShowLeftLockShadow) { leftLockShadow.classList.add('show-shadow'); } else { leftLockShadow.classList.remove('show-shadow'); } } const rightLockShadow = this.domHelper.getRightLockShadow(); if (rightLockShadow) { const shouldShowRightLockShadow = flat.right.length > 0 && this.state.needRenderLock && x < tableBody.scrollWidth - tableBody.clientWidth; if (shouldShowRightLockShadow) { rightLockShadow.classList.add('show-shadow'); } else { rightLockShadow.classList.remove('show-shadow'); } } } getVerticalRenderRange(useVirtual) { const { dataSource } = this.props; const { offsetY, maxRenderHeight } = this.state; const rowCount = dataSource.length; if (useVirtual.vertical) { return this.rowHeightManager.getRenderRange(offsetY, maxRenderHeight, rowCount); } else { return getFullRenderRange(rowCount); } } renderTableBody(info) { const { dataSource, originData = [], getRowProps, primaryKey, isLoading, emptyCellHeight, footerDataSource, components, id, commandClick, format } = this.props; const tableBodyClassName = cx(Classes.tableBody, Classes.horizontalScrollContainer, { 'no-scrollbar': footerDataSource && footerDataSource.length > 0 }); if (dataSource.length === 0) { const { components: comp, emptyContent } = this.props; let EmptyContent = comp?.EmptyContent; if (EmptyContent == null && emptyContent != null) { warnEmptyContentIsDeprecated(); EmptyContent = () => emptyContent; } return /*#__PURE__*/React.createElement("div", { className: tableBodyClassName }, /*#__PURE__*/React.createElement(EmptyHtmlTable, { descriptors: info.visible, isLoading: isLoading, EmptyContent: EmptyContent, emptyCellHeight: emptyCellHeight })); } const { topIndex, bottomBlank, topBlank, bottomIndex } = info.verticalRenderRange; return /*#__PURE__*/React.createElement("div", { className: tableBodyClassName }, topBlank > 0 && /*#__PURE__*/React.createElement("div", { key: "top-blank", className: cx(Classes.virtualBlank, 'top'), style: { height: topBlank } }), /*#__PURE__*/React.createElement(HtmlTable, { id: id ?? '', commandClick: commandClick, components: components, tbodyHtmlTag: "tbody", getRowProps: getRowProps // ----------------- , primaryKey: primaryKey, data: dataSource.slice(topIndex, bottomIndex), originData: originData, format: format, horizontalRenderInfo: info, verticalRenderInfo: { first: 0, offset: topIndex, limit: bottomIndex, last: dataSource.length - 1 } }), bottomBlank > 0 && /*#__PURE__*/React.createElement("div", { key: "bottom-blank", className: cx(Classes.virtualBlank, 'bottom'), style: { height: bottomBlank } })); } renderTableFooter(info) { const { footerDataSource = [], getRowProps, primaryKey, stickyBottom, components, id, format } = this.props; return /*#__PURE__*/React.createElement("div", { className: cx(Classes.tableFooter, Classes.horizontalScrollContainer), style: { bottom: stickyBottom === 0 ? undefined : stickyBottom } }, /*#__PURE__*/React.createElement(HtmlTable, { id: id ?? '' // commandClick={commandClick} , components: components, tbodyHtmlTag: "tfoot", data: footerDataSource, originData: [], format: format, horizontalRenderInfo: info, getRowProps: getRowProps, primaryKey: primaryKey, verticalRenderInfo: { offset: 0, first: 0, last: footerDataSource.length - 1, limit: Infinity } })); } renderLockShadows(info) { return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { className: Classes.lockShadowMask, style: { left: 0, width: info.leftLockTotalWidth + LOCK_SHADOW_PADDING } }, /*#__PURE__*/React.createElement("div", { className: cx(Classes.lockShadow, Classes.leftLockShadow) })), /*#__PURE__*/React.createElement("div", { className: Classes.lockShadowMask, style: { right: 0, width: info.rightLockTotalWidth + LOCK_SHADOW_PADDING } }, /*#__PURE__*/React.createElement("div", { className: cx(Classes.lockShadow, Classes.rightLockShadow) }))); } // eslint-disable-next-line @typescript-eslint/no-unused-vars renderStickyScroll(info) { const { hasStickyScroll, stickyBottom } = this.props; const { hasScroll } = this.state; return /*#__PURE__*/React.createElement("div", { className: cx(Classes.stickyScroll, Classes.horizontalScrollContainer), style: { display: hasStickyScroll && hasScroll ? 'block' : 'none', bottom: stickyBottom, marginTop: 0 } }, /*#__PURE__*/React.createElement("div", { className: Classes.stickyScrollItem })); } render() { const info = calculateRenderInfo(this); this.lastInfo = info; const { dataSource, className, style, showHeader, theme, useOuterBorder, isStickyHeader, isStickyFooter, isLoading, footerDataSource, components } = this.props; const artTableWrapperClassName = cx(Classes.artTableWrapper, { 'use-outer-border': useOuterBorder, empty: dataSource.length === 0, lock: info.hasLockColumn, 'has-header': showHeader, 'theme-dark': theme?.theme === 'dark', 'sticky-header': isStickyHeader, 'has-footer': footerDataSource && footerDataSource.length > 0, 'sticky-footer': isStickyFooter }, className); const artTableWrapperProps = { className: artTableWrapperClassName, style, [STYLED_REF_PROP]: this.artTableWrapperRef }; return /*#__PURE__*/React.createElement(StyledArtTableWrapper, artTableWrapperProps, /*#__PURE__*/React.createElement(Loading, { visible: isLoading, LoadingIcon: components?.LoadingIcon, LoadingContentWrapper: components?.LoadingContentWrapper }, /*#__PURE__*/React.createElement("div", { className: Classes.artTable }, this.renderTableHeader(info), this.renderTableBody(info), this.renderTableFooter(info), this.renderLockShadows(info)), this.renderStickyScroll(info))); } componentDidMount() { this.updateDOMHelper(); this.props$ = new BehaviorSubject(this.props); this.initSubscriptions(); this.didMountOrUpdate(); } componentDidUpdate(prevProps, prevState) { this.updateDOMHelper(); this.props$.next(this.props); this.didMountOrUpdate(prevProps, prevState); } didMountOrUpdate(prevProps, prevState) { this.syncHorizontalScrollFromTableBody(); this.updateStickyScroll(); this.adjustNeedRenderLock(); this.updateRowHeightManager(); this.updateScrollLeftWhenLayoutChanged(prevProps, prevState); } updateScrollLeftWhenLayoutChanged(prevProps, prevState) { if (prevState != null) { if (!prevState.hasScroll && this.state.hasScroll) { this.domHelper.stickyScroll.scrollLeft = 0; } } if (prevProps != null) { const prevHasFooter = prevProps.footerDataSource && prevProps.footerDataSource.length > 0; const currentHasFooter = this.props.footerDataSource && this.props.footerDataSource.length > 0; if (!prevHasFooter && currentHasFooter) { this.domHelper.tableFooter.scrollLeft = this.domHelper.tableBody.scrollLeft; } } } initSubscriptions() { const { tableHeader, tableBody, tableFooter, stickyScroll } = this.domHelper; this.rootSubscription.add(throttledWindowResize$.subscribe(() => { this.updateStickyScroll(); this.adjustNeedRenderLock(); })); // Scroll synchronization this.rootSubscription.add(syncScrollLeft([tableHeader, tableBody, tableFooter, stickyScroll], scrollLeft => { this.syncHorizontalScroll(scrollLeft); })); const richVisibleRects$ = getRichVisibleRectsStream(this.domHelper.artTable, this.props$.pipe(op.skip(1), op.mapTo('structure-may-change')), this.props.virtualDebugLabel).pipe(op.shareReplay()); // When the visible area changes, adjust the loading indicator position (if the loading indicator exists) this.rootSubscription.add(combineLatest([richVisibleRects$.pipe(op.map(p => p.clipRect), op.distinctUntilChanged(shallowEqual)), this.props$.pipe(op.startWith(null), op.pairwise(), op.filter(([prevProps, props]) => prevProps == null || !!!prevProps.isLoading && !!props?.isLoading))]).subscribe(([clipRect]) => { const loadingIndicator = this.domHelper.getLoadingIndicator(); if (!loadingIndicator) { return; } const height = clipRect.bottom - clipRect.top; // fixme Positioning here may fail in some edge cases; see #132 loadingIndicator.style.top = `${height / 2}px`; loadingIndicator.style.marginTop = `${height / 2}px`; })); // When the visible area changes, re-trigger render if virtual scrolling is enabled this.rootSubscription.add(richVisibleRects$.pipe(op.filter(() => { const { horizontal, vertical } = this.lastInfo.useVirtual; return horizontal || vertical; }), op.map(({ clipRect, offsetY }) => ({ maxRenderHeight: clipRect.bottom - clipRect.top, maxRenderWidth: clipRect.right - clipRect.left, offsetY })), op.distinctUntilChanged((x, y) => { return Math.abs(x.maxRenderWidth - y.maxRenderWidth) < OVERSCAN_SIZE / 2 && Math.abs(x.maxRenderHeight - y.maxRenderHeight) < OVERSCAN_SIZE / 2 && Math.abs(x.offsetY - y.offsetY) < OVERSCAN_SIZE / 2; })).subscribe(sizeAndOffset => { this.setState(sizeAndOffset); })); } componentWillUnmount() { this.rootSubscription.unsubscribe(); } /** Update references to DOM nodes to allow other methods to access them */ updateDOMHelper() { // @ts-ignore this.domHelper = new TableDOMHelper(this.artTableWrapperRef.current); } updateRowHeightManager() { const virtualTop = this.domHelper.getVirtualTop(); const virtualTopHeight = virtualTop?.clientHeight ?? 0; let zeroHeightRowCount = 0; let maxRowIndex = -1; let maxRowBottom = -1; for (const tr of this.domHelper.getTableRows()) { const rowIndex = Number(tr.dataset.rowindex); if (isNaN(rowIndex)) { continue; } maxRowIndex = Math.max(maxRowIndex, rowIndex); const offset = tr.offsetTop + virtualTopHeight; const size = tr.offsetHeight; if (size === 0) { zeroHeightRowCount += 1; } maxRowBottom = Math.max(maxRowBottom, offset + size); this.rowHeightManager.updateRow(rowIndex, offset, size); } // When `estimatedRowHeight` is too large, it may result in // "too few rendered rows to cover the visible area". // In this case, we check whether the next render will include more rows. // If so, we directly call `forceUpdate`. // `zeroHeightRowCount === 0` ensures there are currently no rows with `display: none`. if (maxRowIndex !== -1 && zeroHeightRowCount === 0) { if (maxRowBottom < this.state.offsetY + this.state.maxRenderHeight) { const vertical = this.getVerticalRenderRange(this.lastInfo.useVirtual); if (vertical.bottomIndex - 1 > maxRowIndex) { this.forceUpdate(); } } } } /** Calculate the total rendered width of all columns and determine whether lock sections need rendering */ adjustNeedRenderLock() { const { needRenderLock } = this.state; const { flat, hasLockColumn } = this.lastInfo; if (hasLockColumn) { // @ts-ignore const sumOfColWidth = sum(flat.full.map(col => col.width)); const nextNeedRenderLock = sumOfColWidth > this.domHelper.artTable.clientWidth; if (needRenderLock !== nextNeedRenderLock) { this.setState({ needRenderLock: nextNeedRenderLock }); } } else { if (needRenderLock) { this.setState({ needRenderLock: false }); } } } }