Skip to content

Commit

Permalink
feat: add table to lexical
Browse files Browse the repository at this point in the history
  • Loading branch information
Andy Wilson committed May 2, 2024
1 parent e8b6efd commit 013623c
Show file tree
Hide file tree
Showing 18 changed files with 912 additions and 4,585 deletions.
1 change: 1 addition & 0 deletions packages/lexical/__mocks__/styleMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
8 changes: 7 additions & 1 deletion packages/lexical/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"@techstack/react-feather": "workspace:*",
"lexical": "0.14.2",
"react": "18.2.0",
"styled-components": "6.1.1"
"styled-components": "6.1.1",
"typescript": "5.4.5"
},
"devDependencies": {
"@techstack/tcm-cli": "workspace:*",
Expand Down Expand Up @@ -45,5 +46,10 @@
},
"publishConfig": {
"access": "public"
},
"jest": {
"moduleNameMapper": {
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
}
}
}
33 changes: 33 additions & 0 deletions packages/lexical/src/__test__/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,39 @@ exports[`<Editor /> renders text correctly 1`] = `
<div
class="divider"
/>
<button
type="button"
>
<i
class="format element-format"
>
<svg
fill="none"
height="14"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="14"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12"
x2="12"
y1="5"
y2="19"
/>
<line
x1="5"
x2="19"
y1="12"
y2="12"
/>
</svg>
</i>
Table
</button>
<button
aria-label="Left Align"
class="toolbar-item spaced"
Expand Down
60 changes: 60 additions & 0 deletions packages/lexical/src/hooks/useModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {useCallback, useMemo, useState} from 'react';
import * as React from 'react';

import Modal from '../ui/Modal';

export default function useModal(): [
JSX.Element | null,
(title: string, showModal: (onClose: () => void) => React.JSX.Element) => void,
] {
const [modalContent, setModalContent] = useState<null | {
closeOnClickOutside: boolean;
content: JSX.Element;
title: string;
}>(null);

const onClose = useCallback(() => {
setModalContent(null);
}, []);

const modal = useMemo(() => {
if (modalContent === null) {
return null;
}
const {title, content, closeOnClickOutside} = modalContent;
return (
<Modal
onClose={onClose}
title={title}
closeOnClickOutside={closeOnClickOutside}>
{content}
</Modal>
);
}, [modalContent, onClose]);

const showModal = useCallback(
(
title: string,
// eslint-disable-next-line no-shadow
getContent: (onClose: () => void) => JSX.Element,
closeOnClickOutside = false,
) => {
setModalContent({
closeOnClickOutside,
content: getContent(onClose),
title,
});
},
[onClose],
);

return [modal, showModal];
}
37 changes: 22 additions & 15 deletions packages/lexical/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
import { $getRoot, $insertNodes, LexicalEditor, TextNode } from 'lexical';
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
import { TablePlugin } from '@lexical/react/LexicalTablePlugin';

import ToolbarPlugin from './plugins/ToolbarPlugin';
import OnChangePlugin from './plugins/OnChangePlugin';
import { Default } from './themes';
import ExtendedTextNode from './nodes/ExtendedTextNode';
import { TableContext } from './plugins/TablePlugin';

function Placeholder() {
return <div className='editor-placeholder'>Enter some rich text...</div>;
Expand Down Expand Up @@ -94,21 +96,26 @@ function EditorContainer({ value, onChange, name }: EditorProps) {
namespace: `Editor-${name}`,
}}
>
<div className='editor-container'>
<ToolbarPlugin />
<div className='editor-inner'>
<RichTextPlugin
contentEditable={<ContentEditable className='editor-input' />}
placeholder={<Placeholder />}
ErrorBoundary={LexicalErrorBoundary}
/>
</div>
</div>
<LinkPlugin />
<ListPlugin />
<HistoryPlugin />
<MarkdownShortcutPlugin />
<OnChangePlugin onChange={onChangeFn} />
<TableContext>
<>
<div className='editor-container'>
<ToolbarPlugin />
<div className='editor-inner'>
<RichTextPlugin
contentEditable={<ContentEditable className='editor-input' />}
placeholder={<Placeholder />}
ErrorBoundary={LexicalErrorBoundary}
/>
</div>
</div>
<LinkPlugin />
<ListPlugin />
<HistoryPlugin />
<MarkdownShortcutPlugin />
<TablePlugin hasCellMerge hasCellBackgroundColor />
<OnChangePlugin onChange={onChangeFn} />
</>
</TableContext>
</LexicalComposer>
);
}
Expand Down
184 changes: 184 additions & 0 deletions packages/lexical/src/plugins/TablePlugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
$createTableNodeWithDimensions,
INSERT_TABLE_COMMAND,
} from '@lexical/table';
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
EditorThemeClasses,
Klass,
LexicalCommand,
LexicalEditor,
LexicalNode,
} from 'lexical';
import {
createContext,
PropsWithChildren,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import * as React from 'react';

import Button from '../ui/Button';
import { DialogActions } from '../ui/Dialog';
import TextInput from '../ui/TextInput';

export type InsertTableCommandPayload = Readonly<{
columns: string;
rows: string;
includeHeaders?: boolean;
}>;

export type CellContextShape = {
cellEditorConfig: null | CellEditorConfig;
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>;
set: (
cellEditorConfig: null | CellEditorConfig,
cellEditorPlugins: null | React.JSX.Element | Array<React.JSX.Element>
) => void;
};

export type CellEditorConfig = Readonly<{
namespace: string;
nodes?: ReadonlyArray<Klass<LexicalNode>>;
onError: (error: Error, editor: LexicalEditor) => void;
readOnly?: boolean;
theme?: EditorThemeClasses;
}>;

export const INSERT_NEW_TABLE_COMMAND: LexicalCommand<InsertTableCommandPayload> =
createCommand('INSERT_NEW_TABLE_COMMAND');

export const CellContext = createContext<CellContextShape>({
cellEditorConfig: null,
cellEditorPlugins: null,
set: () => {
// Empty
},
});

export function TableContext({ children }: PropsWithChildren) {
const [contextValue, setContextValue] = useState<{
cellEditorConfig: null | CellEditorConfig;
cellEditorPlugins: null | React.JSX.Element | Array<React.JSX.Element>;
}>({
cellEditorConfig: null,
cellEditorPlugins: null,
});
return (
<CellContext.Provider
value={useMemo(
() => ({
cellEditorConfig: contextValue.cellEditorConfig,
cellEditorPlugins: contextValue.cellEditorPlugins,
set: (cellEditorConfig, cellEditorPlugins) => {
setContextValue({ cellEditorConfig, cellEditorPlugins });
},
}),
[contextValue.cellEditorConfig, contextValue.cellEditorPlugins]
)}
>
{children}
</CellContext.Provider>
);
}

export function InsertTableDialog({
activeEditor,
onClose,
}: {
activeEditor: LexicalEditor;
onClose: () => void;
}): React.JSX.Element {
const [rows, setRows] = useState('5');
const [columns, setColumns] = useState('5');
const [isDisabled, setIsDisabled] = useState(true);

useEffect(() => {
const row = Number(rows);
const column = Number(columns);
if (row && row > 0 && row <= 500 && column && column > 0 && column <= 50) {
setIsDisabled(false);
} else {
setIsDisabled(true);
}
}, [rows, columns]);

const onClick = () => {
activeEditor.dispatchCommand(INSERT_TABLE_COMMAND, {
columns,
rows,
});

onClose();
};

return (
<>
<TextInput
placeholder={'# of rows (1-500)'}
label='Rows'
onChange={setRows}
value={rows}
data-test-id='table-modal-rows'
type='number'
/>
<TextInput
placeholder={'# of columns (1-50)'}
label='Columns'
onChange={setColumns}
value={columns}
data-test-id='table-modal-columns'
type='number'
/>
<DialogActions data-test-id='table-model-confirm-insert'>
<Button disabled={isDisabled} onClick={onClick}>
Confirm
</Button>
</DialogActions>
</>
);
}

export function TablePlugin({
cellEditorConfig,
children,
}: {
cellEditorConfig: CellEditorConfig;
children: React.JSX.Element | Array<React.JSX.Element>;
}): React.JSX.Element | null {
const [editor] = useLexicalComposerContext();
const cellContext = useContext(CellContext);

useEffect(() => {
cellContext.set(cellEditorConfig, children);

return editor.registerCommand<InsertTableCommandPayload>(
INSERT_NEW_TABLE_COMMAND,
({ columns, rows, includeHeaders }) => {
const tableNode = $createTableNodeWithDimensions(
Number(rows),
Number(columns),
includeHeaders
);
$insertNodes([tableNode]);
return true;
},
COMMAND_PRIORITY_EDITOR
);
}, [cellContext, cellEditorConfig, children, editor]);

return null;
}
Loading

0 comments on commit 013623c

Please sign in to comment.