UNPKG

@atlaskit/renderer

Version:
853 lines (837 loc) • 38.6 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; /* eslint-disable @atlaskit/ui-styling-standard/no-classname-prop, @atlaskit/ui-styling-standard/enforce-style-prop, @repo/internal/react/no-class-components */ import React from 'react'; import { TableSharedCssClassName, tableMarginTop } from '@atlaskit/editor-common/styles'; import { getTableContainerWidth } from '@atlaskit/editor-common/node-width'; import { FullPagePadding } from '../../ui/Renderer/style'; import { fg } from '@atlaskit/platform-feature-flags'; import { RendererCssClassName } from '../../consts'; import { createCompareNodes, convertProsemirrorTableNodeToArrayOfRows, hasMergedCell, compose } from '@atlaskit/editor-common/utils'; import { SortOrder } from '@atlaskit/editor-common/types'; import { akEditorDefaultLayoutWidth, akEditorFullWidthLayoutWidth, akEditorMaxWidthLayoutWidth } from '@atlaskit/editor-shared-styles'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { TableHeader } from './tableCell'; import { StickyTable, tableStickyPadding, OverflowParent } from './table/sticky'; import { Table } from './table/table'; import { isCommentAppearance, isFullWidthOrFullPageAppearance, isFullWidthAppearance, isMaxWidthAppearance, isFullPageAppearance } from '../utils/appearance'; import { TableStickyScrollbar } from './TableStickyScrollbar'; import { useRendererContext } from '../../renderer-context'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { isTableInContentMode } from '@atlaskit/editor-common/table'; import { isContentModeSupported } from './table/content-mode'; const stickyContainerBaseStyles = { // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 height: "var(--ds-space-250, 20px)", // MAX_BROWSER_SCROLLBAR_HEIGHT // Follow editor to hide by default so it does not show empty gap in SSR // https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master/platform/packages/editor/editor-plugin-table/src/nodeviews/TableComponent.tsx#957 // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 display: 'block', width: '100%' }; const stickyContainerAdditionalStyles = { visibility: 'hidden', overflowX: 'auto', position: 'sticky', bottom: "var(--ds-space-0, 0px)", zIndex: 1 }; export const isTableResizingEnabled = appearance => isFullWidthOrFullPageAppearance(appearance) || isCommentAppearance(appearance); export const isStickyScrollbarEnabled = appearance => isFullWidthOrFullPageAppearance(appearance) && editorExperiment('platform_renderer_table_sticky_scrollbar', true, { exposure: true }); export const orderChildren = (children, tableNode, smartCardStorage, tableOrderStatus) => { if (!tableOrderStatus || tableOrderStatus.order === SortOrder.NO_ORDER) { return children; } const { order, columnIndex } = tableOrderStatus; const compareNodesInOrder = createCompareNodes({ getInlineCardTextFromStore(attrs) { const { url } = attrs; if (!url) { return null; } return smartCardStorage.get(url) || null; } }, order); const tableArray = convertProsemirrorTableNodeToArrayOfRows(tableNode); const tableArrayWithChildren = tableArray.map((rowNodes, index) => ({ rowNodes, rowReact: children[index] })); const headerRow = tableArrayWithChildren.shift(); const sortedTable = tableArrayWithChildren.sort((rowA, rowB) => compareNodesInOrder(rowA.rowNodes[columnIndex], rowB.rowNodes[columnIndex])); if (headerRow) { sortedTable.unshift(headerRow); } return sortedTable.map(elem => elem.rowReact); }; export const hasRowspan = row => { let hasRowspan = false; row.forEach(cell => hasRowspan = hasRowspan || cell.attrs.rowspan > 1); return hasRowspan; }; export const getRefTop = refElement => { return Math.round(refElement.getBoundingClientRect().top); }; export const shouldHeaderStick = (scrollTop, tableTop, tableBottom, rowHeight) => tableTop <= scrollTop && !(tableBottom - rowHeight <= scrollTop); export const shouldHeaderPinBottom = (scrollTop, tableBottom, rowHeight) => tableBottom - rowHeight <= scrollTop && !(tableBottom < scrollTop); export const addSortableColumn = (rows, tableOrderStatus, onSorting // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => { return React.Children.map(rows, (row, index) => { if (index === 0) { return /*#__PURE__*/React.cloneElement(React.Children.only(row), { tableOrderStatus, onSorting }); } return row; }); }; export const isHeaderRowEnabled = (rows // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => { if (!rows.length) { return false; } // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any const { children } = rows[0].props; if (!children.length) { return false; } if (children.length === 1) { return children[0].type === TableHeader; } return children.every(node => node.type === TableHeader); }; export const tableCanBeSticky = (node, children // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => { return isHeaderRowEnabled(children) && node && node.firstChild && !hasRowspan(node.firstChild); }; /** * Fake left/right borders rendered as direct children of TABLE_CONTAINER * (the non-scrolling parent of the horizontally scrolling TABLE_NODE_WRAPPER). * * The visible styling for these divs lives in `tableFakeBorderStyles` * (`renderer/src/ui/Renderer/RendererStyleContainer.tsx`), which is itself * gated on `editorExperiment('platform_synced_block', true)` AND * `isInsideSyncBlock`. * * Shared between `renderer/src/react/nodes/table.tsx` and * `renderer/src/react/nodes/tableNew.tsx` so the two stay in sync. */ const TableFakeBorders = ({ isNumberColumnEnabled }) => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { className: TableSharedCssClassName.TABLE_LEFT_BORDER, "data-with-numbered-table": isNumberColumnEnabled ? 'true' : undefined, "data-testid": "table-left-border" }), /*#__PURE__*/React.createElement("div", { className: TableSharedCssClassName.TABLE_RIGHT_BORDER, "data-with-numbered-table": isNumberColumnEnabled ? 'true' : undefined, "data-testid": "table-right-border" })); /** * Reads `nestedRendererType` from RendererContext and renders the fake left/right * borders only when the current renderer is the nested renderer for a reference * synced block. */ export const RefSyncBlockFakeBorders = ({ isNumberColumnEnabled }) => { const { nestedRendererType } = useRendererContext(); const isInsideOfRefSyncBlock = nestedRendererType === 'syncedBlock'; if (!isInsideOfRefSyncBlock || !editorExperiment('platform_synced_block', true)) { return null; } return /*#__PURE__*/React.createElement(TableFakeBorders, { isNumberColumnEnabled: isNumberColumnEnabled }); }; /** * TableContainer renders tables using only CSS-based rules */ // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/react/no-class-components /** * */ export class TableContainer extends React.Component { constructor(...args) { super(...args); _defineProperty(this, "state", { stickyMode: 'none', wrapperWidth: 0, headerRowHeight: 0 }); _defineProperty(this, "tableRef", /*#__PURE__*/React.createRef()); _defineProperty(this, "stickyHeaderRef", /*#__PURE__*/React.createRef()); _defineProperty(this, "stickyScrollbarRef", /*#__PURE__*/React.createRef()); // used for sync scroll + copying wrapper width to sticky header _defineProperty(this, "stickyWrapperRef", /*#__PURE__*/React.createRef()); _defineProperty(this, "wrapperRef", /*#__PURE__*/React.createRef()); _defineProperty(this, "overflowParent", null); _defineProperty(this, "updatedLayout", 'custom'); _defineProperty(this, "containerRef", null); _defineProperty(this, "_isInsideNestedRenderer", null); // Stores the last computed style values from render() for use by applyNestedRendererTableFix(). // This avoids reading from the DOM which can be stale when React removes properties between renders. _defineProperty(this, "lastComputedStyle", {}); _defineProperty(this, "resizeObserver", null); _defineProperty(this, "applyResizerChange", entries => { let wrapperWidth = this.state.wrapperWidth; let headerRowHeight = this.state.headerRowHeight; for (const entry of entries) { if (entry.target === this.wrapperRef.current) { wrapperWidth = entry.contentRect.width; } else if (entry.target === this.stickyHeaderRef.current) { headerRowHeight = Math.round(entry.contentRect.height); } } if (headerRowHeight !== this.state.headerRowHeight || wrapperWidth !== this.state.wrapperWidth) { this.setState({ wrapperWidth, headerRowHeight }); } }); /** * Callback ref that captures the container DOM element and also forwards * to the handleRef prop from the overflow shadow HOC. */ _defineProperty(this, "setContainerRef", el => { this.containerRef = el; const { handleRef } = this.props; if (typeof handleRef === 'function') { handleRef(el); } else if (handleRef && typeof handleRef === 'object') { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any handleRef.current = el; } }); _defineProperty(this, "componentWillUnmount", () => { if (this.overflowParent) { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners this.overflowParent.removeEventListener('scroll', this.onScroll); } if (this.nextFrame) { cancelAnimationFrame(this.nextFrame); } if (this.resizeObserver) { this.resizeObserver.disconnect(); } if (this.stickyScrollbar) { this.stickyScrollbar.dispose(); } }); _defineProperty(this, "getScrollTop", () => { const { stickyHeaders } = this.props; const offsetTop = stickyHeaders && stickyHeaders.offsetTop || 0; return (this.overflowParent ? this.overflowParent.top : 0) + offsetTop; }); _defineProperty(this, "updateSticky", () => { const tableElem = this.tableRef.current; const refElem = this.stickyHeaderRef.current; if (!tableElem || !refElem) { return; } const scrollTop = this.getScrollTop() + tableStickyPadding; const tableTop = getRefTop(tableElem); const tableBottom = tableTop + tableElem.clientHeight; const shouldSticky = shouldHeaderStick(scrollTop, tableTop, tableBottom, refElem.clientHeight); const shouldPin = shouldHeaderPinBottom(scrollTop, tableBottom, refElem.clientHeight); let stickyMode = 'none'; if (shouldPin) { stickyMode = 'pin-bottom'; } else if (shouldSticky) { stickyMode = 'stick'; } if (this.state.stickyMode !== stickyMode) { this.setState({ stickyMode }); } this.nextFrame = undefined; }); _defineProperty(this, "onScroll", () => { if (!this.nextFrame) { this.nextFrame = requestAnimationFrame(this.updateSticky); } }); _defineProperty(this, "onWrapperScrolled", () => { if (!this.wrapperRef.current || !this.stickyWrapperRef.current) { return; } this.stickyWrapperRef.current.scrollLeft = this.wrapperRef.current.scrollLeft; if (this.stickyScrollbarRef.current) { this.stickyScrollbarRef.current.scrollLeft = this.wrapperRef.current.scrollLeft; } }); _defineProperty(this, "grabFirstRowRef", children => { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any return React.Children.map(children || false, (child, idx) => { if (idx === 0 && /*#__PURE__*/React.isValidElement(child)) { return /*#__PURE__*/React.cloneElement(child, { innerRef: this.stickyHeaderRef }); } return child; }); }); } /** * Checks if this table is inside a nested renderer (e.g. Include Page macro) * by looking for multiple .ak-renderer-document ancestors in the DOM. * The result is cached since a table's position in the DOM tree is stable after mount. */ isInsideNestedRenderer() { if (this._isInsideNestedRenderer !== null) { return this._isInsideNestedRenderer; } if (!this.containerRef) { return false; } let docAncestorCount = 0; let el = this.containerRef.parentElement; while (el) { if (el.classList.contains(RendererCssClassName.DOCUMENT)) { docAncestorCount++; if (docAncestorCount >= 2) { this._isInsideNestedRenderer = true; return true; } } el = el.parentElement; } this._isInsideNestedRenderer = false; return false; } /** * For tables inside nested renderers (e.g. Include Page macro), the parent * renderer's CSS override forces width:100%!important and left:0!important * which overrides the inline styles set by this component. Using * style.setProperty with 'important' priority on inline styles beats * stylesheet !important rules per the CSS cascade. * * Uses lastComputedStyle (populated during render) rather than reading from * element.style, because React may remove properties from the DOM when their * values transition to undefined between renders. */ applyNestedRendererTableFix() { if (!this.containerRef || !fg('platform_nested_table_style_override')) { return; } if (!this.isInsideNestedRenderer()) { return; } const { width, left, marginLeft } = this.lastComputedStyle; const style = this.containerRef.style; style.setProperty('width', width || 'auto', 'important'); style.setProperty('left', left || 'auto', 'important'); style.setProperty('margin-left', marginLeft || '0', 'important'); } /** * Starts observing table dimensions and wires sticky header/scrollbar behavior after mount. * * @example */ componentDidMount() { this.resizeObserver = new ResizeObserver(this.applyResizerChange); if (this.wrapperRef.current) { this.resizeObserver.observe(this.wrapperRef.current); } if (this.stickyHeaderRef.current) { this.resizeObserver.observe(this.stickyHeaderRef.current); } if (this.props.stickyHeaders) { var _this$props$stickyHea; this.overflowParent = OverflowParent.fromElement(this.tableRef.current, (_this$props$stickyHea = this.props.stickyHeaders) === null || _this$props$stickyHea === void 0 ? void 0 : _this$props$stickyHea.defaultScrollRootId_DO_NOT_USE); // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners this.overflowParent.addEventListener('scroll', this.onScroll); } if (this.wrapperRef.current && isStickyScrollbarEnabled(this.props.rendererAppearance)) { this.stickyScrollbar = new TableStickyScrollbar(this.wrapperRef.current); } this.applyNestedRendererTableFix(); } /** * Updates sticky header wiring and scroll synchronization after prop or state changes. * * @param prevProps * @param prevState * @example */ componentDidUpdate(prevProps, prevState) { // toggling sticky headers visiblity if (this.props.stickyHeaders && !this.overflowParent) { var _this$props$stickyHea2; this.overflowParent = OverflowParent.fromElement(this.tableRef.current, (_this$props$stickyHea2 = this.props.stickyHeaders) === null || _this$props$stickyHea2 === void 0 ? void 0 : _this$props$stickyHea2.defaultScrollRootId_DO_NOT_USE); } else if (!this.props.stickyHeaders && this.overflowParent) { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners this.overflowParent.removeEventListener('scroll', this.onScroll); this.overflowParent = null; } // offsetTop might have changed, re-position sticky header if (this.props.stickyHeaders !== prevProps.stickyHeaders) { this.updateSticky(); } // sync horizontal scroll in floating div when toggling modes if (prevState.stickyMode !== this.state.stickyMode) { this.onWrapperScrolled(); } // React re-applies the style prop on every render, which overwrites the // !important priorities set at mount time. Re-apply after each update. this.applyNestedRendererTableFix(); } /** * Calculates the top offset used when the sticky header is pinned to the table bottom. */ get pinTop() { if (!this.tableRef.current || !this.stickyHeaderRef.current) { return; } return this.tableRef.current.offsetHeight - this.stickyHeaderRef.current.offsetHeight + tableMarginTop - tableStickyPadding; } /** * Determines whether sticky header positioning should include the default scroll root offset. */ get shouldAddOverflowParentOffsetTop_DO_NOT_USE() { // IF the StickyHeaderConfig specifies that the default scroll root offsetTop should be added // AND the StickyHeaderConfig specifies a default scroll root id // AND the OverflowParent is the corresponding element // THEN we should add the OverflowParent offset top (RETURN TRUE) return this.props.stickyHeaders && !!this.props.stickyHeaders.shouldAddDefaultScrollRootOffsetTop_DO_NOT_USE && !!this.props.stickyHeaders.defaultScrollRootId_DO_NOT_USE && this.overflowParent && this.overflowParent.id === this.props.stickyHeaders.defaultScrollRootId_DO_NOT_USE; } /** * Resolves the top position for the sticky header based on the current sticky mode. */ get stickyTop() { switch (this.state.stickyMode) { case 'pin-bottom': return this.pinTop; case 'stick': const offsetTop = this.props.stickyHeaders && this.props.stickyHeaders.offsetTop; if (typeof offsetTop === 'number' && this.shouldAddOverflowParentOffsetTop_DO_NOT_USE) { const overflowParentOffsetTop = this.overflowParent ? this.overflowParent.top : 0; return offsetTop + overflowParentOffsetTop; } else { return offsetTop; } default: return undefined; } } /** * Renders the table container, sticky header, table content, sticky scrollbar, and synced block borders. * * @example */ render() { var _this$tableRef$curren; const { isNumberColumnEnabled, layout, columnWidths, stickyHeaders, tableNode, rendererAppearance, isInsideOfBlockNode, isInsideOfTable, isinsideMultiBodiedExtension, allowTableAlignment, allowTableResizing, isPresentational, allowFixedColumnWidthOption } = this.props; const { stickyMode } = this.state; const lineLengthFixedWidth = akEditorDefaultLayoutWidth; let updatedLayout; const fullPageRendererWidthCSS = editorExperiment('platform_editor_preview_panel_responsiveness', true, { exposure: true }) ? 'calc(100cqw - var(--ak-renderer--full-page-gutter) * 2)' : `100cqw - ${FullPagePadding}px * 2`; const renderWidthCSS = rendererAppearance === 'full-page' ? fullPageRendererWidthCSS : `100cqw`; const calcDefaultLayoutWidthByAppearance = (rendererAppearance, tableNode) => { if (rendererAppearance === 'max' && !(tableNode !== null && tableNode !== void 0 && tableNode.attrs.width) && (expValEquals('editor_tinymce_full_width_mode', 'isEnabled', true) || expValEquals('confluence_max_width_content_appearance', 'isEnabled', true))) { return `min(${akEditorMaxWidthLayoutWidth}px, ${renderWidthCSS})`; } else if (rendererAppearance === 'full-width' && !(tableNode !== null && tableNode !== void 0 && tableNode.attrs.width)) { return `min(${akEditorFullWidthLayoutWidth}px, ${renderWidthCSS})`; } else if (rendererAppearance === 'comment' && allowTableResizing && !(tableNode !== null && tableNode !== void 0 && tableNode.attrs.width)) { return renderWidthCSS; } else { // custom width, or width mapped to breakpoint const tableContainerWidth = getTableContainerWidth(tableNode); return `min(${tableContainerWidth}px, ${renderWidthCSS})`; } }; const tableWidthCSS = calcDefaultLayoutWidthByAppearance(rendererAppearance, tableNode); // Logic for table alignment in renderer const isTableAlignStart = tableNode && tableNode.attrs && tableNode.attrs.layout === 'align-start' && allowTableAlignment; const fullWidthLineLengthCSS = `min(${akEditorFullWidthLayoutWidth}px, ${renderWidthCSS})`; const maxWidthLineLengthCSS = `min(${akEditorMaxWidthLayoutWidth}px, ${renderWidthCSS})`; const isCommentAppearanceAndTableAlignmentEnabled = isCommentAppearance(rendererAppearance) && allowTableAlignment; const lineLengthCSS = isFullWidthAppearance(rendererAppearance) ? fullWidthLineLengthCSS : isMaxWidthAppearance(rendererAppearance) && (expValEquals('editor_tinymce_full_width_mode', 'isEnabled', true) || expValEquals('confluence_max_width_content_appearance', 'isEnabled', true)) ? maxWidthLineLengthCSS : isCommentAppearanceAndTableAlignmentEnabled ? renderWidthCSS : `${lineLengthFixedWidth}px`; const tableWidthNew = getTableContainerWidth(tableNode); const shouldCalculateLeftForAlignment = !isInsideOfBlockNode && !isInsideOfTable && isTableAlignStart && (isFullPageAppearance(rendererAppearance) && tableWidthNew <= lineLengthFixedWidth || isFullWidthAppearance(rendererAppearance) || isMaxWidthAppearance(rendererAppearance) && (expValEquals('editor_tinymce_full_width_mode', 'isEnabled', true) || expValEquals('confluence_max_width_content_appearance', 'isEnabled', true)) || isCommentAppearanceAndTableAlignmentEnabled); let leftCSS; if (shouldCalculateLeftForAlignment) { leftCSS = `(${tableWidthCSS} - ${lineLengthCSS}) / 2`; } if (!shouldCalculateLeftForAlignment && isFullPageAppearance(rendererAppearance)) { // Note tableWidthCSS here is the renderer width // When the screen is super wide we want table to break out. // However if screen is smaller than 760px. We want table align to left. leftCSS = `min(0px, ${lineLengthCSS} - ${tableWidthCSS}) / 2`; } const children = React.Children.toArray(this.props.children); // Historically, tables in the full-width renderer had their layout set to 'default' which is deceiving. // This check caters for those tables and helps with SSR logic const isFullWidth = !(tableNode !== null && tableNode !== void 0 && tableNode.attrs.width) && rendererAppearance === 'full-width' && layout !== 'full-width'; if (isFullWidth) { updatedLayout = 'full-width'; // if table has width explicity set, ensure SSR is handled } else if (tableNode !== null && tableNode !== void 0 && tableNode.attrs.width) { updatedLayout = 'custom'; } else { updatedLayout = layout; } let finalTableContainerWidth = allowTableResizing ? tableWidthNew : 'inherit'; // We can only use CSS to determine the width when we have a known width in container. // When appearance is full-page, full-width or comment we use CSS based width calculation. // Otherwise it's fixed table width (customized width) or inherit. if (rendererAppearance === 'full-page' || rendererAppearance === 'full-width' || rendererAppearance === 'max' && (expValEquals('editor_tinymce_full_width_mode', 'isEnabled', true) || expValEquals('confluence_max_width_content_appearance', 'isEnabled', true))) { finalTableContainerWidth = allowTableResizing ? `calc(${tableWidthCSS})` : 'inherit'; } if (rendererAppearance === 'comment' && allowTableResizing && !allowTableAlignment) { // If table alignment is disabled and table width is akEditorDefaultLayoutWidth = 760, // it is most likely a table created before "Support Table in Comments" FF was enabled // and we would see a bug ED-24795. A table created before "Support Table in Comments", // should inhirit the width of the renderer container. // !NOTE: it a table resized to 760 is copied from 'full-page' editor and pasted in comment editor // where (allowTableResizing && !allowTableAlignment), the table will loose 760px width. finalTableContainerWidth = tableNode !== null && tableNode !== void 0 && tableNode.attrs.width && (tableNode === null || tableNode === void 0 ? void 0 : tableNode.attrs.width) !== akEditorDefaultLayoutWidth ? `calc(${tableWidthCSS})` : 'inherit'; } if (rendererAppearance === 'comment' && allowTableResizing && allowTableAlignment) { // If table alignment is enabled and layout is not 'align-start' or 'center', we are loading a table that was // created before "Support Table in Comments" FF was enabled. So the table should have the same width as renderer container // instead of 760 that was set on tableNode when the table had been published. finalTableContainerWidth = ((tableNode === null || tableNode === void 0 ? void 0 : tableNode.attrs.layout) === 'align-start' || (tableNode === null || tableNode === void 0 ? void 0 : tableNode.attrs.layout) === 'center') && tableNode !== null && tableNode !== void 0 && tableNode.attrs.width ? `calc(${tableWidthCSS})` : 'inherit'; } const isContentModeTable = isTableInContentMode({ tableNode, isSupported: isContentModeSupported({ allowTableResizing, rendererAppearance }), isTableNested: isInsideOfBlockNode || isInsideOfTable }) && expValEquals('platform_editor_table_fit_to_content_auto_convert', 'isEnabled', true); const style = { ...(isContentModeTable && { '--renderer-table-max-width': renderWidthCSS }), width: finalTableContainerWidth, left: leftCSS ? `calc(${leftCSS})` : undefined, marginLeft: shouldCalculateLeftForAlignment && leftCSS !== undefined ? `calc(-1 * (${leftCSS}))` : undefined }; // Store computed style values for applyNestedRendererTableFix() to use. // Reading from props rather than the DOM ensures correctness when React // removes properties (transitions from set to undefined) between renders. this.lastComputedStyle = { width: typeof style.width === 'number' ? `${style.width}px` : style.width, left: style.left, marginLeft: style.marginLeft }; return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 className: `${TableSharedCssClassName.TABLE_CONTAINER} ${this.props.shadowClassNames || ''}`, "data-layout": updatedLayout, "data-testid": "table-container", ref: this.setContainerRef, style: style // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 }, isStickyScrollbarEnabled(this.props.rendererAppearance) && /*#__PURE__*/React.createElement("div", { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 className: TableSharedCssClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_TOP, "data-testid": "sticky-scrollbar-sentinel-top" }), stickyHeaders && tableNode && !isContentModeTable && tableCanBeSticky(tableNode, children) && /*#__PURE__*/React.createElement(StickyTable, { isNumberColumnEnabled: isNumberColumnEnabled, tableWidth: "inherit", renderWidth: 0, layout: layout, handleRef: this.props.handleRef, shadowClassNames: this.props.shadowClassNames, top: this.stickyTop, mode: stickyMode, innerRef: this.stickyWrapperRef, wrapperWidth: this.state.wrapperWidth, columnWidths: columnWidths, rowHeight: this.state.headerRowHeight, tableNode: tableNode, rendererAppearance: rendererAppearance, allowTableResizing: allowTableResizing, fixTableSSRResizing: true, allowFixedColumnWidthOption: allowFixedColumnWidthOption }, [children && children[0]]), /*#__PURE__*/React.createElement("div", { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 className: TableSharedCssClassName.TABLE_NODE_WRAPPER, ref: this.wrapperRef, "data-number-column": tableNode === null || tableNode === void 0 ? void 0 : tableNode.attrs.isNumberColumnEnabled, "data-layout": tableNode === null || tableNode === void 0 ? void 0 : tableNode.attrs.layout, "data-autosize": tableNode === null || tableNode === void 0 ? void 0 : tableNode.attrs.__autoSize, "data-table-local-id": tableNode === null || tableNode === void 0 ? void 0 : tableNode.attrs.localId, "data-table-width": tableNode === null || tableNode === void 0 ? void 0 : tableNode.attrs.width, "data-vc": "table-node-wrapper", onScroll: this.props.stickyHeaders && this.onWrapperScrolled }, /*#__PURE__*/React.createElement(Table, { innerRef: this.tableRef, columnWidths: columnWidths, layout: layout, renderWidth: 0, isNumberColumnEnabled: isNumberColumnEnabled, tableNode: tableNode, rendererAppearance: rendererAppearance, isInsideOfBlockNode: isInsideOfBlockNode, isInsideOfTable: isInsideOfTable, isinsideMultiBodiedExtension: isinsideMultiBodiedExtension, allowTableResizing: allowTableResizing, isPresentational: isPresentational, allowFixedColumnWidthOption: allowFixedColumnWidthOption }, this.grabFirstRowRef(children))), isStickyScrollbarEnabled(this.props.rendererAppearance) && /*#__PURE__*/React.createElement("div", { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 className: `${TableSharedCssClassName.TABLE_STICKY_SCROLLBAR_CONTAINER}${fg('confluence_frontend_table_scrollbar_ttvc_fix') ? '-view-page' : ''}`, ref: this.stickyScrollbarRef, "data-vc": "table-sticky-scrollbar-container", style: fg('confluence_frontend_table_scrollbar_ttvc_fix') ? { ...stickyContainerBaseStyles, ...stickyContainerAdditionalStyles } : stickyContainerBaseStyles }, /*#__PURE__*/React.createElement("div", { style: { width: fg('confluence_frontend_table_scrollbar_ttvc_fix') ? '100%' : (_this$tableRef$curren = this.tableRef.current) === null || _this$tableRef$curren === void 0 ? void 0 : _this$tableRef$curren.clientWidth, // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 height: '100%' } })), isStickyScrollbarEnabled(this.props.rendererAppearance) && /*#__PURE__*/React.createElement("div", { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 className: TableSharedCssClassName.TABLE_STICKY_SCROLLBAR_SENTINEL_BOTTOM, "data-testid": "sticky-scrollbar-sentinel-bottom" }), /*#__PURE__*/React.createElement(RefSyncBlockFakeBorders, { isNumberColumnEnabled: isNumberColumnEnabled }))); } } const getCellEdgePropsByCellOffset = tableNode => { const cellEdgePropsByCellOffset = new Map(); const cellRightByCellOffset = new Map(); const occupiedGrid = []; let tableWidth = 0; let cellOffset = 0; tableNode.forEach((rowNode, _rowOffset, rowIndex) => { var _occupiedGrid$rowInde; occupiedGrid[rowIndex] = (_occupiedGrid$rowInde = occupiedGrid[rowIndex]) !== null && _occupiedGrid$rowInde !== void 0 ? _occupiedGrid$rowInde : []; let columnIndex = 0; cellOffset += 1; rowNode.forEach(cellNode => { while (occupiedGrid[rowIndex][columnIndex]) { columnIndex += 1; } const colspan = cellNode.attrs.colspan || 1; const rowspan = cellNode.attrs.rowspan || 1; const cellLeft = columnIndex; const cellRight = cellLeft + colspan; const cellTop = rowIndex; const cellBottom = cellTop + rowspan; for (let row = cellTop; row < cellBottom; row += 1) { var _occupiedGrid$row; occupiedGrid[row] = (_occupiedGrid$row = occupiedGrid[row]) !== null && _occupiedGrid$row !== void 0 ? _occupiedGrid$row : []; for (let column = cellLeft; column < cellRight; column += 1) { occupiedGrid[row][column] = true; } } tableWidth = Math.max(tableWidth, cellRight); cellRightByCellOffset.set(cellOffset, cellRight); cellEdgePropsByCellOffset.set(cellOffset, { reachesBottom: cellBottom >= tableNode.childCount, reachesLeft: cellLeft === 0, reachesRight: false, reachesTop: cellTop === 0 }); columnIndex = cellRight; cellOffset += cellNode.nodeSize; }); cellOffset += 1; }); cellRightByCellOffset.forEach((cellRight, currentCellOffset) => { const edgeProps = cellEdgePropsByCellOffset.get(currentCellOffset); if (edgeProps) { edgeProps.reachesRight = cellRight >= tableWidth; } }); return cellEdgePropsByCellOffset; }; const addTableCellEdgeProps = (rows, tableNode) => { try { if (!tableNode) { return rows; } const cellEdgePropsByCellOffset = getCellEdgePropsByCellOffset(tableNode); let cellOffset = 0; return React.Children.map(rows, (row, rowIndex) => { const rowNode = tableNode.child(rowIndex); cellOffset += 1; let cellIndex = 0; const rowChildren = React.Children.map(row.props.children, child => { if (! /*#__PURE__*/React.isValidElement(child)) { return child; } const cellNode = rowNode.child(cellIndex); const edgeProps = cellEdgePropsByCellOffset.get(cellOffset); cellIndex += 1; cellOffset += cellNode.nodeSize; return edgeProps ? /*#__PURE__*/React.cloneElement(child, edgeProps) : child; }); cellOffset += 1; return /*#__PURE__*/React.cloneElement(row, undefined, rowChildren); }); } catch { // Renderer can receive malformed historical ADF. If the table shape cannot // be described safely, keep rendering without rounded edge metadata. return rows; } }; /** * Processes table children before passing them to the styled table container. */ // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/react/no-class-components export class TableProcessorWithContainerStyles extends React.Component { constructor(...args) { super(...args); _defineProperty(this, "state", { tableOrderStatus: undefined }); // adds sortable + re-orders children _defineProperty(this, "addSortableColumn", childrenArray => { const { tableNode, allowColumnSorting, smartCardStorage } = this.props; const { tableOrderStatus } = this.state; if (allowColumnSorting && isHeaderRowEnabled(childrenArray) && tableNode && !hasMergedCell(tableNode)) { return addSortableColumn(orderChildren(childrenArray, tableNode, smartCardStorage, tableOrderStatus), tableOrderStatus, this.changeSortOrder); } return childrenArray; }); _defineProperty(this, "changeSortOrder", (columnIndex, sortOrder) => { this.setState({ tableOrderStatus: { columnIndex, order: sortOrder } }); }); // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any _defineProperty(this, "addNumberColumnIndexes", rows => { const { isNumberColumnEnabled } = this.props; const headerRowEnabled = isHeaderRowEnabled(rows); return React.Children.map(rows, (row, index) => { return /*#__PURE__*/React.cloneElement(React.Children.only(row), { isNumberColumnEnabled, index: headerRowEnabled ? index === 0 ? '' : index : index + 1 }); }); }); } /** * Renders processed table children inside the table container. * * @example */ render() { const { allowColumnSorting, allowFixedColumnWidthOption, allowTableAlignment, allowTableResizing, children, columnWidths, disableTableOverflowShadow, handleRef, isinsideMultiBodiedExtension, isInsideOfBlockNode, isInsideOfTable, isNumberColumnEnabled, isPresentational, layout, rendererAppearance, renderWidth, shadowClassNames, smartCardStorage, stickyHeaders, tabIndex, tableNode } = this.props; if (!children) { return null; } const childrenArray = React.Children.toArray(children); const childrenWithTableEdgeProps = expValEquals('platform_editor_table_q4_loveability', 'isEnabled', true) ? addTableCellEdgeProps(childrenArray, tableNode) : childrenArray; const orderedChildren = compose(this.addNumberColumnIndexes, this.addSortableColumn // @ts-expect-error TS2345: Argument of type '(ReactChild | ReactFragment | ReactPortal)[]' is not assignable to parameter of type 'ReactElement<any, string | JSXElementConstructor<any>>[]' )(childrenWithTableEdgeProps); return /*#__PURE__*/React.createElement(TableContainer, { allowColumnSorting: allowColumnSorting, allowFixedColumnWidthOption: allowFixedColumnWidthOption, allowTableAlignment: allowTableAlignment, allowTableResizing: allowTableResizing, columnWidths: columnWidths, disableTableOverflowShadow: disableTableOverflowShadow, handleRef: handleRef, isinsideMultiBodiedExtension: isinsideMultiBodiedExtension, isInsideOfBlockNode: isInsideOfBlockNode, isInsideOfTable: isInsideOfTable, isNumberColumnEnabled: isNumberColumnEnabled, isPresentational: isPresentational, layout: layout, rendererAppearance: rendererAppearance, renderWidth: renderWidth, shadowClassNames: shadowClassNames, smartCardStorage: smartCardStorage, stickyHeaders: stickyHeaders, tabIndex: tabIndex, tableNode: tableNode }, orderedChildren); } }