Skip to content

Commit

Permalink
[lexical-table] feat: Add row striping (#6547)
Browse files Browse the repository at this point in the history
Co-authored-by: Bob Ippolito <bob@redivi.com>
  • Loading branch information
ivailop7 and etrepum authored Aug 30, 2024
1 parent 1f778da commit 96e2767
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 53 deletions.
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

0 comments on commit 96e2767

Please sign in to comment.