Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lexical-table] feat: Add row striping #6547

Merged
merged 7 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,9 @@ function TableActionMenu({
throw new Error('Expected to find tableElement in DOM');
}

const tableSelection = getTableObserverFromTableElement(tableElement);
if (tableSelection !== null) {
tableSelection.clearHighlight();
const tableObserver = getTableObserverFromTableElement(tableElement);
if (tableObserver !== null) {
tableObserver.clearHighlight();
}

tableNode.markDirty();
Expand Down Expand Up @@ -457,7 +457,19 @@ function TableActionMenu({

tableCell.toggleHeaderStyle(TableCellHeaderStates.COLUMN);
}
clearTableSelection();
onClose();
});
}, [editor, tableCellNode, clearTableSelection, onClose]);

const toggleRowStriping = useCallback(() => {
editor.update(() => {
if (tableCellNode.isAttached()) {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
if (tableNode) {
tableNode.setRowStriping(!tableNode.getRowStriping());
}
}
clearTableSelection();
onClose();
});
Expand Down Expand Up @@ -537,6 +549,13 @@ function TableActionMenu({
data-test-id="table-background-color">
<span className="text">Background color</span>
</button>
<button
type="button"
className="item"
onClick={() => toggleRowStriping()}
data-test-id="table-row-striping">
<span className="text">Toggle Row Striping</span>
</button>
<hr />
<button
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,12 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {

const removeRootListener = editor.registerRootListener(
(rootElement, prevRootElement) => {
rootElement?.addEventListener('mousemove', onMouseMove);
rootElement?.addEventListener('mousedown', onMouseDown);
rootElement?.addEventListener('mouseup', onMouseUp);

prevRootElement?.removeEventListener('mousemove', onMouseMove);
prevRootElement?.removeEventListener('mousedown', onMouseDown);
prevRootElement?.removeEventListener('mouseup', onMouseUp);
rootElement?.addEventListener('mousemove', onMouseMove);
rootElement?.addEventListener('mousedown', onMouseDown);
rootElement?.addEventListener('mouseup', onMouseUp);
},
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@
width: max-content;
margin: 0px 25px 30px 0px;
}
.PlaygroundEditorTheme__tableRowStriping tr:nth-child(even) {
background-color: #f2f5fb;
}
.PlaygroundEditorTheme__tableSelection *::selection {
background-color: transparent;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const theme: EditorThemeClasses = {
tableCellSelected: 'PlaygroundEditorTheme__tableCellSelected',
tableCellSortedIndicator: 'PlaygroundEditorTheme__tableCellSortedIndicator',
tableResizeRuler: 'PlaygroundEditorTheme__tableCellResizeRuler',
tableRowStriping: 'PlaygroundEditorTheme__tableRowStriping',
tableSelected: 'PlaygroundEditorTheme__tableSelected',
tableSelection: 'PlaygroundEditorTheme__tableSelection',
text: {
Expand Down
61 changes: 37 additions & 24 deletions packages/lexical-react/src/LexicalTablePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,40 +110,53 @@ export function TablePlugin({
}, [editor]);

useEffect(() => {
const tableSelections = new Map<NodeKey, TableObserver>();
const tableSelections = new Map<
NodeKey,
[TableObserver, HTMLTableElementWithWithTableSelectionState]
>();

const initializeTableNode = (tableNode: TableNode) => {
const nodeKey = tableNode.getKey();
const tableElement = editor.getElementByKey(
nodeKey,
) as HTMLTableElementWithWithTableSelectionState;
if (tableElement && !tableSelections.has(nodeKey)) {
const tableSelection = applyTableHandlers(
tableNode,
tableElement,
editor,
hasTabHandler,
);
tableSelections.set(nodeKey, tableSelection);
}
const initializeTableNode = (
tableNode: TableNode,
nodeKey: NodeKey,
dom: HTMLElement,
) => {
const tableElement = dom as HTMLTableElementWithWithTableSelectionState;
const tableSelection = applyTableHandlers(
tableNode,
tableElement,
editor,
hasTabHandler,
);
tableSelections.set(nodeKey, [tableSelection, tableElement]);
};

const unregisterMutationListener = editor.registerMutationListener(
TableNode,
(nodeMutations) => {
for (const [nodeKey, mutation] of nodeMutations) {
if (mutation === 'created') {
editor.getEditorState().read(() => {
const tableNode = $getNodeByKey<TableNode>(nodeKey);
if ($isTableNode(tableNode)) {
initializeTableNode(tableNode);
if (mutation === 'created' || mutation === 'updated') {
const tableSelection = tableSelections.get(nodeKey);
const dom = editor.getElementByKey(nodeKey);
if (!(tableSelection && dom === tableSelection[1])) {
// The update created a new DOM node, destroy the existing TableObserver
if (tableSelection) {
tableSelection[0].removeListeners();
tableSelections.delete(nodeKey);
}
});
if (dom !== null) {
// Create a new TableObserver
editor.getEditorState().read(() => {
const tableNode = $getNodeByKey<TableNode>(nodeKey);
if ($isTableNode(tableNode)) {
initializeTableNode(tableNode, nodeKey, dom);
}
});
}
}
} else if (mutation === 'destroyed') {
const tableSelection = tableSelections.get(nodeKey);

if (tableSelection !== undefined) {
tableSelection.removeListeners();
tableSelection[0].removeListeners();
tableSelections.delete(nodeKey);
}
}
Expand All @@ -156,7 +169,7 @@ export function TablePlugin({
unregisterMutationListener();
// Hook might be called multiple times so cleaning up tables listeners as well,
// as it'll be reinitialized during recurring call
for (const [, tableSelection] of tableSelections) {
for (const [, [tableSelection]] of tableSelections) {
tableSelection.removeListeners();
}
};
Expand Down
79 changes: 69 additions & 10 deletions packages/lexical-table/src/LexicalTableNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*
*/

import type {TableCellNode} from './LexicalTableCellNode';
import type {
DOMConversionMap,
DOMConversionOutput,
Expand All @@ -16,24 +15,51 @@ import type {
LexicalNode,
NodeKey,
SerializedElementNode,
Spread,
} from 'lexical';

import {addClassNamesToElement, isHTMLElement} from '@lexical/utils';
import {
addClassNamesToElement,
isHTMLElement,
removeClassNamesFromElement,
} from '@lexical/utils';
import {
$applyNodeReplacement,
$getNearestNodeFromDOMNode,
ElementNode,
} from 'lexical';

import {$isTableCellNode} from './LexicalTableCellNode';
import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode';
import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode';
import {getTable} from './LexicalTableSelectionHelpers';

export type SerializedTableNode = SerializedElementNode;
export type SerializedTableNode = Spread<
{
rowStriping?: boolean;
},
SerializedElementNode
>;

function setRowStriping(
dom: HTMLElement,
config: EditorConfig,
rowStriping: boolean,
) {
if (rowStriping) {
addClassNamesToElement(dom, config.theme.tableRowStriping);
dom.setAttribute('data-lexical-row-striping', 'true');
} else {
removeClassNamesFromElement(dom, config.theme.tableRowStriping);
dom.removeAttribute('data-lexical-row-striping');
}
}

/** @noInheritDoc */
export class TableNode extends ElementNode {
/** @internal */
__rowStriping: boolean;

static getType(): string {
return 'table';
}
Expand All @@ -42,6 +68,11 @@ export class TableNode extends ElementNode {
return new TableNode(node.__key);
}

afterCloneFrom(prevNode: this) {
super.afterCloneFrom(prevNode);
this.__rowStriping = prevNode.__rowStriping;
}

static importDOM(): DOMConversionMap | null {
return {
table: (_node: Node) => ({
Expand All @@ -51,17 +82,21 @@ export class TableNode extends ElementNode {
};
}

static importJSON(_serializedNode: SerializedTableNode): TableNode {
return $createTableNode();
static importJSON(serializedNode: SerializedTableNode): TableNode {
const tableNode = $createTableNode();
tableNode.__rowStriping = serializedNode.rowStriping || false;
return tableNode;
}

constructor(key?: NodeKey) {
super(key);
this.__rowStriping = false;
}

exportJSON(): SerializedElementNode {
exportJSON(): SerializedTableNode {
return {
...super.exportJSON(),
rowStriping: this.__rowStriping ? this.__rowStriping : undefined,
type: 'table',
version: 1,
};
Expand All @@ -71,11 +106,21 @@ export class TableNode extends ElementNode {
const tableElement = document.createElement('table');

addClassNamesToElement(tableElement, config.theme.table);
if (this.__rowStriping) {
setRowStriping(tableElement, config, true);
}

return tableElement;
}

updateDOM(): boolean {
updateDOM(
prevNode: TableNode,
dom: HTMLElement,
config: EditorConfig,
): boolean {
if (prevNode.__rowStriping !== this.__rowStriping) {
setRowStriping(dom, config, this.__rowStriping);
}
return false;
}

Expand Down Expand Up @@ -221,6 +266,14 @@ export class TableNode extends ElementNode {
return node;
}

getRowStriping(): boolean {
return Boolean(this.getLatest().__rowStriping);
}

setRowStriping(newRowStriping: boolean): void {
this.getWritable().__rowStriping = newRowStriping;
}

canSelectBefore(): true {
return true;
}
Expand All @@ -243,8 +296,14 @@ export function $getElementForTableNode(
return getTable(tableElement);
}

export function $convertTableElement(_domNode: Node): DOMConversionOutput {
return {node: $createTableNode()};
export function $convertTableElement(
domNode: HTMLElement,
): DOMConversionOutput {
const tableNode = $createTableNode();
if (domNode.hasAttribute('data-lexical-row-striping')) {
tableNode.setRowStriping(true);
}
return {node: tableNode};
}

export function $createTableNode(): TableNode {
Expand Down
6 changes: 6 additions & 0 deletions packages/lexical-table/src/LexicalTableObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export class TableObserver {
tableSelection: TableSelection | null;
hasHijackedSelectionStyles: boolean;
isSelecting: boolean;
abortController: AbortController;
listenerOptions: {signal: AbortSignal};

constructor(editor: LexicalEditor, tableNodeKey: string) {
this.isHighlightingCells = false;
Expand All @@ -96,16 +98,20 @@ export class TableObserver {
this.hasHijackedSelectionStyles = false;
this.trackTable();
this.isSelecting = false;
this.abortController = new AbortController();
this.listenerOptions = {signal: this.abortController.signal};
}

getTable(): TableDOMTable {
return this.table;
}

removeListeners() {
this.abortController.abort('removeListeners');
Array.from(this.listenersToRemove).forEach((removeListener) =>
removeListener(),
);
this.listenersToRemove.clear();
}

trackTable() {
Expand Down
Loading
Loading