@lexical/table 0.48.0 → 0.48.1-nightly.20260720.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LexicalTable.dev.js +311 -113
- package/dist/LexicalTable.dev.mjs +311 -115
- package/dist/LexicalTable.js.flow +5 -6
- package/dist/LexicalTable.mjs +2 -0
- package/dist/LexicalTable.node.mjs +2 -0
- package/dist/LexicalTable.prod.js +1 -1
- package/dist/LexicalTable.prod.mjs +1 -1
- package/dist/LexicalTableCellNode.d.ts +28 -5
- package/dist/LexicalTableExtension.d.ts +13 -0
- package/dist/LexicalTableNode.d.ts +30 -7
- package/dist/LexicalTableRowNode.d.ts +21 -6
- package/dist/LexicalTableSelection.d.ts +12 -0
- package/dist/LexicalTableUtils.d.ts +4 -0
- package/dist/index.d.ts +3 -2
- package/package.json +7 -7
- package/src/LexicalTableCellNode.ts +14 -29
- package/src/LexicalTableExtension.ts +100 -0
- package/src/LexicalTableNode.ts +234 -43
- package/src/LexicalTableRowNode.ts +13 -21
- package/src/LexicalTableSelection.ts +17 -7
- package/src/LexicalTableSelectionHelpers.ts +2 -62
- package/src/LexicalTableUtils.ts +4 -0
- package/src/index.ts +3 -0
package/dist/LexicalTable.dev.js
CHANGED
|
@@ -56,11 +56,20 @@ class TableCellNode extends lexical.ElementNode {
|
|
|
56
56
|
__backgroundColor;
|
|
57
57
|
/** @internal */
|
|
58
58
|
__verticalAlign;
|
|
59
|
-
|
|
60
|
-
return 'tablecell'
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
$config() {
|
|
60
|
+
return this.config('tablecell', {
|
|
61
|
+
extends: lexical.ElementNode,
|
|
62
|
+
importDOM: {
|
|
63
|
+
td: () => ({
|
|
64
|
+
conversion: $convertTableCellNodeElement,
|
|
65
|
+
priority: 0
|
|
66
|
+
}),
|
|
67
|
+
th: () => ({
|
|
68
|
+
conversion: $convertTableCellNodeElement,
|
|
69
|
+
priority: 0
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
});
|
|
64
73
|
}
|
|
65
74
|
afterCloneFrom(node) {
|
|
66
75
|
super.afterCloneFrom(node);
|
|
@@ -71,21 +80,6 @@ class TableCellNode extends lexical.ElementNode {
|
|
|
71
80
|
this.__headerState = node.__headerState;
|
|
72
81
|
this.__width = node.__width;
|
|
73
82
|
}
|
|
74
|
-
static importDOM() {
|
|
75
|
-
return {
|
|
76
|
-
td: node => ({
|
|
77
|
-
conversion: $convertTableCellNodeElement,
|
|
78
|
-
priority: 0
|
|
79
|
-
}),
|
|
80
|
-
th: node => ({
|
|
81
|
-
conversion: $convertTableCellNodeElement,
|
|
82
|
-
priority: 0
|
|
83
|
-
})
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
static importJSON(serializedNode) {
|
|
87
|
-
return $createTableCellNode().updateFromJSON(serializedNode);
|
|
88
|
-
}
|
|
89
83
|
updateFromJSON(serializedNode) {
|
|
90
84
|
return super.updateFromJSON(serializedNode).setHeaderStyles(serializedNode.headerState).setColSpan(serializedNode.colSpan || 1).setRowSpan(serializedNode.rowSpan || 1).setWidth(serializedNode.width || undefined).setBackgroundColor(serializedNode.backgroundColor || null).setVerticalAlign(serializedNode.verticalAlign || undefined);
|
|
91
85
|
}
|
|
@@ -383,31 +377,28 @@ function formatDevErrorMessage(message) {
|
|
|
383
377
|
class TableRowNode extends lexical.ElementNode {
|
|
384
378
|
/** @internal */
|
|
385
379
|
__height;
|
|
386
|
-
|
|
387
|
-
return 'tablerow'
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
380
|
+
$config() {
|
|
381
|
+
return this.config('tablerow', {
|
|
382
|
+
extends: lexical.ElementNode,
|
|
383
|
+
importDOM: {
|
|
384
|
+
tr: () => ({
|
|
385
|
+
conversion: $convertTableRowElement,
|
|
386
|
+
priority: 0
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
});
|
|
391
390
|
}
|
|
392
391
|
afterCloneFrom(prevNode) {
|
|
393
392
|
super.afterCloneFrom(prevNode);
|
|
394
393
|
this.__height = prevNode.__height;
|
|
395
394
|
}
|
|
396
|
-
static importDOM() {
|
|
397
|
-
return {
|
|
398
|
-
tr: node => ({
|
|
399
|
-
conversion: $convertTableRowElement,
|
|
400
|
-
priority: 0
|
|
401
|
-
})
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
static importJSON(serializedNode) {
|
|
405
|
-
return $createTableRowNode().updateFromJSON(serializedNode);
|
|
406
|
-
}
|
|
407
395
|
updateFromJSON(serializedNode) {
|
|
408
396
|
return super.updateFromJSON(serializedNode).setHeight(serializedNode.height);
|
|
409
397
|
}
|
|
410
|
-
|
|
398
|
+
|
|
399
|
+
// `height` carries an explicit `undefined` default so the constructor reports
|
|
400
|
+
// zero required arguments and `$config` can synthesize the static `clone`.
|
|
401
|
+
constructor(height = undefined, key) {
|
|
411
402
|
super(key);
|
|
412
403
|
this.__height = height;
|
|
413
404
|
}
|
|
@@ -1391,6 +1382,11 @@ function $computeTableCellRectSpans(map, boundary) {
|
|
|
1391
1382
|
topSpan
|
|
1392
1383
|
};
|
|
1393
1384
|
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Compute the bounding rectangle of two table cells in a table map,
|
|
1388
|
+
* expanding iteratively to include any merged cells that overlap the boundary.
|
|
1389
|
+
*/
|
|
1394
1390
|
function $computeTableCellRectBoundary(map, cellAMap, cellBMap) {
|
|
1395
1391
|
// Initial boundaries based on the anchor and focus cells
|
|
1396
1392
|
let minColumn = Math.min(cellAMap.startColumn, cellBMap.startColumn);
|
|
@@ -1899,7 +1895,7 @@ class TableSelection {
|
|
|
1899
1895
|
selection.insertNodes(nodes);
|
|
1900
1896
|
}
|
|
1901
1897
|
|
|
1902
|
-
|
|
1898
|
+
/** @deprecated Use {@link $computeTableMap} and {@link $computeTableCellRectBoundary} directly. */
|
|
1903
1899
|
getShape() {
|
|
1904
1900
|
const {
|
|
1905
1901
|
anchorCell,
|
|
@@ -2011,19 +2007,28 @@ class TableSelection {
|
|
|
2011
2007
|
return textContent;
|
|
2012
2008
|
}
|
|
2013
2009
|
}
|
|
2010
|
+
|
|
2011
|
+
/** Type guard for {@link TableSelection}. */
|
|
2014
2012
|
function $isTableSelection(x) {
|
|
2015
2013
|
return x instanceof TableSelection;
|
|
2016
2014
|
}
|
|
2015
|
+
|
|
2016
|
+
/** @deprecated Use {@link $createTableSelectionFrom} instead. */
|
|
2017
2017
|
function $createTableSelection() {
|
|
2018
|
-
// TODO this is a suboptimal design, it doesn't make sense to have
|
|
2019
|
-
// a table selection that isn't associated with a table. This
|
|
2020
|
-
// constructor should have required arguments and in __DEV__ we
|
|
2021
|
-
// should check that they point to a table and are element points to
|
|
2022
|
-
// cell nodes of that table.
|
|
2023
2018
|
const anchor = lexical.$createPoint('root', 0, 'element');
|
|
2024
2019
|
const focus = lexical.$createPoint('root', 0, 'element');
|
|
2025
2020
|
return new TableSelection('root', anchor, focus);
|
|
2026
2021
|
}
|
|
2022
|
+
|
|
2023
|
+
/**
|
|
2024
|
+
* Creates a {@link TableSelection} spanning from `anchorCell` to `focusCell`
|
|
2025
|
+
* within `tableNode`. In `__DEV__` mode, validates that both cells belong to
|
|
2026
|
+
* the given table and that the table is attached to the editor state.
|
|
2027
|
+
*
|
|
2028
|
+
* If the current selection is already a TableSelection, it clones and
|
|
2029
|
+
* re-targets it (preserving identity for dirty-checking); otherwise it
|
|
2030
|
+
* constructs a fresh one.
|
|
2031
|
+
*/
|
|
2027
2032
|
function $createTableSelectionFrom(tableNode, anchorCell, focusCell) {
|
|
2028
2033
|
const tableNodeKey = tableNode.getKey();
|
|
2029
2034
|
const anchorCellKey = anchorCell.getKey();
|
|
@@ -2040,7 +2045,7 @@ function $createTableSelectionFrom(tableNode, anchorCell, focusCell) {
|
|
|
2040
2045
|
} // TODO: Check for rectangular grid
|
|
2041
2046
|
}
|
|
2042
2047
|
const prevSelection = lexical.$getSelection();
|
|
2043
|
-
const nextSelection = $isTableSelection(prevSelection) ? prevSelection.clone() :
|
|
2048
|
+
const nextSelection = $isTableSelection(prevSelection) ? prevSelection.clone() : new TableSelection('root', lexical.$createPoint('root', 0, 'element'), lexical.$createPoint('root', 0, 'element'));
|
|
2044
2049
|
nextSelection.set(tableNode.getKey(), anchorCell.getKey(), focusCell.getKey());
|
|
2045
2050
|
return nextSelection;
|
|
2046
2051
|
}
|
|
@@ -2899,7 +2904,7 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler, tableObse
|
|
|
2899
2904
|
}
|
|
2900
2905
|
return false;
|
|
2901
2906
|
}, lexical.COMMAND_PRIORITY_HIGH));
|
|
2902
|
-
const deleteTextHandler =
|
|
2907
|
+
const $deleteTextHandler = () => {
|
|
2903
2908
|
const selection = lexical.$getSelection();
|
|
2904
2909
|
if (!$isSelectionInTable(selection, tableNode)) {
|
|
2905
2910
|
return false;
|
|
@@ -2907,34 +2912,11 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler, tableObse
|
|
|
2907
2912
|
if ($isTableSelection(selection)) {
|
|
2908
2913
|
tableObserver.$clearText();
|
|
2909
2914
|
return true;
|
|
2910
|
-
} else if (lexical.$isRangeSelection(selection)) {
|
|
2911
|
-
const tableCellNode = $findParentTableCellNodeInTable(tableNode, selection.anchor.getNode());
|
|
2912
|
-
if (!$isTableCellNode(tableCellNode)) {
|
|
2913
|
-
return false;
|
|
2914
|
-
}
|
|
2915
|
-
const anchorNode = selection.anchor.getNode();
|
|
2916
|
-
const focusNode = selection.focus.getNode();
|
|
2917
|
-
const isAnchorInside = tableNode.isParentOf(anchorNode);
|
|
2918
|
-
const isFocusInside = tableNode.isParentOf(focusNode);
|
|
2919
|
-
const selectionContainsPartialTable = isAnchorInside && !isFocusInside || isFocusInside && !isAnchorInside;
|
|
2920
|
-
if (selectionContainsPartialTable) {
|
|
2921
|
-
tableObserver.$clearText();
|
|
2922
|
-
return true;
|
|
2923
|
-
}
|
|
2924
|
-
const nearestElementNode = lexical.$findMatchingParent(selection.anchor.getNode(), n => lexical.$isElementNode(n));
|
|
2925
|
-
const topLevelCellElementNode = nearestElementNode && lexical.$findMatchingParent(nearestElementNode, n => lexical.$isElementNode(n) && $isTableCellNode(n.getParent()));
|
|
2926
|
-
if (!lexical.$isElementNode(topLevelCellElementNode) || !lexical.$isElementNode(nearestElementNode)) {
|
|
2927
|
-
return false;
|
|
2928
|
-
}
|
|
2929
|
-
if (command === lexical.DELETE_LINE_COMMAND && topLevelCellElementNode.getPreviousSibling() === null) {
|
|
2930
|
-
// TODO: Fix Delete Line in Table Cells.
|
|
2931
|
-
return true;
|
|
2932
|
-
}
|
|
2933
2915
|
}
|
|
2934
2916
|
return false;
|
|
2935
2917
|
};
|
|
2936
2918
|
for (const command of DELETE_TEXT_COMMANDS) {
|
|
2937
|
-
tableObserver.listenersToRemove.add(editor.registerCommand(command, deleteTextHandler
|
|
2919
|
+
tableObserver.listenersToRemove.add(editor.registerCommand(command, $deleteTextHandler, lexical.COMMAND_PRIORITY_HIGH));
|
|
2938
2920
|
}
|
|
2939
2921
|
const $deleteCellHandler = event => {
|
|
2940
2922
|
const selection = lexical.$getSelection();
|
|
@@ -3047,11 +3029,6 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler, tableObse
|
|
|
3047
3029
|
if ($isTableSelection(selection)) {
|
|
3048
3030
|
tableObserver.$formatCells(payload);
|
|
3049
3031
|
return true;
|
|
3050
|
-
} else if (lexical.$isRangeSelection(selection)) {
|
|
3051
|
-
const tableCellNode = lexical.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
|
|
3052
|
-
if (!$isTableCellNode(tableCellNode)) {
|
|
3053
|
-
return false;
|
|
3054
|
-
}
|
|
3055
3032
|
}
|
|
3056
3033
|
return false;
|
|
3057
3034
|
}, lexical.COMMAND_PRIORITY_HIGH));
|
|
@@ -4197,10 +4174,167 @@ function alignTableElement(dom, config, formatType) {
|
|
|
4197
4174
|
lexical.removeClassNamesFromElement(dom, ...removeClasses);
|
|
4198
4175
|
lexical.addClassNamesToElement(dom, ...addClasses);
|
|
4199
4176
|
}
|
|
4177
|
+
function $createScrollableWrapper(tableElement, config, hideNativeScrollbar) {
|
|
4178
|
+
const wrapper = lexical.$getDocument().createElement('div');
|
|
4179
|
+
const classes = config.theme.tableScrollableWrapper;
|
|
4180
|
+
if (classes) {
|
|
4181
|
+
lexical.addClassNamesToElement(wrapper, classes);
|
|
4182
|
+
} else {
|
|
4183
|
+
wrapper.style.overflowX = 'auto';
|
|
4184
|
+
}
|
|
4185
|
+
if (hideNativeScrollbar) {
|
|
4186
|
+
wrapper.style.scrollbarWidth = 'none';
|
|
4187
|
+
}
|
|
4188
|
+
wrapper.appendChild(tableElement);
|
|
4189
|
+
return wrapper;
|
|
4190
|
+
}
|
|
4191
|
+
function $createStickyScrollbar(config) {
|
|
4192
|
+
const doc = lexical.$getDocument();
|
|
4193
|
+
const scrollbar = doc.createElement('div');
|
|
4194
|
+
const classes = config.theme.tableStickyScrollbar;
|
|
4195
|
+
if (classes) {
|
|
4196
|
+
lexical.addClassNamesToElement(scrollbar, classes);
|
|
4197
|
+
} else {
|
|
4198
|
+
scrollbar.style.position = 'sticky';
|
|
4199
|
+
scrollbar.style.bottom = '0';
|
|
4200
|
+
scrollbar.style.overflowX = 'scroll';
|
|
4201
|
+
scrollbar.style.overflowY = 'hidden';
|
|
4202
|
+
}
|
|
4203
|
+
scrollbar.style.display = 'none';
|
|
4204
|
+
scrollbar.setAttribute('aria-hidden', 'true');
|
|
4205
|
+
scrollbar.tabIndex = -1;
|
|
4206
|
+
const spacer = doc.createElement('div');
|
|
4207
|
+
spacer.style.height = '1px';
|
|
4208
|
+
spacer.style.width = '0px';
|
|
4209
|
+
scrollbar.appendChild(spacer);
|
|
4210
|
+
return scrollbar;
|
|
4211
|
+
}
|
|
4212
|
+
// Unthemed sticky scrollbars whose environment cannot render a persistent
|
|
4213
|
+
// proxy scrollbar (overlay scrollbars reserve no height and show no idle
|
|
4214
|
+
// thumb), permanently hidden in favor of the wrapper's native scrollbar.
|
|
4215
|
+
const overlayStickyScrollbars = new WeakSet();
|
|
4216
|
+
function measureScrollbarThickness(scrollbar) {
|
|
4217
|
+
const prevDisplay = scrollbar.style.display;
|
|
4218
|
+
scrollbar.style.display = '';
|
|
4219
|
+
const thickness = scrollbar.offsetHeight - scrollbar.clientHeight;
|
|
4220
|
+
scrollbar.style.display = prevDisplay;
|
|
4221
|
+
return thickness;
|
|
4222
|
+
}
|
|
4223
|
+
function attachStickyScrollbarListeners(elements) {
|
|
4224
|
+
const {
|
|
4225
|
+
scrollable,
|
|
4226
|
+
scrollbar,
|
|
4227
|
+
tableElement
|
|
4228
|
+
} = elements;
|
|
4229
|
+
// A themed scrollbar (theme.tableStickyScrollbar, detectable by its
|
|
4230
|
+
// classes) is the integrator's responsibility to keep visible (e.g. via
|
|
4231
|
+
// ::-webkit-scrollbar height or scrollbar-width). The unthemed fallback
|
|
4232
|
+
// relies on classic native scrollbars for its height, so where the
|
|
4233
|
+
// environment renders overlay scrollbars (no reserved thickness — e.g.
|
|
4234
|
+
// macOS "show scrollbars when scrolling", or non-layout environments like
|
|
4235
|
+
// jsdom) the proxy would be an invisible strip: keep it hidden and
|
|
4236
|
+
// restore the wrapper's native scrollbar instead of presenting no scroll
|
|
4237
|
+
// affordance at all.
|
|
4238
|
+
if (scrollbar.classList.length === 0 && measureScrollbarThickness(scrollbar) === 0) {
|
|
4239
|
+
overlayStickyScrollbars.add(scrollbar);
|
|
4240
|
+
scrollbar.style.display = 'none';
|
|
4241
|
+
scrollable.style.scrollbarWidth = 'auto';
|
|
4242
|
+
return () => {};
|
|
4243
|
+
}
|
|
4244
|
+
const onWrapperScroll = () => {
|
|
4245
|
+
if (scrollbar.scrollLeft !== scrollable.scrollLeft) {
|
|
4246
|
+
scrollbar.scrollLeft = scrollable.scrollLeft;
|
|
4247
|
+
}
|
|
4248
|
+
};
|
|
4249
|
+
const onScrollbarScroll = () => {
|
|
4250
|
+
if (scrollable.scrollLeft !== scrollbar.scrollLeft) {
|
|
4251
|
+
scrollable.scrollLeft = scrollbar.scrollLeft;
|
|
4252
|
+
}
|
|
4253
|
+
};
|
|
4254
|
+
scrollable.addEventListener('scroll', onWrapperScroll, {
|
|
4255
|
+
passive: true
|
|
4256
|
+
});
|
|
4257
|
+
scrollbar.addEventListener('scroll', onScrollbarScroll, {
|
|
4258
|
+
passive: true
|
|
4259
|
+
});
|
|
4260
|
+
let resizeObserver = null;
|
|
4261
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
4262
|
+
resizeObserver = new ResizeObserver(() => {
|
|
4263
|
+
syncStickyScrollbar(scrollable, scrollbar);
|
|
4264
|
+
});
|
|
4265
|
+
resizeObserver.observe(scrollable);
|
|
4266
|
+
resizeObserver.observe(tableElement);
|
|
4267
|
+
}
|
|
4268
|
+
const cleanup = () => {
|
|
4269
|
+
scrollable.removeEventListener('scroll', onWrapperScroll);
|
|
4270
|
+
scrollbar.removeEventListener('scroll', onScrollbarScroll);
|
|
4271
|
+
if (resizeObserver) {
|
|
4272
|
+
resizeObserver.disconnect();
|
|
4273
|
+
}
|
|
4274
|
+
};
|
|
4275
|
+
syncStickyScrollbar(scrollable, scrollbar);
|
|
4276
|
+
return cleanup;
|
|
4277
|
+
}
|
|
4278
|
+
function syncStickyScrollbar(scrollable, scrollbar) {
|
|
4279
|
+
if (overlayStickyScrollbars.has(scrollbar)) {
|
|
4280
|
+
return;
|
|
4281
|
+
}
|
|
4282
|
+
const spacer = scrollbar.firstElementChild;
|
|
4283
|
+
if (!spacer || !lexical.isHTMLElement(spacer) || !scrollable.isConnected) {
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4286
|
+
const view = scrollable.ownerDocument.defaultView;
|
|
4287
|
+
if (!view) {
|
|
4288
|
+
scrollbar.style.display = 'none';
|
|
4289
|
+
return;
|
|
4290
|
+
}
|
|
4291
|
+
// All layout reads happen before any write so a sync forces at most one
|
|
4292
|
+
// reflow, and both metrics come from the same element so their rounding
|
|
4293
|
+
// can't disagree at fractional zoom levels.
|
|
4294
|
+
const overflowX = view.getComputedStyle(scrollable).overflowX;
|
|
4295
|
+
const isScrollable = overflowX === 'auto' || overflowX === 'scroll';
|
|
4296
|
+
const scrollWidth = scrollable.scrollWidth;
|
|
4297
|
+
const clientWidth = scrollable.clientWidth;
|
|
4298
|
+
const spacerWidth = scrollWidth + 'px';
|
|
4299
|
+
if (spacer.style.width !== spacerWidth) {
|
|
4300
|
+
spacer.style.width = spacerWidth;
|
|
4301
|
+
}
|
|
4302
|
+
const hasOverflow = isScrollable && scrollWidth > clientWidth;
|
|
4303
|
+
scrollbar.style.display = hasOverflow ? '' : 'none';
|
|
4304
|
+
}
|
|
4305
|
+
function findStickyScrollbarElements(dom) {
|
|
4306
|
+
if (!dom.hasAttribute('data-lexical-sticky-scrollbar')) {
|
|
4307
|
+
return null;
|
|
4308
|
+
}
|
|
4309
|
+
const firstChild = dom.firstElementChild;
|
|
4310
|
+
if (!isHTMLDivElement(firstChild)) {
|
|
4311
|
+
return null;
|
|
4312
|
+
}
|
|
4313
|
+
const tableElement = firstChild.querySelector(':scope > table');
|
|
4314
|
+
if (!isHTMLTableElement(tableElement)) {
|
|
4315
|
+
return null;
|
|
4316
|
+
}
|
|
4317
|
+
const scrollbar = firstChild.nextElementSibling;
|
|
4318
|
+
if (!isHTMLDivElement(scrollbar)) {
|
|
4319
|
+
return null;
|
|
4320
|
+
}
|
|
4321
|
+
return {
|
|
4322
|
+
scrollable: firstChild,
|
|
4323
|
+
scrollbar,
|
|
4324
|
+
tableElement
|
|
4325
|
+
};
|
|
4326
|
+
}
|
|
4200
4327
|
const scrollableEditors = new WeakSet();
|
|
4201
4328
|
function $isScrollableTablesActive(editor = lexical.$getEditor()) {
|
|
4202
4329
|
return scrollableEditors.has(editor);
|
|
4203
4330
|
}
|
|
4331
|
+
function $isStickyScrollbarActive(editor = lexical.$getEditor()) {
|
|
4332
|
+
const dep = extension.getPeerDependencyFromEditor(editor, '@lexical/table/Table');
|
|
4333
|
+
// peek() so a reconcile that runs inside a signals effect (e.g. via a
|
|
4334
|
+
// discrete update or force-commit read) does not subscribe that effect to
|
|
4335
|
+
// the table config; re-rendering on change is the TableExtension's job.
|
|
4336
|
+
return dep ? dep.output.hasStickyScrollbar.peek() && dep.output.hasHorizontalScroll.peek() : false;
|
|
4337
|
+
}
|
|
4204
4338
|
function setScrollableTablesActive(editor, active) {
|
|
4205
4339
|
if (active) {
|
|
4206
4340
|
if (!editor._config.theme.tableScrollableWrapper) {
|
|
@@ -4215,12 +4349,24 @@ function setScrollableTablesActive(editor, active) {
|
|
|
4215
4349
|
/** @noInheritDoc */
|
|
4216
4350
|
class TableNode extends lexical.ElementNode {
|
|
4217
4351
|
/** @internal */
|
|
4218
|
-
__rowStriping;
|
|
4219
|
-
__frozenColumnCount;
|
|
4220
|
-
__frozenRowCount;
|
|
4221
|
-
__colWidths
|
|
4222
|
-
|
|
4223
|
-
|
|
4352
|
+
__rowStriping = false;
|
|
4353
|
+
__frozenColumnCount = 0;
|
|
4354
|
+
__frozenRowCount = 0;
|
|
4355
|
+
// Initialized unconditionally (not `__colWidths?: ...`) so a freshly
|
|
4356
|
+
// constructed instance always has the own property — `@lexical/yjs` derives a
|
|
4357
|
+
// node's syncable properties from `Object.keys(new Klass())`, so an
|
|
4358
|
+
// uninitialized optional field would silently drop out of collab sync.
|
|
4359
|
+
__colWidths = undefined;
|
|
4360
|
+
$config() {
|
|
4361
|
+
return this.config('table', {
|
|
4362
|
+
extends: lexical.ElementNode,
|
|
4363
|
+
importDOM: {
|
|
4364
|
+
table: () => ({
|
|
4365
|
+
conversion: $convertTableElement,
|
|
4366
|
+
priority: 1
|
|
4367
|
+
})
|
|
4368
|
+
}
|
|
4369
|
+
});
|
|
4224
4370
|
}
|
|
4225
4371
|
getColWidths() {
|
|
4226
4372
|
const self = this.getLatest();
|
|
@@ -4232,9 +4378,6 @@ class TableNode extends lexical.ElementNode {
|
|
|
4232
4378
|
self.__colWidths = colWidths !== undefined && __DEV__ ? Object.freeze(colWidths) : colWidths;
|
|
4233
4379
|
return self;
|
|
4234
4380
|
}
|
|
4235
|
-
static clone(node) {
|
|
4236
|
-
return new TableNode(node.__key);
|
|
4237
|
-
}
|
|
4238
4381
|
afterCloneFrom(prevNode) {
|
|
4239
4382
|
super.afterCloneFrom(prevNode);
|
|
4240
4383
|
this.__colWidths = prevNode.__colWidths;
|
|
@@ -4242,27 +4385,9 @@ class TableNode extends lexical.ElementNode {
|
|
|
4242
4385
|
this.__frozenColumnCount = prevNode.__frozenColumnCount;
|
|
4243
4386
|
this.__frozenRowCount = prevNode.__frozenRowCount;
|
|
4244
4387
|
}
|
|
4245
|
-
static importDOM() {
|
|
4246
|
-
return {
|
|
4247
|
-
table: _node => ({
|
|
4248
|
-
conversion: $convertTableElement,
|
|
4249
|
-
priority: 1
|
|
4250
|
-
})
|
|
4251
|
-
};
|
|
4252
|
-
}
|
|
4253
|
-
static importJSON(serializedNode) {
|
|
4254
|
-
return $createTableNode().updateFromJSON(serializedNode);
|
|
4255
|
-
}
|
|
4256
4388
|
updateFromJSON(serializedNode) {
|
|
4257
4389
|
return super.updateFromJSON(serializedNode).setRowStriping(serializedNode.rowStriping || false).setFrozenColumns(serializedNode.frozenColumnCount || 0).setFrozenRows(serializedNode.frozenRowCount || 0).setColWidths(serializedNode.colWidths);
|
|
4258
4390
|
}
|
|
4259
|
-
constructor(key) {
|
|
4260
|
-
super(key);
|
|
4261
|
-
this.__rowStriping = false;
|
|
4262
|
-
this.__frozenColumnCount = 0;
|
|
4263
|
-
this.__frozenRowCount = 0;
|
|
4264
|
-
this.__colWidths = undefined;
|
|
4265
|
-
}
|
|
4266
4391
|
exportJSON() {
|
|
4267
4392
|
return {
|
|
4268
4393
|
...super.exportJSON(),
|
|
@@ -4293,16 +4418,19 @@ class TableNode extends lexical.ElementNode {
|
|
|
4293
4418
|
lexical.addClassNamesToElement(tableElement, config.theme.table);
|
|
4294
4419
|
this.updateTableElement(null, tableElement, config);
|
|
4295
4420
|
if ($isScrollableTablesActive(editor)) {
|
|
4296
|
-
const
|
|
4297
|
-
const
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4421
|
+
const hasStickyScrollbar = $isStickyScrollbarActive(editor);
|
|
4422
|
+
const scrollableWrapper = $createScrollableWrapper(tableElement, config, hasStickyScrollbar);
|
|
4423
|
+
this.updateTableWrapper(null, scrollableWrapper, tableElement, config);
|
|
4424
|
+
if (hasStickyScrollbar) {
|
|
4425
|
+
const stickyScrollbar = $createStickyScrollbar(config);
|
|
4426
|
+
const outerWrapper = lexical.$getDocument().createElement('div');
|
|
4427
|
+
outerWrapper.setAttribute('data-lexical-sticky-scrollbar', 'true');
|
|
4428
|
+
outerWrapper.appendChild(scrollableWrapper);
|
|
4429
|
+
outerWrapper.appendChild(stickyScrollbar);
|
|
4430
|
+
lexical.setDOMUnmanaged(stickyScrollbar);
|
|
4431
|
+
return outerWrapper;
|
|
4432
|
+
}
|
|
4433
|
+
return scrollableWrapper;
|
|
4306
4434
|
}
|
|
4307
4435
|
return tableElement;
|
|
4308
4436
|
}
|
|
@@ -4334,7 +4462,16 @@ class TableNode extends lexical.ElementNode {
|
|
|
4334
4462
|
return true;
|
|
4335
4463
|
}
|
|
4336
4464
|
if (isHTMLDivElement(dom)) {
|
|
4337
|
-
|
|
4465
|
+
const hasStickyDom = dom.hasAttribute('data-lexical-sticky-scrollbar');
|
|
4466
|
+
if (hasStickyDom !== $isStickyScrollbarActive()) {
|
|
4467
|
+
return true;
|
|
4468
|
+
}
|
|
4469
|
+
// The scrollable wrapper is the table's immediate parent in both
|
|
4470
|
+
// layouts (the inner div in sticky mode, dom itself otherwise).
|
|
4471
|
+
const scrollable = tableElement.parentElement;
|
|
4472
|
+
if (isHTMLDivElement(scrollable)) {
|
|
4473
|
+
this.updateTableWrapper(prevNode, scrollable, tableElement, config);
|
|
4474
|
+
}
|
|
4338
4475
|
}
|
|
4339
4476
|
this.updateTableElement(prevNode, tableElement, config);
|
|
4340
4477
|
return false;
|
|
@@ -5157,6 +5294,51 @@ const TableImportRules = [TableRule, TableRowRule, TableCellRule];
|
|
|
5157
5294
|
*
|
|
5158
5295
|
*/
|
|
5159
5296
|
|
|
5297
|
+
function registerStickyScrollbar(editor) {
|
|
5298
|
+
const attached = new Map();
|
|
5299
|
+
const detachAll = () => {
|
|
5300
|
+
for (const {
|
|
5301
|
+
cleanup
|
|
5302
|
+
} of attached.values()) {
|
|
5303
|
+
cleanup();
|
|
5304
|
+
}
|
|
5305
|
+
attached.clear();
|
|
5306
|
+
};
|
|
5307
|
+
return lexical.mergeRegister(detachAll, editor.registerMutationListener(TableNode, nodeMutations => {
|
|
5308
|
+
for (const [nodeKey, mutation] of nodeMutations) {
|
|
5309
|
+
const prev = attached.get(nodeKey);
|
|
5310
|
+
if (mutation === 'destroyed') {
|
|
5311
|
+
if (prev) {
|
|
5312
|
+
prev.cleanup();
|
|
5313
|
+
attached.delete(nodeKey);
|
|
5314
|
+
}
|
|
5315
|
+
continue;
|
|
5316
|
+
}
|
|
5317
|
+
const dom = editor.getElementByKey(nodeKey);
|
|
5318
|
+
const parts = dom && lexical.isHTMLElement(dom) ? findStickyScrollbarElements(dom) : null;
|
|
5319
|
+
if (prev && parts && prev.parts.scrollable === parts.scrollable && prev.parts.scrollbar === parts.scrollbar && prev.parts.tableElement === parts.tableElement) {
|
|
5320
|
+
// Same DOM: keep the listeners, but resync since the
|
|
5321
|
+
// update may still change scrollability (e.g. a frozen
|
|
5322
|
+
// rows toggle switches the wrapper to overflow-x: clip).
|
|
5323
|
+
syncStickyScrollbar(parts.scrollable, parts.scrollbar);
|
|
5324
|
+
continue;
|
|
5325
|
+
}
|
|
5326
|
+
if (prev) {
|
|
5327
|
+
prev.cleanup();
|
|
5328
|
+
attached.delete(nodeKey);
|
|
5329
|
+
}
|
|
5330
|
+
if (parts) {
|
|
5331
|
+
attached.set(nodeKey, {
|
|
5332
|
+
cleanup: attachStickyScrollbarListeners(parts),
|
|
5333
|
+
parts
|
|
5334
|
+
});
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
}, {
|
|
5338
|
+
skipInitialization: false
|
|
5339
|
+
}));
|
|
5340
|
+
}
|
|
5341
|
+
|
|
5160
5342
|
/**
|
|
5161
5343
|
* Configures {@link TableNode}, {@link TableRowNode}, {@link TableCellNode} and
|
|
5162
5344
|
* registers table behaviors (see {@link TableConfig})
|
|
@@ -5170,6 +5352,7 @@ const TableExtension = /* @__PURE__ */lexical.defineExtension({
|
|
|
5170
5352
|
hasCellMerge: true,
|
|
5171
5353
|
hasHorizontalScroll: true,
|
|
5172
5354
|
hasNestedTables: false,
|
|
5355
|
+
hasStickyScrollbar: false,
|
|
5173
5356
|
hasTabHandler: true
|
|
5174
5357
|
}),
|
|
5175
5358
|
dependencies: [
|
|
@@ -5183,18 +5366,31 @@ const TableExtension = /* @__PURE__ */lexical.defineExtension({
|
|
|
5183
5366
|
nodes: () => [TableNode, TableRowNode, TableCellNode],
|
|
5184
5367
|
register(editor, config, state) {
|
|
5185
5368
|
const stores = state.getOutput();
|
|
5369
|
+
let prevStickyScrollbar = false;
|
|
5186
5370
|
return lexical.mergeRegister(extension.effect(() => {
|
|
5187
5371
|
const hasHorizontalScroll = stores.hasHorizontalScroll.value;
|
|
5372
|
+
const hasStickyScrollbar = stores.hasStickyScrollbar.value && hasHorizontalScroll;
|
|
5188
5373
|
const hadHorizontalScroll = $isScrollableTablesActive(editor);
|
|
5189
5374
|
if (hadHorizontalScroll !== hasHorizontalScroll) {
|
|
5190
5375
|
setScrollableTablesActive(editor, hasHorizontalScroll);
|
|
5376
|
+
}
|
|
5377
|
+
if (hadHorizontalScroll !== hasHorizontalScroll || prevStickyScrollbar !== hasStickyScrollbar) {
|
|
5191
5378
|
// Re-render existing tables through the new scroll-wrapper config
|
|
5192
5379
|
// without cloning every TableNode the way marking them dirty would. A
|
|
5193
5380
|
// full reconcile marks no nodes dirty, so it's deferred (no
|
|
5194
5381
|
// synchronous render from this effect) and produces no history entry.
|
|
5195
5382
|
editor.update(lexical.$fullReconcile);
|
|
5196
5383
|
}
|
|
5197
|
-
|
|
5384
|
+
prevStickyScrollbar = hasStickyScrollbar;
|
|
5385
|
+
}), registerTablePlugin(editor, stores), extension.effect(() => registerTableSelectionObserver(editor, stores.hasTabHandler.value)), extension.effect(() => {
|
|
5386
|
+
if (stores.hasStickyScrollbar.value && stores.hasHorizontalScroll.value) {
|
|
5387
|
+
return editor.registerRootListener(rootElement => {
|
|
5388
|
+
if (rootElement) {
|
|
5389
|
+
return registerStickyScrollbar(editor);
|
|
5390
|
+
}
|
|
5391
|
+
});
|
|
5392
|
+
}
|
|
5393
|
+
}), extension.effect(() => stores.hasCellMerge.value ? undefined : registerTableCellUnmergeTransform(editor)), extension.effect(() => stores.hasCellBackgroundColor.value ? undefined : editor.registerNodeTransform(TableCellNode, node => {
|
|
5198
5394
|
if (node.getBackgroundColor() !== null) {
|
|
5199
5395
|
node.setBackgroundColor(null);
|
|
5200
5396
|
}
|
|
@@ -5216,6 +5412,7 @@ const TableImportExtension = /* @__PURE__ */lexical.defineExtension({
|
|
|
5216
5412
|
name: '@lexical/table/Import'
|
|
5217
5413
|
});
|
|
5218
5414
|
|
|
5415
|
+
exports.$computeTableCellRectBoundary = $computeTableCellRectBoundary;
|
|
5219
5416
|
exports.$computeTableMap = $computeTableMap;
|
|
5220
5417
|
exports.$computeTableMapSkipCellCheck = $computeTableMapSkipCellCheck;
|
|
5221
5418
|
exports.$createTableCellNode = $createTableCellNode;
|
|
@@ -5250,6 +5447,7 @@ exports.$insertTableRowAtSelection = $insertTableRowAtSelection;
|
|
|
5250
5447
|
exports.$insertTableRow__EXPERIMENTAL = $insertTableRow__EXPERIMENTAL;
|
|
5251
5448
|
exports.$isScrollableTablesActive = $isScrollableTablesActive;
|
|
5252
5449
|
exports.$isSimpleTable = $isSimpleTable;
|
|
5450
|
+
exports.$isStickyScrollbarActive = $isStickyScrollbarActive;
|
|
5253
5451
|
exports.$isTableCellNode = $isTableCellNode;
|
|
5254
5452
|
exports.$isTableNode = $isTableNode;
|
|
5255
5453
|
exports.$isTableRowNode = $isTableRowNode;
|