@atlaskit/renderer
Version:
Renderer component
391 lines (373 loc) • 20.8 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import React, { useContext } from 'react';
import { tableCellBorderWidth, tableCellMinWidth } from '@atlaskit/editor-common/styles';
import { WidthContext } from '@atlaskit/editor-common/ui';
import { akEditorTableNumberColumnWidth, akEditorTableLegacyCellMinWidth, akEditorTableCellMinWidth } from '@atlaskit/editor-shared-styles';
import { getTableContainerWidth } from '@atlaskit/editor-common/node-width';
import { useFeatureFlags } from '../../../use-feature-flags';
import { useRendererContext } from '../../../renderer-context';
import { fg } from '@atlaskit/platform-feature-flags';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
// we allow scaling down column widths by no more than 30%
// this intends to reduce unwanted scrolling in the Renderer in these scenarios:
// User A creates a table with column widths → User B views it on a smaller screen
// User A creates a table with column widths → User A views it with reduced viewport space (eg. Confluence sidebar is open)
var MAX_SCALING_PERCENT = 0.3;
var MAX_SCALING_PERCENT_TABLES_WITH_FIXED_COLUMN_WIDTHS_OPTION = 0.4;
var isTableColumnResized = function isTableColumnResized(columnWidths) {
var filteredWidths = columnWidths.filter(function (width) {
return width !== 0;
});
return !!filteredWidths.length;
};
var fixColumnWidth = function fixColumnWidth(_ref) {
var columnWidth = _ref.columnWidth,
zeroWidthColumnsCount = _ref.zeroWidthColumnsCount,
scaleDownPercent = _ref.scaleDownPercent,
skipMinWidth = _ref.skipMinWidth;
if (columnWidth === 0) {
return columnWidth;
}
// If the tables total width (including no zero widths col or cols without width) is less than the current layout
// We scale up the columns to meet the minimum of the table layout.
if (zeroWidthColumnsCount === 0 && scaleDownPercent) {
var scaled = Math.floor((1 - scaleDownPercent) * columnWidth);
return skipMinWidth ? scaled : Math.max(scaled, tableCellMinWidth);
}
var adjusted = columnWidth - tableCellBorderWidth;
if (skipMinWidth) {
return adjusted;
}
return Math.max(adjusted, zeroWidthColumnsCount ? akEditorTableLegacyCellMinWidth : tableCellMinWidth);
};
var calcScalePercent = function calcScalePercent(_ref2) {
var renderWidth = _ref2.renderWidth,
tableWidth = _ref2.tableWidth,
maxScale = _ref2.maxScale,
isNumberColumnEnabled = _ref2.isNumberColumnEnabled;
var noNumColumnScalePercent = renderWidth / tableWidth;
// when numbered column is enabled, we need to calculate the scale percent without the akEditorTableNumberColumnWidth
// As numbered column width is not scaled down
var numColumnScalePercent = (renderWidth - akEditorTableNumberColumnWidth) / (tableWidth - akEditorTableNumberColumnWidth);
var diffPercent = 1 - noNumColumnScalePercent;
return diffPercent < maxScale ? isNumberColumnEnabled ? 1 - numColumnScalePercent : diffPercent : maxScale;
};
export var colWidthSum = function colWidthSum(columnWidths) {
return columnWidths.reduce(function (prev, curr) {
return curr + prev;
}, 0);
};
/**
* Returns the data-column available width: total width minus the fixed number column if enabled.
*/
var getDataColumnWidth = function getDataColumnWidth(totalWidth, isNumberColumnEnabled) {
return isNumberColumnEnabled ? totalWidth - akEditorTableNumberColumnWidth : totalWidth;
};
/**
* Scales column widths proportionally to fit within availableWidth, matching the editor's
* scaleTableTo(): floors each column to the nearest pixel, then redistributes any rounding
* remainder to the first column that can absorb it without going below tableCellMinWidth.
*/
var scaleColumnsToWidth = function scaleColumnsToWidth(columnWidths, availableWidth) {
var rawTotalWidth = columnWidths.reduce(function (sum, w) {
return sum + w;
}, 0);
var scaleFactor = availableWidth / rawTotalWidth;
var scaledWidths = columnWidths.map(function (colWidth) {
return Math.max(Math.floor(colWidth * scaleFactor), tableCellMinWidth);
});
var totalScaled = scaledWidths.reduce(function (sum, w) {
return sum + w;
}, 0);
var diff = availableWidth - totalScaled;
if (diff !== 0 && Math.abs(diff) < tableCellMinWidth) {
for (var i = 0; i < scaledWidths.length; i++) {
if (scaledWidths[i] + diff > tableCellMinWidth) {
scaledWidths[i] += diff;
break;
}
}
}
return scaledWidths.map(function (width) {
return {
width: "".concat(width, "px")
};
});
};
/**
* Computes column widths for tables inside sync blocks, matching the editor's scaleTableTo() exactly.
* Returns null if not inside a sync block.
*
* For nested tables (isInsideOfTable=true), we use getTableContainerWidth(tableNode) as the
* reference — the width the editor saved, which already accounts for the parent cell's available
* space (colwidth minus tableCellPadding * 2). This matches bodiedSyncBlock where isRendererNested=false,
* so renderScaleDownColgroup uses getTableContainerWidth(tableNode). For syncBlock the nested
* renderer has isRendererNested=true, which incorrectly overrides tableContainerWidth with renderWidth
* (the full container), causing overflow by 2 * tableCellPadding (16px).
*/
var renderSyncBlockColgroup = function renderSyncBlockColgroup(_ref3) {
var isInsideOfSyncBlock = _ref3.isInsideOfSyncBlock,
isInsideOfTable = _ref3.isInsideOfTable,
tableNode = _ref3.tableNode,
columnWidths = _ref3.columnWidths,
isNumberColumnEnabled = _ref3.isNumberColumnEnabled,
renderWidthProp = _ref3.renderWidth,
contextWidth = _ref3.contextWidth;
if (!isInsideOfSyncBlock) {
return null;
}
var rawTotalWidth = columnWidths.reduce(function (sum, w) {
return sum + w;
}, 0);
if (isInsideOfTable) {
return null;
}
// SSR / first client render before WidthObserver measures. Output % of original ADF
// proportions so columns are stable — the CSS container query (100cqw) handles actual
// scaling width. We key off renderWidthProp alone because WidthProvider initialises with
// document.body.offsetWidth (non-zero on the client), so contextWidth > 0 even before
// WidthObserver has measured the real container, which would skip this branch and produce
// column widths scaled to the full viewport.
if (renderWidthProp <= 0) {
var fullTableWidth = isNumberColumnEnabled ? rawTotalWidth + akEditorTableNumberColumnWidth : rawTotalWidth;
return columnWidths.map(function (colWidth) {
return {
width: "".concat(colWidth / fullTableWidth * 100, "%")
};
});
}
// contextWidth measures the sync block content area. Subtract 2 to match the editor's
// getParentNodeWidth() border offset. Fall back to renderWidthProp for the non-CSS path.
var effectiveRenderWidth = contextWidth > 0 ? contextWidth - 2 : renderWidthProp;
var availableWidth = getDataColumnWidth(effectiveRenderWidth, isNumberColumnEnabled);
// Only scale down if the table is actually wider than the available space.
// If the table fits, return original widths with the same tableCellBorderWidth (2px) adjustment
// that renderScaleDownColgroup applies via fixColumnWidth's no-scale path.
if (availableWidth >= rawTotalWidth) {
return columnWidths.map(function (colWidth) {
return {
width: "".concat(Math.max(colWidth - tableCellBorderWidth, tableCellMinWidth), "px")
};
});
}
// Cap scaling at MAX_SCALING_PERCENT (30%), matching renderScaleDownColgroup's calcScalePercent.
//
// Both `availableWidth` and `rawTotalWidth` are already in data-column space (number column
// excluded via getDataColumnWidth), so `diffPercent = 1 - availableWidth / rawTotalWidth` is
// equivalent to calcScalePercent's `numColumnScalePercent` path when isNumberColumnEnabled,
// and to the `noNumColumnScalePercent` path otherwise. We express the cap as a target width
// (clampedAvailableWidth) rather than a scale-down percentage, but the result is the same:
// columns are scaled by at most MAX_SCALING_PERCENT (30%).
var diffPercent = 1 - availableWidth / rawTotalWidth;
var clampedAvailableWidth = diffPercent <= MAX_SCALING_PERCENT ? availableWidth : Math.floor(rawTotalWidth * (1 - MAX_SCALING_PERCENT));
return scaleColumnsToWidth(columnWidths, clampedAvailableWidth);
};
var renderScaleDownColgroup = function renderScaleDownColgroup(props) {
var _props$tableNode;
var columnWidths = props.columnWidths,
isNumberColumnEnabled = props.isNumberColumnEnabled,
renderWidth = props.renderWidth,
tableNode = props.tableNode,
rendererAppearance = props.rendererAppearance,
isInsideOfBlockNode = props.isInsideOfBlockNode,
isInsideOfTable = props.isInsideOfTable,
isinsideMultiBodiedExtension = props.isinsideMultiBodiedExtension,
isTableScalingEnabled = props.isTableScalingEnabled,
isTableFixedColumnWidthsOptionEnabled = props.isTableFixedColumnWidthsOptionEnabled,
allowTableResizing = props.allowTableResizing,
isTopLevelRenderer = props.isTopLevelRenderer,
isInsideOfSyncBlock = props.isInsideOfSyncBlock;
var skipMinWidth = !!(isInsideOfTable && isInsideOfSyncBlock);
if (!columnWidths || columnWidths.every(function (width) {
return width === 0;
}) && fg('platform_editor_numbered_column_in_include')) {
return [];
}
var tableColumnResized = isTableColumnResized(columnWidths);
var noOfColumns = columnWidths.length;
var targetWidths;
// This is a fix for ED-23259
// Some extensions (for ex: Page Properties or Excerpt) do not renderer tables directly inside themselves. They use ReactRenderer.
// So if we add a check like isInsideExtension (similar to exising isInsideBlockNode), it will fail, and to the only way to learn
// if the table is rendered inside another node, is to check if the Renderer itself is nested.
var isRendererNested = isTopLevelRenderer === false;
// appearance == comment && allowTableResizing && !tableNode?.attrs.width, means it is a comment
// appearance == comment && !allowTableResizing && !tableNode?.attrs.width, means it is a inline comment
// When comment and inline comment table width inherits from the parent container, we want tableContainerWidth === renderWidth
// Tables with numbered columns inside of extensions cannot use the renderWidth when it is 0. In this case, it should
// explicitly use the width coming from the table node as the final table container width.
var tableContainerWidth = rendererAppearance === 'comment' && !(tableNode !== null && tableNode !== void 0 && tableNode.attrs.width) || isRendererNested && (!fg('platform_fix_nested_num_column_scaling') || !isNumberColumnEnabled) ? renderWidth : getTableContainerWidth(tableNode);
if (allowTableResizing && !isInsideOfBlockNode && !isInsideOfTable && !isinsideMultiBodiedExtension && !tableColumnResized) {
// when no columns are resized, each column should have equal width, equals to tableWidth / noOfColumns
var _tableWidth = (isNumberColumnEnabled ? tableContainerWidth - akEditorTableNumberColumnWidth : tableContainerWidth) - 1;
var defaultColumnWidth = _tableWidth / noOfColumns;
targetWidths = new Array(noOfColumns).fill(defaultColumnWidth);
} else if (!tableColumnResized) {
return null;
}
var sumOfColumns = colWidthSum(columnWidths);
// tables in the wild may be smaller than table container width (col resizing bugs, created before custom widths etc.)
// this causes issues with num column scaling as we add a new table column in renderer
var isTableSmallerThanContainer = sumOfColumns < tableContainerWidth - 1;
var forceScaleForNumColumn = isTableScalingEnabled && isNumberColumnEnabled && tableColumnResized;
// when table resized and number column is enabled, we need to scale down the table in render
if (forceScaleForNumColumn) {
var calculatedTableContainerWidth = rendererAppearance === 'comment' ? sumOfColumns : tableContainerWidth;
var scalePercentage = +((calculatedTableContainerWidth - akEditorTableNumberColumnWidth) / calculatedTableContainerWidth);
var targetMaxWidth = calculatedTableContainerWidth - akEditorTableNumberColumnWidth;
var totalWidthAfterScale = 0;
var newScaledTargetWidths = columnWidths.map(function (width) {
// we need to scale each column UP, to ensure total width of table matches table container
var patchedWidth = isTableSmallerThanContainer ? width / sumOfColumns * (calculatedTableContainerWidth - 1) : width;
var newWidth = Math.floor(patchedWidth * scalePercentage);
totalWidthAfterScale += newWidth;
return newWidth;
});
var diff = targetMaxWidth - totalWidthAfterScale;
targetWidths = newScaledTargetWidths;
if (diff > 0 || diff < 0 && Math.abs(diff) < tableCellMinWidth) {
var updated = false;
targetWidths = targetWidths.map(function (width) {
if (!updated && width + diff > tableCellMinWidth) {
updated = true;
width += diff;
}
return width;
});
}
}
targetWidths = targetWidths || columnWidths;
// @see ED-6056
var maxTableWidth = renderWidth < tableContainerWidth ? renderWidth : tableContainerWidth;
var tableWidth = isNumberColumnEnabled ? akEditorTableNumberColumnWidth : 0;
var minTableWidth = tableWidth;
var zeroWidthColumnsCount = 0;
targetWidths.forEach(function (width) {
if (width) {
tableWidth += Math.ceil(width);
} else {
zeroWidthColumnsCount += 1;
}
minTableWidth += Math.ceil(width) || akEditorTableLegacyCellMinWidth;
});
var cellMinWidth = 0;
var scaleDownPercent = 0;
var isTableScalingWithFixedColumnWidthsOptionEnabled = isTableScalingEnabled && isTableFixedColumnWidthsOptionEnabled;
var isTableWidthFixed = isTableScalingWithFixedColumnWidthsOptionEnabled && ((_props$tableNode = props.tableNode) === null || _props$tableNode === void 0 ? void 0 : _props$tableNode.attrs.displayMode) === 'fixed';
var maxScalingPercent = isTableScalingWithFixedColumnWidthsOptionEnabled || isTableScalingEnabled && rendererAppearance === 'comment' ? MAX_SCALING_PERCENT_TABLES_WITH_FIXED_COLUMN_WIDTHS_OPTION : MAX_SCALING_PERCENT;
// fixes migration tables with zero-width columns
if (zeroWidthColumnsCount > 0) {
if (minTableWidth > maxTableWidth) {
var minWidth = Math.ceil((maxTableWidth - tableWidth) / zeroWidthColumnsCount);
cellMinWidth = minWidth < akEditorTableLegacyCellMinWidth ? akEditorTableLegacyCellMinWidth : minWidth;
}
}
// scaling down
else if (renderWidth < tableWidth && !isTableWidthFixed) {
var shouldTable100ScaleDown = rendererAppearance === 'comment' && allowTableResizing && !(tableNode !== null && tableNode !== void 0 && tableNode.attrs.width);
scaleDownPercent = calcScalePercent({
renderWidth: renderWidth,
tableWidth: tableWidth,
maxScale: fg('platform-ssr-table-resize') ? maxScalingPercent : shouldTable100ScaleDown ? 1 : maxScalingPercent,
isNumberColumnEnabled: isNumberColumnEnabled
});
}
if (isNumberColumnEnabled && (tableWidth < maxTableWidth || maxTableWidth === 0)) {
var fixedColWidths = targetWidths.map(function (width) {
return fixColumnWidth({
columnWidth: width,
zeroWidthColumnsCount: zeroWidthColumnsCount,
scaleDownPercent: scaleDownPercent,
skipMinWidth: skipMinWidth
}) || cellMinWidth;
});
var sumFixedColumnWidths = colWidthSum(fixedColWidths);
return fixedColWidths.map(function (colWidth) {
var width = Math.max(colWidth, cellMinWidth);
if (colWidth > akEditorTableCellMinWidth) {
// To make sure the numbered column isn't scaled, use
// percentages for the other columns.
// Calculate the percentage based on the sum of the fixed column widths
return {
width: "".concat(width / sumFixedColumnWidths * 100, "%")
};
} else {
// If the column is equal to or less than the minimum cell width, we need to return a fixed pixel width.
// This is to prevent columns being scaled below the minimum cell width.
var style = width ? {
width: "".concat(width, "px")
} : {};
return style;
}
});
}
return targetWidths.map(function (colWidth) {
var width = fixColumnWidth({
columnWidth: colWidth,
zeroWidthColumnsCount: zeroWidthColumnsCount,
scaleDownPercent: scaleDownPercent,
skipMinWidth: skipMinWidth
}) || cellMinWidth;
var style = width ? {
width: "".concat(width, "px")
} : {};
return style;
});
};
export var Colgroup = function Colgroup(props) {
var _ref4, _renderSyncBlockColgr;
var _useRendererContext = useRendererContext(),
isTopLevelRenderer = _useRendererContext.isTopLevelRenderer,
nestedRendererType = _useRendererContext.nestedRendererType;
var columnWidths = props.columnWidths,
isNumberColumnEnabled = props.isNumberColumnEnabled;
var _useContext = useContext(WidthContext),
contextWidth = _useContext.width;
var flags = useFeatureFlags();
if (!columnWidths) {
return null;
}
var isTableFixedColumnWidthsOptionEnabled = (_ref4 = fg('platform_editor_table_fixed_column_width_prop') ? props.allowFixedColumnWidthOption : flags && 'tableWithFixedColumnWidthsOption' in flags && flags.tableWithFixedColumnWidthsOption) !== null && _ref4 !== void 0 ? _ref4 : false;
// For referenced sync blocks, nestedRendererType='syncedBlock' is set via RendererContextProvider
// in AKRendererWrapper. ReactSerializer is a class and cannot read React context, so we detect
// it here in the Colgroup component via useRendererContext() instead of prop-drilling.
var isInsideOfSyncBlock = nestedRendererType === 'syncedBlock';
// renderSyncBlockColgroup returns null when not applicable (flag off, SSR, not a sync block),
// in which case ?? falls back to the standard renderScaleDownColgroup path.
var colStyles = (_renderSyncBlockColgr = renderSyncBlockColgroup({
isInsideOfSyncBlock: isInsideOfSyncBlock,
isInsideOfTable: !!props.isInsideOfTable,
tableNode: props.tableNode,
columnWidths: columnWidths,
isNumberColumnEnabled: !!isNumberColumnEnabled,
renderWidth: props.renderWidth,
contextWidth: contextWidth
})) !== null && _renderSyncBlockColgr !== void 0 ? _renderSyncBlockColgr : renderScaleDownColgroup(_objectSpread(_objectSpread({}, props), {}, {
isTopLevelRenderer: isTopLevelRenderer,
isInsideOfSyncBlock: isInsideOfSyncBlock,
isTableScalingEnabled: props.rendererAppearance === 'full-page' || props.rendererAppearance === 'full-width' || props.rendererAppearance === 'max' && (expValEquals('editor_tinymce_full_width_mode', 'isEnabled', true) || expValEquals('confluence_max_width_content_appearance', 'isEnabled', true)) || props.rendererAppearance === 'comment',
isTableFixedColumnWidthsOptionEnabled: isTableFixedColumnWidthsOptionEnabled && (props.rendererAppearance === 'full-page' || props.rendererAppearance === 'full-width' || props.rendererAppearance === 'max' && (expValEquals('editor_tinymce_full_width_mode', 'isEnabled', true) || expValEquals('confluence_max_width_content_appearance', 'isEnabled', true)))
}));
if (!colStyles) {
return null;
}
return /*#__PURE__*/React.createElement("colgroup", null, isNumberColumnEnabled && /*#__PURE__*/React.createElement("col", {
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop, @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766
style: {
width: akEditorTableNumberColumnWidth
},
"data-test-id": 'num'
}), colStyles.map(function (style, idx) {
return (
/*#__PURE__*/
// Ignored via go/ees005
// eslint-disable-next-line react/no-array-index-key, @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
React.createElement("col", {
key: idx,
style: style
})
);
}));
};