mirror of
https://github.com/facebook/lexical.git
synced 2025-05-17 23:26:16 +08:00
[lexical-table] Support first column freeze (#7134)
This commit is contained in:
@ -1330,6 +1330,7 @@ i.page-break,
|
||||
|
||||
.table-cell-action-button-container {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 0;
|
||||
left: 0;
|
||||
will-change: transform;
|
||||
|
@ -448,6 +448,21 @@ function TableActionMenu({
|
||||
});
|
||||
}, [editor, tableCellNode, clearTableSelection, onClose]);
|
||||
|
||||
const toggleFirstColumnFreeze = useCallback(() => {
|
||||
editor.update(() => {
|
||||
if (tableCellNode.isAttached()) {
|
||||
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
|
||||
if (tableNode) {
|
||||
tableNode.setFrozenColumns(
|
||||
tableNode.getFrozenColumns() === 0 ? 1 : 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
clearTableSelection();
|
||||
onClose();
|
||||
});
|
||||
}, [editor, tableCellNode, clearTableSelection, onClose]);
|
||||
|
||||
const handleCellBackgroundColor = useCallback(
|
||||
(value: string) => {
|
||||
editor.update(() => {
|
||||
@ -587,6 +602,13 @@ function TableActionMenu({
|
||||
</div>
|
||||
</DropDownItem>
|
||||
</DropDown>
|
||||
<button
|
||||
type="button"
|
||||
className="item"
|
||||
onClick={() => toggleFirstColumnFreeze()}
|
||||
data-test-id="table-freeze-first-column">
|
||||
<span className="text">Toggle First Column Freeze</span>
|
||||
</button>
|
||||
<hr />
|
||||
<button
|
||||
type="button"
|
||||
|
@ -214,6 +214,27 @@
|
||||
margin-top: 25px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.PlaygroundEditorTheme__tableFrozenColumn tr > td:first-child {
|
||||
background-color: #ffffff;
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
}
|
||||
.PlaygroundEditorTheme__tableFrozenColumn tr > th:first-child {
|
||||
background-color: #f2f3f5;
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
}
|
||||
.PlaygroundEditorTheme__tableFrozenColumn tr > :first-child::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
border-right: 1px solid #bbb;
|
||||
}
|
||||
.PlaygroundEditorTheme__tableRowStriping tr:nth-child(even) {
|
||||
background-color: #f2f5fb;
|
||||
}
|
||||
|
@ -104,6 +104,7 @@ const theme: EditorThemeClasses = {
|
||||
tableCellHeader: 'PlaygroundEditorTheme__tableCellHeader',
|
||||
tableCellResizer: 'PlaygroundEditorTheme__tableCellResizer',
|
||||
tableCellSelected: 'PlaygroundEditorTheme__tableCellSelected',
|
||||
tableFrozenColumn: 'PlaygroundEditorTheme__tableFrozenColumn',
|
||||
tableRowStriping: 'PlaygroundEditorTheme__tableRowStriping',
|
||||
tableScrollableWrapper: 'PlaygroundEditorTheme__tableScrollableWrapper',
|
||||
tableSelected: 'PlaygroundEditorTheme__tableSelected',
|
||||
|
@ -48,6 +48,7 @@ export type SerializedTableNode = Spread<
|
||||
{
|
||||
colWidths?: readonly number[];
|
||||
rowStriping?: boolean;
|
||||
frozenColumnCount?: number;
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
@ -88,6 +89,20 @@ function setRowStriping(
|
||||
}
|
||||
}
|
||||
|
||||
function setFrozenColumns(
|
||||
dom: HTMLElement,
|
||||
config: EditorConfig,
|
||||
frozenColumnCount: number,
|
||||
): void {
|
||||
if (frozenColumnCount > 0) {
|
||||
addClassNamesToElement(dom, config.theme.tableFrozenColumn);
|
||||
dom.setAttribute('data-lexical-frozen-column', 'true');
|
||||
} else {
|
||||
removeClassNamesFromElement(dom, config.theme.tableFrozenColumn);
|
||||
dom.removeAttribute('data-lexical-frozen-column');
|
||||
}
|
||||
}
|
||||
|
||||
function alignTableElement(
|
||||
dom: HTMLElement,
|
||||
config: EditorConfig,
|
||||
@ -137,6 +152,7 @@ export function setScrollableTablesActive(
|
||||
export class TableNode extends ElementNode {
|
||||
/** @internal */
|
||||
__rowStriping: boolean;
|
||||
__frozenColumnCount: number;
|
||||
__colWidths?: readonly number[];
|
||||
|
||||
static getType(): string {
|
||||
@ -164,6 +180,7 @@ export class TableNode extends ElementNode {
|
||||
super.afterCloneFrom(prevNode);
|
||||
this.__colWidths = prevNode.__colWidths;
|
||||
this.__rowStriping = prevNode.__rowStriping;
|
||||
this.__frozenColumnCount = prevNode.__frozenColumnCount;
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
@ -183,18 +200,23 @@ export class TableNode extends ElementNode {
|
||||
return super
|
||||
.updateFromJSON(serializedNode)
|
||||
.setRowStriping(serializedNode.rowStriping || false)
|
||||
.setFrozenColumns(serializedNode.frozenColumnCount || 0)
|
||||
.setColWidths(serializedNode.colWidths);
|
||||
}
|
||||
|
||||
constructor(key?: NodeKey) {
|
||||
super(key);
|
||||
this.__rowStriping = false;
|
||||
this.__frozenColumnCount = 0;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTableNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
colWidths: this.getColWidths(),
|
||||
frozenColumnCount: this.__frozenColumnCount
|
||||
? this.__frozenColumnCount
|
||||
: undefined,
|
||||
rowStriping: this.__rowStriping ? this.__rowStriping : undefined,
|
||||
};
|
||||
}
|
||||
@ -234,6 +256,9 @@ export class TableNode extends ElementNode {
|
||||
|
||||
addClassNamesToElement(tableElement, config.theme.table);
|
||||
alignTableElement(tableElement, config, this.getFormatType());
|
||||
if (this.__frozenColumnCount) {
|
||||
setFrozenColumns(tableElement, config, this.__frozenColumnCount);
|
||||
}
|
||||
if (this.__rowStriping) {
|
||||
setRowStriping(tableElement, config, true);
|
||||
}
|
||||
@ -256,6 +281,9 @@ export class TableNode extends ElementNode {
|
||||
if (prevNode.__rowStriping !== this.__rowStriping) {
|
||||
setRowStriping(dom, config, this.__rowStriping);
|
||||
}
|
||||
if (prevNode.__frozenColumnCount !== this.__frozenColumnCount) {
|
||||
setFrozenColumns(dom, config, this.__frozenColumnCount);
|
||||
}
|
||||
updateColgroup(dom, config, this.getColumnCount(), this.getColWidths());
|
||||
alignTableElement(
|
||||
this.getDOMSlot(dom).element,
|
||||
@ -472,6 +500,16 @@ export class TableNode extends ElementNode {
|
||||
return self;
|
||||
}
|
||||
|
||||
setFrozenColumns(columnCount: number): this {
|
||||
const self = this.getWritable();
|
||||
self.__frozenColumnCount = columnCount;
|
||||
return self;
|
||||
}
|
||||
|
||||
getFrozenColumns(): number {
|
||||
return this.getLatest().__frozenColumnCount;
|
||||
}
|
||||
|
||||
canSelectBefore(): true {
|
||||
return true;
|
||||
}
|
||||
|
@ -77,6 +77,7 @@ const editorConfig = Object.freeze({
|
||||
center: 'test-table-alignment-center',
|
||||
right: 'test-table-alignment-right',
|
||||
},
|
||||
tableFrozenColumn: 'test-table-frozen-column-class',
|
||||
tableRowStriping: 'test-table-row-striping-class',
|
||||
tableScrollableWrapper: 'table-scrollable-wrapper',
|
||||
},
|
||||
@ -1044,6 +1045,513 @@ describe('LexicalTableNode tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Toggle frozen first column ON/OFF', async () => {
|
||||
const {editor} = testEnv;
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const table = $createTableNodeWithDimensions(4, 4, true);
|
||||
root.append(table);
|
||||
});
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const table = root.getLastChild<TableNode>();
|
||||
if (table) {
|
||||
table.setFrozenColumns(1);
|
||||
}
|
||||
});
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const table = root.getLastChild<TableNode>();
|
||||
expectTableHtmlToBeEqual(
|
||||
table!.createDOM(editorConfig).outerHTML,
|
||||
html`
|
||||
<table
|
||||
class="${editorConfig.theme.table} ${editorConfig.theme
|
||||
.tableFrozenColumn}"
|
||||
data-lexical-frozen-column="true">
|
||||
<colgroup>
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
</colgroup>
|
||||
</table>
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
const stringifiedEditorState = JSON.stringify(
|
||||
editor.getEditorState(),
|
||||
);
|
||||
const expectedStringifiedEditorState = `{
|
||||
"root": {
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 3,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 1,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 1,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 1,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"type": "tablerow",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 2,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 0,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 0,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 0,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"type": "tablerow",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 2,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 0,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 0,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 0,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"type": "tablerow",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 2,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 0,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 0,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"backgroundColor": null,
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"textFormat": 0,
|
||||
"textStyle": "",
|
||||
"type": "paragraph",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"colSpan": 1,
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"headerState": 0,
|
||||
"indent": 0,
|
||||
"rowSpan": 1,
|
||||
"type": "tablecell",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"type": "tablerow",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"frozenColumnCount": 1,
|
||||
"indent": 0,
|
||||
"type": "table",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"direction": null,
|
||||
"format": "",
|
||||
"indent": 0,
|
||||
"type": "root",
|
||||
"version": 1
|
||||
}
|
||||
}`;
|
||||
|
||||
expect(JSON.parse(stringifiedEditorState)).toEqual(
|
||||
JSON.parse(expectedStringifiedEditorState),
|
||||
);
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const table = root.getLastChild<TableNode>();
|
||||
if (table) {
|
||||
table.setFrozenColumns(0);
|
||||
}
|
||||
});
|
||||
|
||||
await editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const table = root.getLastChild<TableNode>();
|
||||
expectTableHtmlToBeEqual(
|
||||
table!.createDOM(editorConfig).outerHTML,
|
||||
html`
|
||||
<table class="${editorConfig.theme.table}">
|
||||
<colgroup>
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
</colgroup>
|
||||
</table>
|
||||
`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Change Table-level alignment', async () => {
|
||||
const {editor} = testEnv;
|
||||
|
||||
|
Reference in New Issue
Block a user