es-grid-template
Version:
es-grid-template
529 lines (515 loc) • 17.8 kB
JavaScript
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
});
}
}
}
}