@atlaskit/renderer
Version:
Renderer component
853 lines (837 loc) • 38.6 kB
JavaScript
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);
}
}