From f5ae852608efc4c1ad9b017e8e85957389858f8d Mon Sep 17 00:00:00 2001 From: Bedru Umer <63902795+bedre7@users.noreply.github.com> Date: Tue, 5 Nov 2024 20:34:25 +0300 Subject: [PATCH 01/12] [lexical-react] Bug Fix: LexicalTypeaheadMenuPlugin SSR error: ReferenceError: document is not defined (#6794) Co-authored-by: Bob Ippolito --- .../src/LexicalContextMenuPlugin.tsx | 4 +- .../src/LexicalNodeMenuPlugin.tsx | 4 +- .../src/LexicalTypeaheadMenuPlugin.tsx | 4 +- .../__tests__/unit/useMenuAnchorRef.test.tsx | 64 +++++++++++++++++++ .../lexical-react/src/shared/LexicalMenu.ts | 14 ++-- 5 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 packages/lexical-react/src/__tests__/unit/useMenuAnchorRef.test.tsx diff --git a/packages/lexical-react/src/LexicalContextMenuPlugin.tsx b/packages/lexical-react/src/LexicalContextMenuPlugin.tsx index cc0dfcf37b0..c105d48b8f1 100644 --- a/packages/lexical-react/src/LexicalContextMenuPlugin.tsx +++ b/packages/lexical-react/src/LexicalContextMenuPlugin.tsx @@ -144,7 +144,9 @@ export function LexicalContextMenuPlugin({ return () => document.removeEventListener('click', handleClick); }, [editor, handleClick]); - return resolution === null || editor === null ? null : ( + return anchorElementRef.current === null || + resolution === null || + editor === null ? null : ( ({ } }, [editor, positionOrCloseMenu, nodeKey]); - return resolution === null || editor === null ? null : ( + return anchorElementRef.current === null || + resolution === null || + editor === null ? null : ( ({ openTypeahead, ]); - return resolution === null || editor === null ? null : ( + return resolution === null || + editor === null || + anchorElementRef.current === null ? null : ( ({ + useLexicalComposerContext: () => [createTestEditor()], +})); + +jest.mock('shared/canUseDOM', () => ({ + CAN_USE_DOM: false, +})); + +describe('useMenuAnchorRef', () => { + let container: HTMLDivElement | null = null; + let reactRoot: Root; + + beforeEach(() => { + container = document.createElement('div'); + reactRoot = createRoot(container); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return null if CAN_USE_DOM is false', async () => { + let anchorElementRef; + + function App() { + const resolution = null; + const setResolution = jest.fn(); + const anchorClassName = 'some-class'; + const parent = undefined; + const shouldIncludePageYOffset__EXPERIMENTAL = true; + + anchorElementRef = useMenuAnchorRef( + resolution, + setResolution, + anchorClassName, + parent, + shouldIncludePageYOffset__EXPERIMENTAL, + ); + + return null; + } + + await ReactTestUtils.act(async () => { + reactRoot.render(); + }); + + expect(anchorElementRef!.current).toBeNull(); + }); +}); diff --git a/packages/lexical-react/src/shared/LexicalMenu.ts b/packages/lexical-react/src/shared/LexicalMenu.ts index 42877233258..cda6ca3c5cb 100644 --- a/packages/lexical-react/src/shared/LexicalMenu.ts +++ b/packages/lexical-react/src/shared/LexicalMenu.ts @@ -32,6 +32,7 @@ import { useRef, useState, } from 'react'; +import {CAN_USE_DOM} from 'shared/canUseDOM'; import useLayoutEffect from 'shared/useLayoutEffect'; export type MenuTextMatch = { @@ -267,7 +268,7 @@ export function LexicalMenu({ }: { close: () => void; editor: LexicalEditor; - anchorElementRef: MutableRefObject; + anchorElementRef: MutableRefObject; resolution: MenuResolution; options: Array; shouldSplitNodeWithQuery?: boolean; @@ -481,12 +482,17 @@ export function useMenuAnchorRef( resolution: MenuResolution | null, setResolution: (r: MenuResolution | null) => void, className?: string, - parent: HTMLElement = document.body, + parent: HTMLElement | undefined = CAN_USE_DOM ? document.body : undefined, shouldIncludePageYOffset__EXPERIMENTAL: boolean = true, -): MutableRefObject { +): MutableRefObject { const [editor] = useLexicalComposerContext(); - const anchorElementRef = useRef(document.createElement('div')); + const anchorElementRef = useRef( + CAN_USE_DOM ? document.createElement('div') : null, + ); const positionMenu = useCallback(() => { + if (anchorElementRef.current === null || parent === undefined) { + return; + } anchorElementRef.current.style.top = anchorElementRef.current.style.bottom; const rootElement = editor.getRootElement(); const containerDiv = anchorElementRef.current; From 2c1a8f10c84444560c0982784ce3c14242f82340 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Tue, 5 Nov 2024 10:35:12 -0700 Subject: [PATCH 02/12] [lexical-markdown] Feature: add ability to control finding the end of a node matched by TextMatchTransformer (#6681) --- .../lexical-markdown/src/MarkdownImport.ts | 9 +++- .../src/MarkdownTransformers.ts | 9 ++++ .../__tests__/unit/LexicalMarkdown.test.ts | 53 ++++++++++++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/lexical-markdown/src/MarkdownImport.ts b/packages/lexical-markdown/src/MarkdownImport.ts index 2f7dc27324b..47f56dd3b7e 100644 --- a/packages/lexical-markdown/src/MarkdownImport.ts +++ b/packages/lexical-markdown/src/MarkdownImport.ts @@ -383,7 +383,14 @@ function importTextMatchTransformers( } const startIndex = match.index || 0; - const endIndex = startIndex + match[0].length; + const endIndex = transformer.getEndIndex + ? transformer.getEndIndex(textNode, match) + : startIndex + match[0].length; + + if (endIndex === false) { + continue; + } + let replaceNode, newTextNode; if (startIndex === 0) { diff --git a/packages/lexical-markdown/src/MarkdownTransformers.ts b/packages/lexical-markdown/src/MarkdownTransformers.ts index 2a335156213..2a4fa5d5cf0 100644 --- a/packages/lexical-markdown/src/MarkdownTransformers.ts +++ b/packages/lexical-markdown/src/MarkdownTransformers.ts @@ -174,6 +174,15 @@ export type TextMatchTransformer = Readonly<{ * Determines how the matched markdown text should be transformed into a node during the markdown import process */ replace?: (node: TextNode, match: RegExpMatchArray) => void; + /** + * For import operations, this function can be used to determine the end index of the match, after `importRegExp` has matched. + * Without this function, the end index will be determined by the length of the match from `importRegExp`. Manually determining the end index can be useful if + * the match from `importRegExp` is not the entire text content of the node. That way, `importRegExp` can be used to match only the start of the node, and `getEndIndex` + * can be used to match the end of the node. + * + * @returns The end index of the match, or false if the match was unsuccessful and a different transformer should be tried. + */ + getEndIndex?: (node: TextNode, match: RegExpMatchArray) => number | false; /** * Single character that allows the transformer to trigger when typed in the editor. This does not affect markdown imports outside of the markdown shortcut plugin. * If the trigger is matched, the `regExp` will be used to match the text in the second step. diff --git a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts index be7199eefab..f78fc4a3056 100644 --- a/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts +++ b/packages/lexical-markdown/src/__tests__/unit/LexicalMarkdown.test.ts @@ -9,7 +9,7 @@ import {$createCodeNode, CodeNode} from '@lexical/code'; import {createHeadlessEditor} from '@lexical/headless'; import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html'; -import {LinkNode} from '@lexical/link'; +import {$createLinkNode, LinkNode} from '@lexical/link'; import {ListItemNode, ListNode} from '@lexical/list'; import {HeadingNode, QuoteNode} from '@lexical/rich-text'; import {$createTextNode, $getRoot, $insertNodes} from 'lexical'; @@ -28,6 +28,51 @@ import { normalizeMarkdown, } from '../../MarkdownTransformers'; +const SIMPLE_INLINE_JSX_MATCHER: TextMatchTransformer = { + dependencies: [LinkNode], + getEndIndex(node, match) { + // Find the closing tag. Count the number of opening and closing tags to find the correct closing tag. + // For simplicity, this will only count the opening and closing tags without checking for "MyTag" specifically. + let openedSubStartMatches = 0; + const start = (match.index ?? 0) + match[0].length; + let endIndex = start; + const line = node.getTextContent(); + + for (let i = start; i < line.length; i++) { + const char = line[i]; + if (char === '<') { + const nextChar = line[i + 1]; + if (nextChar === '/') { + if (openedSubStartMatches === 0) { + endIndex = i + ''.length; + break; + } + openedSubStartMatches--; + } else { + openedSubStartMatches++; + } + } + } + return endIndex; + }, + importRegExp: /<(MyTag)\s*>/, + regExp: /__ignore__/, + replace: (textNode, match) => { + const linkNode = $createLinkNode('simple-jsx'); + + const textStart = match[0].length + (match.index ?? 0); + const textEnd = + (match.index ?? 0) + textNode.getTextContent().length - ''.length; + const text = match.input?.slice(textStart, textEnd); + + const linkTextNode = $createTextNode(text); + linkTextNode.setFormat(textNode.getFormat()); + linkNode.append(linkTextNode); + textNode.replace(linkNode); + }, + type: 'text-match', +}; + // Matches html within a mdx file const MDX_HTML_TRANSFORMER: MultilineElementTransformer = { dependencies: [CodeNode], @@ -461,6 +506,12 @@ describe('Markdown', () => { md: '```ts\nCode\n```ts\nSub Code\n```\n```', skipExport: true, }, + { + customTransformers: [SIMPLE_INLINE_JSX_MATCHER], + html: '

Hello One <MyTag>Two</MyTag> there

', + md: 'Hello One Two there', + skipExport: true, + }, ]; const HIGHLIGHT_TEXT_MATCH_IMPORT: TextMatchTransformer = { From fc1bea0d0d1c0feaf1b9640ff93f603ff8929376 Mon Sep 17 00:00:00 2001 From: Fadekemi Adebayo <82163647+Shopiley@users.noreply.github.com> Date: Tue, 5 Nov 2024 20:29:35 +0100 Subject: [PATCH 03/12] [Lexical-website] BugFix: Change button text colour to improve visibility (#6796) --- .../lexical-website/src/components/HomepageExamples/index.js | 2 +- packages/lexical-website/src/css/custom.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/lexical-website/src/components/HomepageExamples/index.js b/packages/lexical-website/src/components/HomepageExamples/index.js index b60e64392c4..5189561dc03 100644 --- a/packages/lexical-website/src/components/HomepageExamples/index.js +++ b/packages/lexical-website/src/components/HomepageExamples/index.js @@ -71,7 +71,7 @@ export default function HomepageExamples() { {EXAMPLES.map(({id, label}) => (
  • Date: Wed, 6 Nov 2024 07:45:12 +1000 Subject: [PATCH 04/12] [lexical-yjs] Bug Fix: clean up dangling text after undo in collaboration (#6670) Co-authored-by: James Fitzsimmons Co-authored-by: James Fitzsimmons <119275535+james-atticus@users.noreply.github.com> Co-authored-by: Bob Ippolito --- .../__tests__/e2e/Collaboration.spec.mjs | 157 ++++++++++++++++++ packages/lexical-yjs/src/CollabElementNode.ts | 32 ++-- 2 files changed, 175 insertions(+), 14 deletions(-) diff --git a/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs b/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs index 762ee82e94e..b47b3c04f6a 100644 --- a/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Collaboration.spec.mjs @@ -230,4 +230,161 @@ test.describe('Collaboration', () => { focusPath: [1, 1, 0], }); }); + + test('Remove dangling text from YJS when there is no preceding text node', async ({ + isRichText, + page, + isCollab, + browserName, + }) => { + test.skip(!isCollab); + + // Left collaborator types two paragraphs of text + await focusEditor(page); + await page.keyboard.type('Line 1'); + await page.keyboard.press('Enter'); + await sleep(1050); // default merge interval is 1000, add 50ms as overhead due to CI latency. + await page.keyboard.type('This is a test. '); + + // Right collaborator types at the end of paragraph 2 + await sleep(1050); + await page + .frameLocator('iframe[name="right"]') + .locator('[data-lexical-editor="true"]') + .focus(); + await page.keyboard.press('ArrowDown'); // Move caret to end of paragraph 2 + await page.keyboard.press('ArrowDown'); + await page.keyboard.type('Word'); + + await assertHTML( + page, + html` +

    + Line 1 +

    +

    + This is a test. Word +

    + `, + ); + + // Left collaborator undoes their text in the second paragraph. + await sleep(50); + await page.frameLocator('iframe[name="left"]').getByLabel('Undo').click(); + + // The undo also removed the text node from YJS. + // Check that the dangling text from right user was also removed. + await assertHTML( + page, + html` +

    + Line 1 +

    +


    + `, + ); + + // Left collaborator refreshes their page + await page.evaluate(() => { + document + .querySelector('iframe[name="left"]') + .contentDocument.location.reload(); + }); + + // Page content should be the same as before the refresh + await assertHTML( + page, + html` +

    + Line 1 +

    +


    + `, + ); + }); + + test('Merge dangling text into preceding text node', async ({ + isRichText, + page, + isCollab, + browserName, + }) => { + test.skip(!isCollab); + + // Left collaborator types two pieces of text in the same paragraph, but with different styling. + await focusEditor(page); + await page.keyboard.type('normal'); + await sleep(1050); + await toggleBold(page); + await page.keyboard.type('bold'); + + // Right collaborator types at the end of the paragraph. + await sleep(50); + await page + .frameLocator('iframe[name="right"]') + .locator('[data-lexical-editor="true"]') + .focus(); + await page.keyboard.press('ArrowDown'); // Move caret to end of paragraph + await page.keyboard.type('BOLD'); + + await assertHTML( + page, + html` +

    + normal + + boldBOLD + +

    + `, + ); + + // Left collaborator undoes their bold text. + await sleep(50); + await page.frameLocator('iframe[name="left"]').getByLabel('Undo').click(); + + // The undo also removed bold the text node from YJS. + // Check that the dangling text from right user was merged into the preceding text node. + await assertHTML( + page, + html` +

    + normalBOLD +

    + `, + ); + + // Left collaborator refreshes their page + await page.evaluate(() => { + document + .querySelector('iframe[name="left"]') + .contentDocument.location.reload(); + }); + + // Page content should be the same as before the refresh + await assertHTML( + page, + html` +

    + normalBOLD +

    + `, + ); + }); }); diff --git a/packages/lexical-yjs/src/CollabElementNode.ts b/packages/lexical-yjs/src/CollabElementNode.ts index f4c3f124c55..c38171af0e8 100644 --- a/packages/lexical-yjs/src/CollabElementNode.ts +++ b/packages/lexical-yjs/src/CollabElementNode.ts @@ -157,21 +157,25 @@ export class CollabElementNode { nodeIndex !== 0 ? children[nodeIndex - 1] : null; const nodeSize = node.getSize(); - if ( - offset === 0 && - delCount === 1 && - nodeIndex > 0 && - prevCollabNode instanceof CollabTextNode && - length === nodeSize && - // If the node has no keys, it's been deleted - Array.from(node._map.keys()).length === 0 - ) { - // Merge the text node with previous. - prevCollabNode._text += node._text; - children.splice(nodeIndex, 1); - } else if (offset === 0 && delCount === nodeSize) { - // The entire thing needs removing + if (offset === 0 && length === nodeSize) { + // Text node has been deleted. children.splice(nodeIndex, 1); + // If this was caused by an undo from YJS, there could be dangling text. + const danglingText = spliceString( + node._text, + offset, + delCount - 1, + '', + ); + if (danglingText.length > 0) { + if (prevCollabNode instanceof CollabTextNode) { + // Merge the text node with previous. + prevCollabNode._text += danglingText; + } else { + // No previous text node to merge into, just delete the text. + this._xmlText.delete(offset, danglingText.length); + } + } } else { node._text = spliceString(node._text, offset, delCount, ''); } From 86eba220414e77d8e770353065bf5dcc6901cfad Mon Sep 17 00:00:00 2001 From: Ajaezo Kingsley <54126417+Kingscliq@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:53:25 +0100 Subject: [PATCH 05/12] [lexical-website] Documentation Update: Add Documentation for html Property in Lexical Editor Configuration (#6770) Co-authored-by: Bob Ippolito --- examples/react-rich/src/App.tsx | 116 +++++++++++++++++- examples/react-rich/src/ExampleTheme.ts | 1 + examples/react-rich/src/styleConfig.ts | 25 ++++ .../docs/concepts/serialization.md | 29 +++++ 4 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 examples/react-rich/src/styleConfig.ts diff --git a/examples/react-rich/src/App.tsx b/examples/react-rich/src/App.tsx index e2e7adbcf5a..206d5624c19 100644 --- a/examples/react-rich/src/App.tsx +++ b/examples/react-rich/src/App.tsx @@ -5,27 +5,137 @@ * LICENSE file in the root directory of this source tree. * */ + import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin'; import {LexicalComposer} from '@lexical/react/LexicalComposer'; import {ContentEditable} from '@lexical/react/LexicalContentEditable'; import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary'; import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin'; import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin'; +import { + $isTextNode, + DOMConversionMap, + DOMExportOutput, + Klass, + LexicalEditor, + LexicalNode, + ParagraphNode, + TextNode, +} from 'lexical'; import ExampleTheme from './ExampleTheme'; import ToolbarPlugin from './plugins/ToolbarPlugin'; import TreeViewPlugin from './plugins/TreeViewPlugin'; +import {parseAllowedColor, parseAllowedFontSize} from './styleConfig'; const placeholder = 'Enter some rich text...'; +const removeStylesExportDOM = ( + editor: LexicalEditor, + target: LexicalNode, +): DOMExportOutput => { + const output = target.exportDOM(editor); + if (output && output.element instanceof HTMLElement) { + // Remove all inline styles and classes if the element is an HTMLElement + // Children are checked as well since TextNode can be nested + // in i, b, and strong tags. + for (const el of [ + output.element, + ...output.element.querySelectorAll('[style],[class],[dir="ltr"]'), + ]) { + el.removeAttribute('class'); + el.removeAttribute('style'); + if (el.getAttribute('dir') === 'ltr') { + el.removeAttribute('dir'); + } + } + } + return output; +}; + +const exportMap = new Map< + Klass, + (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput +>([ + [ParagraphNode, removeStylesExportDOM], + [TextNode, removeStylesExportDOM], +]); + +const getExtraStyles = (element: HTMLElement): string => { + // Parse styles from pasted input, but only if they match exactly the + // sort of styles that would be produced by exportDOM + let extraStyles = ''; + const fontSize = parseAllowedFontSize(element.style.fontSize); + const backgroundColor = parseAllowedColor(element.style.backgroundColor); + const color = parseAllowedColor(element.style.color); + if (fontSize !== '' && fontSize !== '15px') { + extraStyles += `font-size: ${fontSize};`; + } + if (backgroundColor !== '' && backgroundColor !== 'rgb(255, 255, 255)') { + extraStyles += `background-color: ${backgroundColor};`; + } + if (color !== '' && color !== 'rgb(0, 0, 0)') { + extraStyles += `color: ${color};`; + } + return extraStyles; +}; + +const constructImportMap = (): DOMConversionMap => { + const importMap: DOMConversionMap = {}; + + // Wrap all TextNode importers with a function that also imports + // the custom styles implemented by the playground + for (const [tag, fn] of Object.entries(TextNode.importDOM() || {})) { + importMap[tag] = (importNode) => { + const importer = fn(importNode); + if (!importer) { + return null; + } + return { + ...importer, + conversion: (element) => { + const output = importer.conversion(element); + if ( + output === null || + output.forChild === undefined || + output.after !== undefined || + output.node !== null + ) { + return output; + } + const extraStyles = getExtraStyles(element); + if (extraStyles) { + const {forChild} = output; + return { + ...output, + forChild: (child, parent) => { + const textNode = forChild(child, parent); + if ($isTextNode(textNode)) { + textNode.setStyle(textNode.getStyle() + extraStyles); + } + return textNode; + }, + }; + } + return output; + }, + }; + }; + } + + return importMap; +}; + const editorConfig = { + html: { + export: exportMap, + import: constructImportMap(), + }, namespace: 'React.js Demo', - nodes: [], - // Handling of errors during update + nodes: [ParagraphNode, TextNode], onError(error: Error) { throw error; }, - // The editor theme theme: ExampleTheme, }; diff --git a/examples/react-rich/src/ExampleTheme.ts b/examples/react-rich/src/ExampleTheme.ts index bbd871b653a..1cc2bc15528 100644 --- a/examples/react-rich/src/ExampleTheme.ts +++ b/examples/react-rich/src/ExampleTheme.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * */ + export default { code: 'editor-code', heading: { diff --git a/examples/react-rich/src/styleConfig.ts b/examples/react-rich/src/styleConfig.ts new file mode 100644 index 00000000000..d2d121c7980 --- /dev/null +++ b/examples/react-rich/src/styleConfig.ts @@ -0,0 +1,25 @@ +/** + * 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. + * + */ + +const MIN_ALLOWED_FONT_SIZE = 8; +const MAX_ALLOWED_FONT_SIZE = 72; + +export const parseAllowedFontSize = (input: string): string => { + const match = input.match(/^(\d+(?:\.\d+)?)px$/); + if (match) { + const n = Number(match[1]); + if (n >= MIN_ALLOWED_FONT_SIZE && n <= MAX_ALLOWED_FONT_SIZE) { + return input; + } + } + return ''; +}; + +export function parseAllowedColor(input: string) { + return /^rgb\(\d+, \d+, \d+\)$/.test(input) ? input : ''; +} diff --git a/packages/lexical-website/docs/concepts/serialization.md b/packages/lexical-website/docs/concepts/serialization.md index 03ed5f924e8..90eaf313fe3 100644 --- a/packages/lexical-website/docs/concepts/serialization.md +++ b/packages/lexical-website/docs/concepts/serialization.md @@ -437,3 +437,32 @@ function patchStyleConversion( }; } ``` + +### `html` Property for Import and Export Configuration + +The `html` property in `CreateEditorArgs` provides an alternate way to configure HTML import and export behavior in Lexical without subclassing or node replacement. It includes two properties: + +- `import` - Similar to `importDOM`, it controls how HTML elements are transformed into `LexicalNodes`. However, instead of defining conversions directly on each `LexicalNode`, `html.import` provides a configuration that can be overridden easily in the editor setup. + +- `export` - Similar to `exportDOM`, this property customizes how `LexicalNodes` are serialized into HTML. With `html.export`, users can specify transformations for various nodes collectively, offering a flexible override mechanism that can adapt without needing to extend or replace specific `LexicalNodes`. + +#### Key Differences from `importDOM` and `exportDOM` + +While `importDOM` and `exportDOM` allow for highly customized, node-specific conversions by defining them directly within the `LexicalNode` class, the `html` property enables broader, editor-wide configurations. This setup benefits situations where: + +- **Consistent Transformations**: You want uniform import/export behavior across different nodes without adjusting each node individually. +- **No Subclassing Required**: Overrides to import and export logic are applied at the editor configuration level, simplifying customization and reducing the need for extensive subclassing. + +#### Type Definitions + +```typescript +type HTMLConfig = { + export?: DOMExportOutputMap; // Optional map defining how nodes are exported to HTML. + import?: DOMConversionMap; // Optional record defining how HTML is converted into nodes. +}; +``` + +#### Example of a use case for the `html` Property for Import and Export Configuration: + +[Rich text sandbox](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-rich?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview) + From b284bead0d6664e99e76af118c98eff0f2e33012 Mon Sep 17 00:00:00 2001 From: Bedru Umer <63902795+bedre7@users.noreply.github.com> Date: Wed, 6 Nov 2024 04:23:57 +0300 Subject: [PATCH 06/12] [lexical-playground] Feature: Add more keyboard shortcuts (#6754) Co-authored-by: Bob Ippolito --- .../__tests__/e2e/KeyboardShortcuts.spec.mjs | 372 +++++++++++ .../__tests__/keyboardShortcuts/index.mjs | 156 +++++ packages/lexical-playground/src/App.tsx | 29 +- packages/lexical-playground/src/Editor.tsx | 19 +- .../src/context/ToolbarContext.tsx | 125 ++++ packages/lexical-playground/src/index.css | 19 +- .../src/plugins/ShortcutsPlugin/index.tsx | 168 +++++ .../src/plugins/ShortcutsPlugin/shortcuts.ts | 225 +++++++ .../src/plugins/ToolbarPlugin/fontSize.tsx | 152 +---- .../src/plugins/ToolbarPlugin/index.tsx | 611 +++++++----------- .../src/plugins/ToolbarPlugin/utils.ts | 292 +++++++++ 11 files changed, 1663 insertions(+), 505 deletions(-) create mode 100644 packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs create mode 100644 packages/lexical-playground/src/context/ToolbarContext.tsx create mode 100644 packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx create mode 100644 packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts create mode 100644 packages/lexical-playground/src/plugins/ToolbarPlugin/utils.ts diff --git a/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs b/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs new file mode 100644 index 00000000000..989a4bbbeb8 --- /dev/null +++ b/packages/lexical-playground/__tests__/e2e/KeyboardShortcuts.spec.mjs @@ -0,0 +1,372 @@ +/** + * 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 { + applyCodeBlock, + applyHeading, + applyNormalFormat, + applyQuoteBlock, + centerAlign, + clearFormatting, + decreaseFontSize, + increaseFontSize, + indent, + justifyAlign, + leftAlign, + outdent, + rightAlign, + selectCharacters, + toggleBold, + toggleBulletList, + toggleChecklist, + toggleInsertCodeBlock, + toggleItalic, + toggleNumberedList, + toggleStrikethrough, + toggleSubscript, + toggleSuperscript, + toggleUnderline, +} from '../keyboardShortcuts/index.mjs'; +import { + assertHTML, + assertSelection, + evaluate, + expect, + focusEditor, + html, + initialize, + test, + textContent, +} from '../utils/index.mjs'; + +const formatTestCases = [ + { + applyShortcut: (page) => applyNormalFormat(page), + canToggle: false, + format: 'Normal', + }, + { + applyShortcut: (page) => applyHeading(page, 1), + canToggle: false, + format: 'Heading 1', + }, + { + applyShortcut: (page) => applyHeading(page, 2), + canToggle: false, + format: 'Heading 2', + }, + { + applyShortcut: (page) => applyHeading(page, 3), + canToggle: false, + format: 'Heading 3', + }, + { + applyShortcut: (page) => toggleBulletList(page), + canToggle: true, + format: 'Bulleted List', + }, + { + applyShortcut: (page) => toggleNumberedList(page), + canToggle: true, + format: 'Numbered List', + }, + { + applyShortcut: (page) => toggleChecklist(page), + canToggle: true, + format: 'Check List', + }, + { + applyShortcut: (page) => applyQuoteBlock(page), + canToggle: false, + format: 'Quote', + }, + { + applyShortcut: (page) => applyCodeBlock(page), + canToggle: false, + format: 'Code Block', + }, +]; + +const alignmentTestCases = [ + { + alignment: 'Left Align', + applyShortcut: (page) => leftAlign(page), + }, + { + alignment: 'Center Align', + applyShortcut: (page) => centerAlign(page), + }, + { + alignment: 'Right Align', + applyShortcut: (page) => rightAlign(page), + }, + { + alignment: 'Justify Align', + applyShortcut: (page) => justifyAlign(page), + }, +]; + +const additionalStylesTestCases = [ + { + applyShortcut: (page) => toggleStrikethrough(page), + style: 'Strikethrough', + }, + { + applyShortcut: (page) => toggleSubscript(page), + style: 'Subscript', + }, + { + applyShortcut: (page) => toggleSuperscript(page), + style: 'Superscript', + }, +]; + +const DEFAULT_FORMAT = 'Normal'; + +const getSelectedFormat = async (page) => { + return await textContent( + page, + '.toolbar-item.block-controls > .text.dropdown-button-text', + ); +}; + +const isDropdownItemActive = async (page, dropdownItemIndex) => { + return await evaluate( + page, + async (_dropdownItemIndex) => { + await document + .querySelector( + 'button[aria-label="Formatting options for additional text styles"]', + ) + .click(); + + const isActive = await document + .querySelector('.dropdown') + .children[_dropdownItemIndex].classList.contains('active'); + + await document + .querySelector( + 'button[aria-label="Formatting options for additional text styles"]', + ) + .click(); + + return isActive; + }, + dropdownItemIndex, + ); +}; + +test.describe('Keyboard shortcuts', () => { + test.beforeEach(({isPlainText, isCollab, page}) => { + test.skip(isPlainText); + return initialize({isCollab, page}); + }); + + formatTestCases.forEach(({format, applyShortcut, canToggle}) => { + test(`Can use ${format} format with the shortcut`, async ({ + page, + isPlainText, + }) => { + await focusEditor(page); + + if (format === DEFAULT_FORMAT) { + // Apply a different format first + await applyHeading(page, 1); + } + + await applyShortcut(page); + + expect(await getSelectedFormat(page)).toBe(format); + + if (canToggle) { + await applyShortcut(page); + + // Should revert back to the default format + expect(await getSelectedFormat(page)).toBe(DEFAULT_FORMAT); + } + }); + }); + + alignmentTestCases.forEach(({alignment, applyShortcut}, index) => { + test(`Can use ${alignment} with the shortcut`, async ({ + page, + isPlainText, + }) => { + await focusEditor(page); + await applyShortcut(page); + + const selectedAlignment = await textContent( + page, + '.toolbar-item.spaced.alignment > .text.dropdown-button-text', + ); + + expect(selectedAlignment).toBe(alignment); + }); + }); + + additionalStylesTestCases.forEach( + ({applyShortcut, style}, dropdownItemIndex) => { + test(`Can use ${style} with the shortcut`, async ({ + page, + isPlainText, + }) => { + await focusEditor(page); + await applyShortcut(page); + + expect(await isDropdownItemActive(page, dropdownItemIndex)).toBe(true); + + // Toggle the style off and check if it's off + await focusEditor(page); + await applyShortcut(page); + expect(await isDropdownItemActive(page, dropdownItemIndex)).toBe(false); + }); + }, + ); + + test('Can increase and decrease font size with the shortcuts', async ({ + page, + isPlainText, + }) => { + await focusEditor(page); + await increaseFontSize(page); + + const getFontSize = async () => { + return await evaluate(page, () => { + return document.querySelector('.font-size-input').value; + }); + }; + + expect(await getFontSize()).toBe('17'); + await decreaseFontSize(page); + expect(await getFontSize()).toBe('15'); + }); + + test('Can clear formatting with the shortcut', async ({ + page, + isPlainText, + }) => { + await focusEditor(page); + // Apply some formatting first + await page.keyboard.type('abc'); + await selectCharacters(page, 'left', 3); + + await assertSelection(page, { + anchorOffset: 3, + anchorPath: [0, 0, 0], + focusOffset: 0, + focusPath: [0, 0, 0], + }); + + await toggleBold(page); + await toggleItalic(page); + await toggleUnderline(page); + await toggleStrikethrough(page); + await toggleSubscript(page); + + await assertHTML( + page, + html` +

    + + + abc + + +

    + `, + ); + + await clearFormatting(page); + + await assertHTML( + page, + html` +

    + abc +

    + `, + ); + }); + + test('Can toggle Insert Code Block with the shortcut', async ({ + page, + isPlainText, + }) => { + await focusEditor(page); + + const isCodeBlockActive = async () => { + return await evaluate(page, () => { + return document + .querySelector(`button[aria-label="Insert code block"]`) + .classList.contains('active'); + }); + }; + + // Toggle the code block on + await toggleInsertCodeBlock(page); + expect(await isCodeBlockActive()).toBe(true); + + // Toggle the code block off + await toggleInsertCodeBlock(page); + expect(await isCodeBlockActive()).toBe(false); + }); + + test('Can indent and outdent with the shortcuts', async ({ + page, + isPlainText, + }) => { + await focusEditor(page); + await page.keyboard.type('abc'); + await indent(page, 3); + + await assertHTML( + page, + html` +

    + abc +

    + `, + ); + + await outdent(page, 2); + + await assertHTML( + page, + html` +

    + abc +

    + `, + ); + + await outdent(page, 1); + + await assertHTML( + page, + html` +

    + abc +

    + `, + ); + }); +}); diff --git a/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs b/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs index f767c590698..41893cd7900 100644 --- a/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs +++ b/packages/lexical-playground/__tests__/keyboardShortcuts/index.mjs @@ -253,6 +253,22 @@ export async function toggleItalic(page) { await keyUpCtrlOrMeta(page); } +export async function toggleInsertCodeBlock(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('c'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + +export async function toggleStrikethrough(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('s'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + export async function pressShiftEnter(page) { await page.keyboard.down('Shift'); await page.keyboard.press('Enter'); @@ -288,3 +304,143 @@ export async function paste(page) { await page.keyboard.press('KeyV'); await keyUpCtrlOrMeta(page); } + +export async function toggleSubscript(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.press(','); + await keyUpCtrlOrMeta(page); +} + +export async function toggleSuperscript(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.press('.'); + await keyUpCtrlOrMeta(page); +} + +export async function clearFormatting(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.press('\\'); + await keyUpCtrlOrMeta(page); +} + +export async function leftAlign(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('l'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + +export async function centerAlign(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('e'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + +export async function rightAlign(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('r'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + +export async function justifyAlign(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + await page.keyboard.press('j'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + +export async function outdent(page, times = 1) { + for (let i = 0; i < times; i++) { + await keyDownCtrlOrMeta(page); + await page.keyboard.press('['); + await keyUpCtrlOrMeta(page); + } +} + +export async function indent(page, times = 1) { + for (let i = 0; i < times; i++) { + await keyDownCtrlOrMeta(page); + await page.keyboard.press(']'); + await keyUpCtrlOrMeta(page); + } +} + +export async function applyNormalFormat(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Alt'); + await page.keyboard.press('0'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Alt'); +} + +export async function applyHeading(page, level) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Alt'); + await page.keyboard.press(level.toString()); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Alt'); +} + +export async function toggleBulletList(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Alt'); + await page.keyboard.press('4'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Alt'); +} + +export async function toggleNumberedList(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Alt'); + await page.keyboard.press('5'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Alt'); +} + +export async function toggleChecklist(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Alt'); + await page.keyboard.press('6'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Alt'); +} + +export async function applyQuoteBlock(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Alt'); + await page.keyboard.press('q'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Alt'); +} + +export async function applyCodeBlock(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Alt'); + await page.keyboard.press('c'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Alt'); +} + +export async function increaseFontSize(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + // shift + '.' becomes '>' on US keyboard layout. See https://keycode.info/ + await page.keyboard.press('>'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} + +export async function decreaseFontSize(page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.down('Shift'); + // shift + ',' becomes '<' on US keyboard layout. See https://keycode.info/ + await page.keyboard.press('<'); + await keyUpCtrlOrMeta(page); + await page.keyboard.up('Shift'); +} diff --git a/packages/lexical-playground/src/App.tsx b/packages/lexical-playground/src/App.tsx index 3cdc15f505f..6f20d8c3841 100644 --- a/packages/lexical-playground/src/App.tsx +++ b/packages/lexical-playground/src/App.tsx @@ -24,6 +24,7 @@ import {FlashMessageContext} from './context/FlashMessageContext'; import {SettingsContext, useSettings} from './context/SettingsContext'; import {SharedAutocompleteContext} from './context/SharedAutocompleteContext'; import {SharedHistoryContext} from './context/SharedHistoryContext'; +import {ToolbarContext} from './context/ToolbarContext'; import Editor from './Editor'; import logo from './images/logo.svg'; import PlaygroundNodes from './nodes/PlaygroundNodes'; @@ -211,20 +212,22 @@ function App(): JSX.Element { -
    - - Lexical Logo - -
    -
    - -
    - - {isDevPlayground ? : null} - {isDevPlayground ? : null} - {isDevPlayground ? : null} + +
    + + Lexical Logo + +
    +
    + +
    + + {isDevPlayground ? : null} + {isDevPlayground ? : null} + {isDevPlayground ? : null} - {measureTypingPerf ? : null} + {measureTypingPerf ? : null} +
    diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index 31d8a38d433..2c4f0419575 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -12,6 +12,7 @@ import {CheckListPlugin} from '@lexical/react/LexicalCheckListPlugin'; import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin'; import {ClickableLinkPlugin} from '@lexical/react/LexicalClickableLinkPlugin'; import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin'; +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary'; import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin'; import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin'; @@ -59,6 +60,7 @@ import {MaxLengthPlugin} from './plugins/MaxLengthPlugin'; import MentionsPlugin from './plugins/MentionsPlugin'; import PageBreakPlugin from './plugins/PageBreakPlugin'; import PollPlugin from './plugins/PollPlugin'; +import ShortcutsPlugin from './plugins/ShortcutsPlugin'; import SpeechToTextPlugin from './plugins/SpeechToTextPlugin'; import TabFocusPlugin from './plugins/TabFocusPlugin'; import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin'; @@ -104,6 +106,8 @@ export default function Editor(): JSX.Element { useState(null); const [isSmallWidthViewport, setIsSmallWidthViewport] = useState(false); + const [editor] = useLexicalComposerContext(); + const [activeEditor, setActiveEditor] = useState(editor); const [isLinkEditMode, setIsLinkEditMode] = useState(false); const onRef = (_floatingAnchorElem: HTMLDivElement) => { @@ -131,7 +135,20 @@ export default function Editor(): JSX.Element { return ( <> - {isRichText && } + {isRichText && ( + + )} + {isRichText && ( + + )}
    = ToolbarState[Key]; + +type ContextShape = { + toolbarState: ToolbarState; + updateToolbarState( + key: Key, + value: ToolbarStateValue, + ): void; +}; + +const Context = createContext(undefined); + +export const ToolbarContext = ({ + children, +}: { + children: ReactNode; +}): JSX.Element => { + const [toolbarState, setToolbarState] = useState(INITIAL_TOOLBAR_STATE); + const selectionFontSize = toolbarState.fontSize; + + const updateToolbarState = useCallback( + (key: Key, value: ToolbarStateValue) => { + setToolbarState((prev) => ({ + ...prev, + [key]: value, + })); + }, + [], + ); + + useEffect(() => { + updateToolbarState('fontSizeInputValue', selectionFontSize.slice(0, -2)); + }, [selectionFontSize, updateToolbarState]); + + const contextValue = useMemo(() => { + return { + toolbarState, + updateToolbarState, + }; + }, [toolbarState, updateToolbarState]); + + return {children}; +}; + +export const useToolbarState = () => { + const context = useContext(Context); + + if (context === undefined) { + throw new Error('useToolbarState must be used within a ToolbarProvider'); + } + + return context; +}; diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index 40b443d0976..e5362290c69 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -751,9 +751,22 @@ i.page-break, min-width: 100px; } -.dropdown .item.fontsize-item, -.dropdown .item.fontsize-item .text { - min-width: unset; +.dropdown .item.wide { + align-items: center; + width: 248px; +} + +.dropdown .item.wide .icon-text-container { + display: flex; + + .text { + min-width: 120px; + } +} + +.dropdown .item .shortcut { + color: #939393; + self-align: flex-end; } .dropdown .item .active { diff --git a/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx b/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx new file mode 100644 index 00000000000..4549d8a10e8 --- /dev/null +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/index.tsx @@ -0,0 +1,168 @@ +/** + * 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 {TOGGLE_LINK_COMMAND} from '@lexical/link'; +import {HeadingTagType} from '@lexical/rich-text'; +import { + COMMAND_PRIORITY_NORMAL, + FORMAT_ELEMENT_COMMAND, + FORMAT_TEXT_COMMAND, + INDENT_CONTENT_COMMAND, + KEY_MODIFIER_COMMAND, + LexicalEditor, + OUTDENT_CONTENT_COMMAND, +} from 'lexical'; +import {Dispatch, useEffect} from 'react'; + +import {useToolbarState} from '../../context/ToolbarContext'; +import {sanitizeUrl} from '../../utils/url'; +import { + clearFormatting, + formatBulletList, + formatCheckList, + formatCode, + formatHeading, + formatNumberedList, + formatParagraph, + formatQuote, + updateFontSize, + UpdateFontSizeType, +} from '../ToolbarPlugin/utils'; +import { + isCenterAlign, + isClearFormatting, + isDecreaseFontSize, + isFormatBulletList, + isFormatCheckList, + isFormatCode, + isFormatHeading, + isFormatNumberedList, + isFormatParagraph, + isFormatQuote, + isIncreaseFontSize, + isIndent, + isInsertCodeBlock, + isInsertLink, + isJustifyAlign, + isLeftAlign, + isOutdent, + isRightAlign, + isStrikeThrough, + isSubscript, + isSuperscript, +} from './shortcuts'; + +export default function ShortcutsPlugin({ + editor, + setIsLinkEditMode, +}: { + editor: LexicalEditor; + setIsLinkEditMode: Dispatch; +}): null { + const {toolbarState} = useToolbarState(); + + useEffect(() => { + const keyboardShortcutsHandler = (payload: KeyboardEvent) => { + const event: KeyboardEvent = payload; + + if (isFormatParagraph(event)) { + event.preventDefault(); + formatParagraph(editor); + } else if (isFormatHeading(event)) { + event.preventDefault(); + const {code} = event; + const headingSize = `h${code[code.length - 1]}` as HeadingTagType; + formatHeading(editor, toolbarState.blockType, headingSize); + } else if (isFormatBulletList(event)) { + event.preventDefault(); + formatBulletList(editor, toolbarState.blockType); + } else if (isFormatNumberedList(event)) { + event.preventDefault(); + formatNumberedList(editor, toolbarState.blockType); + } else if (isFormatCheckList(event)) { + event.preventDefault(); + formatCheckList(editor, toolbarState.blockType); + } else if (isFormatCode(event)) { + event.preventDefault(); + formatCode(editor, toolbarState.blockType); + } else if (isFormatQuote(event)) { + event.preventDefault(); + formatQuote(editor, toolbarState.blockType); + } else if (isStrikeThrough(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough'); + } else if (isIndent(event)) { + event.preventDefault(); + editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined); + } else if (isOutdent(event)) { + event.preventDefault(); + editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined); + } else if (isCenterAlign(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center'); + } else if (isLeftAlign(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left'); + } else if (isRightAlign(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right'); + } else if (isJustifyAlign(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify'); + } else if (isSubscript(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript'); + } else if (isSuperscript(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript'); + } else if (isInsertCodeBlock(event)) { + event.preventDefault(); + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code'); + } else if (isIncreaseFontSize(event)) { + event.preventDefault(); + updateFontSize( + editor, + UpdateFontSizeType.increment, + toolbarState.fontSizeInputValue, + ); + } else if (isDecreaseFontSize(event)) { + event.preventDefault(); + updateFontSize( + editor, + UpdateFontSizeType.decrement, + toolbarState.fontSizeInputValue, + ); + } else if (isClearFormatting(event)) { + event.preventDefault(); + clearFormatting(editor); + } else if (isInsertLink(event)) { + event.preventDefault(); + const url = toolbarState.isLink ? null : sanitizeUrl('https://'); + setIsLinkEditMode(!toolbarState.isLink); + + editor.dispatchCommand(TOGGLE_LINK_COMMAND, url); + } + + return false; + }; + + return editor.registerCommand( + KEY_MODIFIER_COMMAND, + keyboardShortcutsHandler, + COMMAND_PRIORITY_NORMAL, + ); + }, [ + editor, + toolbarState.isLink, + toolbarState.blockType, + toolbarState.fontSizeInputValue, + setIsLinkEditMode, + ]); + + return null; +} diff --git a/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts new file mode 100644 index 00000000000..4a959f9dcac --- /dev/null +++ b/packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts @@ -0,0 +1,225 @@ +/** + * 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 {IS_APPLE} from 'shared/environment'; + +//disable eslint sorting rule for quick reference to shortcuts +/* eslint-disable sort-keys-fix/sort-keys-fix */ +export const SHORTCUTS = Object.freeze({ + // (Ctrl|⌘) + (Alt|Option) + shortcuts + NORMAL: IS_APPLE ? '⌘+Opt+0' : 'Ctrl+Alt+0', + HEADING1: IS_APPLE ? '⌘+Opt+1' : 'Ctrl+Alt+1', + HEADING2: IS_APPLE ? '⌘+Opt+2' : 'Ctrl+Alt+2', + HEADING3: IS_APPLE ? '⌘+Opt+3' : 'Ctrl+Alt+3', + BULLET_LIST: IS_APPLE ? '⌘+Opt+4' : 'Ctrl+Alt+4', + NUMBERED_LIST: IS_APPLE ? '⌘+Opt+5' : 'Ctrl+Alt+5', + CHECK_LIST: IS_APPLE ? '⌘+Opt+6' : 'Ctrl+Alt+6', + CODE_BLOCK: IS_APPLE ? '⌘+Opt+C' : 'Ctrl+Alt+C', + QUOTE: IS_APPLE ? '⌘+Opt+Q' : 'Ctrl+Alt+Q', + + // (Ctrl|⌘) + Shift + shortcuts + INCREASE_FONT_SIZE: IS_APPLE ? '⌘+Shift+.' : 'Ctrl+Shift+.', + DECREASE_FONT_SIZE: IS_APPLE ? '⌘+Shift+,' : 'Ctrl+Shift+,', + INSERT_CODE_BLOCK: IS_APPLE ? '⌘+Shift+C' : 'Ctrl+Shift+C', + STRIKETHROUGH: IS_APPLE ? '⌘+Shift+S' : 'Ctrl+Shift+S', + CENTER_ALIGN: IS_APPLE ? '⌘+Shift+E' : 'Ctrl+Shift+E', + JUSTIFY_ALIGN: IS_APPLE ? '⌘+Shift+J' : 'Ctrl+Shift+J', + LEFT_ALIGN: IS_APPLE ? '⌘+Shift+L' : 'Ctrl+Shift+L', + RIGHT_ALIGN: IS_APPLE ? '⌘+Shift+R' : 'Ctrl+Shift+R', + + // (Ctrl|⌘) + shortcuts + SUBSCRIPT: IS_APPLE ? '⌘+,' : 'Ctrl+,', + SUPERSCRIPT: IS_APPLE ? '⌘+.' : 'Ctrl+.', + INDENT: IS_APPLE ? '⌘+]' : 'Ctrl+]', + OUTDENT: IS_APPLE ? '⌘+[' : 'Ctrl+[', + CLEAR_FORMATTING: IS_APPLE ? '⌘+\\' : 'Ctrl+\\', + REDO: IS_APPLE ? '⌘+Shift+Z' : 'Ctrl+Y', + UNDO: IS_APPLE ? '⌘+Z' : 'Ctrl+Z', + BOLD: IS_APPLE ? '⌘+B' : 'Ctrl+B', + ITALIC: IS_APPLE ? '⌘+I' : 'Ctrl+I', + UNDERLINE: IS_APPLE ? '⌘+U' : 'Ctrl+U', + INSERT_LINK: IS_APPLE ? '⌘+K' : 'Ctrl+K', +}); + +export function controlOrMeta(metaKey: boolean, ctrlKey: boolean): boolean { + return IS_APPLE ? metaKey : ctrlKey; +} + +export function isFormatParagraph(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + + return ( + (code === 'Numpad0' || code === 'Digit0') && + !shiftKey && + altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isFormatHeading(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + const keyNumber = code[code.length - 1]; + + return ( + ['1', '2', '3'].includes(keyNumber) && + !shiftKey && + altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isFormatBulletList(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + (code === 'Numpad4' || code === 'Digit4') && + !shiftKey && + altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isFormatNumberedList(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + (code === 'Numpad5' || code === 'Digit5') && + !shiftKey && + altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isFormatCheckList(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + (code === 'Numpad6' || code === 'Digit6') && + !shiftKey && + altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isFormatCode(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'KeyC' && !shiftKey && altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isFormatQuote(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'KeyQ' && !shiftKey && altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isStrikeThrough(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'KeyS' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isIndent(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'BracketRight' && + !shiftKey && + !altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isOutdent(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'BracketLeft' && + !shiftKey && + !altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isCenterAlign(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'KeyE' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isLeftAlign(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'KeyL' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isRightAlign(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'KeyR' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isJustifyAlign(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'KeyJ' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isSubscript(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'Comma' && !shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isSuperscript(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'Period' && !shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isInsertCodeBlock(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'KeyC' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isIncreaseFontSize(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'Period' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isDecreaseFontSize(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'Comma' && shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isClearFormatting(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'Backslash' && + !shiftKey && + !altKey && + controlOrMeta(metaKey, ctrlKey) + ); +} + +export function isInsertLink(event: KeyboardEvent): boolean { + const {code, shiftKey, altKey, metaKey, ctrlKey} = event; + return ( + code === 'KeyK' && !shiftKey && !altKey && controlOrMeta(metaKey, ctrlKey) + ); +} diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/fontSize.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/fontSize.tsx index 1e1f5406820..ceb0dd408bd 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/fontSize.tsx +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/fontSize.tsx @@ -8,13 +8,19 @@ import './fontSize.css'; -import {$patchStyleText} from '@lexical/selection'; -import {$getSelection, LexicalEditor} from 'lexical'; +import {LexicalEditor} from 'lexical'; import * as React from 'react'; -const MIN_ALLOWED_FONT_SIZE = 8; -const MAX_ALLOWED_FONT_SIZE = 72; -const DEFAULT_FONT_SIZE = 15; +import { + MAX_ALLOWED_FONT_SIZE, + MIN_ALLOWED_FONT_SIZE, +} from '../../context/ToolbarContext'; +import {SHORTCUTS} from '../ShortcutsPlugin/shortcuts'; +import { + updateFontSize, + updateFontSizeInSelection, + UpdateFontSizeType, +} from './utils'; export function parseAllowedFontSize(input: string): string { const match = input.match(/^(\d+(?:\.\d+)?)px$/); @@ -27,12 +33,6 @@ export function parseAllowedFontSize(input: string): string { return ''; } -// eslint-disable-next-line no-shadow -enum updateFontSizeType { - increment = 1, - decrement, -} - export default function FontSize({ selectionFontSize, disabled, @@ -45,105 +45,6 @@ export default function FontSize({ const [inputValue, setInputValue] = React.useState(selectionFontSize); const [inputChangeFlag, setInputChangeFlag] = React.useState(false); - /** - * Calculates the new font size based on the update type. - * @param currentFontSize - The current font size - * @param updateType - The type of change, either increment or decrement - * @returns the next font size - */ - const calculateNextFontSize = ( - currentFontSize: number, - updateType: updateFontSizeType | null, - ) => { - if (!updateType) { - return currentFontSize; - } - - let updatedFontSize: number = currentFontSize; - switch (updateType) { - case updateFontSizeType.decrement: - switch (true) { - case currentFontSize > MAX_ALLOWED_FONT_SIZE: - updatedFontSize = MAX_ALLOWED_FONT_SIZE; - break; - case currentFontSize >= 48: - updatedFontSize -= 12; - break; - case currentFontSize >= 24: - updatedFontSize -= 4; - break; - case currentFontSize >= 14: - updatedFontSize -= 2; - break; - case currentFontSize >= 9: - updatedFontSize -= 1; - break; - default: - updatedFontSize = MIN_ALLOWED_FONT_SIZE; - break; - } - break; - - case updateFontSizeType.increment: - switch (true) { - case currentFontSize < MIN_ALLOWED_FONT_SIZE: - updatedFontSize = MIN_ALLOWED_FONT_SIZE; - break; - case currentFontSize < 12: - updatedFontSize += 1; - break; - case currentFontSize < 20: - updatedFontSize += 2; - break; - case currentFontSize < 36: - updatedFontSize += 4; - break; - case currentFontSize <= 60: - updatedFontSize += 12; - break; - default: - updatedFontSize = MAX_ALLOWED_FONT_SIZE; - break; - } - break; - - default: - break; - } - return updatedFontSize; - }; - /** - * Patches the selection with the updated font size. - */ - - const updateFontSizeInSelection = React.useCallback( - (newFontSize: string | null, updateType: updateFontSizeType | null) => { - const getNextFontSize = (prevFontSize: string | null): string => { - if (!prevFontSize) { - prevFontSize = `${DEFAULT_FONT_SIZE}px`; - } - prevFontSize = prevFontSize.slice(0, -2); - const nextFontSize = calculateNextFontSize( - Number(prevFontSize), - updateType, - ); - return `${nextFontSize}px`; - }; - - editor.update(() => { - if (editor.isEditable()) { - const selection = $getSelection(); - if (selection !== null) { - $patchStyleText(selection, { - 'font-size': newFontSize || getNextFontSize, - }); - } - } - }); - }, - [editor], - ); - const handleKeyPress = (e: React.KeyboardEvent) => { const inputValueNumber = Number(inputValue); @@ -170,18 +71,6 @@ export default function FontSize({ } }; - const handleButtonClick = (updateType: updateFontSizeType) => { - if (inputValue !== '') { - const nextFontSize = calculateNextFontSize( - Number(inputValue), - updateType, - ); - updateFontSizeInSelection(String(nextFontSize) + 'px', null); - } else { - updateFontSizeInSelection(null, updateType); - } - }; - const updateFontSizeByInputValue = (inputValueNumber: number) => { let updatedFontSize = inputValueNumber; if (inputValueNumber > MAX_ALLOWED_FONT_SIZE) { @@ -191,7 +80,7 @@ export default function FontSize({ } setInputValue(String(updatedFontSize)); - updateFontSizeInSelection(String(updatedFontSize) + 'px', null); + updateFontSizeInSelection(editor, String(updatedFontSize) + 'px', null); setInputChangeFlag(false); }; @@ -208,13 +97,18 @@ export default function FontSize({ (selectionFontSize !== '' && Number(inputValue) <= MIN_ALLOWED_FONT_SIZE) } - onClick={() => handleButtonClick(updateFontSizeType.decrement)} - className="toolbar-item font-decrement"> + onClick={() => + updateFontSize(editor, UpdateFontSizeType.decrement, inputValue) + } + className="toolbar-item font-decrement" + aria-label="Decrease font size" + title={`Decrease font size (${SHORTCUTS.DECREASE_FONT_SIZE})`}> = MAX_ALLOWED_FONT_SIZE) } - onClick={() => handleButtonClick(updateFontSizeType.increment)} - className="toolbar-item font-increment"> + onClick={() => + updateFontSize(editor, UpdateFontSizeType.increment, inputValue) + } + className="toolbar-item font-increment" + aria-label="Increase font size" + title={`Increase font size (${SHORTCUTS.INCREASE_FONT_SIZE})`}> diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx index 7ce9abbdbcc..ed5da202bca 100644 --- a/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx @@ -7,63 +7,42 @@ */ import { - $createCodeNode, $isCodeNode, CODE_LANGUAGE_FRIENDLY_NAME_MAP, CODE_LANGUAGE_MAP, getLanguageFriendlyName, } from '@lexical/code'; import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link'; -import { - $isListNode, - INSERT_CHECK_LIST_COMMAND, - INSERT_ORDERED_LIST_COMMAND, - INSERT_UNORDERED_LIST_COMMAND, - ListNode, -} from '@lexical/list'; +import {$isListNode, ListNode} from '@lexical/list'; import {INSERT_EMBED_COMMAND} from '@lexical/react/LexicalAutoEmbedPlugin'; -import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {$isDecoratorBlockNode} from '@lexical/react/LexicalDecoratorBlockNode'; import {INSERT_HORIZONTAL_RULE_COMMAND} from '@lexical/react/LexicalHorizontalRuleNode'; -import { - $createHeadingNode, - $createQuoteNode, - $isHeadingNode, - $isQuoteNode, - HeadingTagType, -} from '@lexical/rich-text'; +import {$isHeadingNode} from '@lexical/rich-text'; import { $getSelectionStyleValueForProperty, $isParentElementRTL, $patchStyleText, - $setBlocksType, } from '@lexical/selection'; import {$isTableNode, $isTableSelection} from '@lexical/table'; import { $findMatchingParent, - $getNearestBlockElementAncestorOrThrow, $getNearestNodeOfType, $isEditorIsNestedEditor, mergeRegister, } from '@lexical/utils'; import { - $createParagraphNode, $getNodeByKey, $getRoot, $getSelection, $isElementNode, $isRangeSelection, $isRootOrShadowRoot, - $isTextNode, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_CRITICAL, - COMMAND_PRIORITY_NORMAL, ElementFormatType, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, INDENT_CONTENT_COMMAND, - KEY_MODIFIER_COMMAND, LexicalEditor, NodeKey, OUTDENT_CONTENT_COMMAND, @@ -75,6 +54,10 @@ import {Dispatch, useCallback, useEffect, useState} from 'react'; import * as React from 'react'; import {IS_APPLE} from 'shared/environment'; +import { + blockTypeToBlockName, + useToolbarState, +} from '../../context/ToolbarContext'; import useModal from '../../hooks/useModal'; import catTypingGif from '../../images/cat-typing.gif'; import {$createStickyNode} from '../../nodes/StickyNode'; @@ -95,23 +78,19 @@ import {InsertInlineImageDialog} from '../InlineImagePlugin'; import InsertLayoutDialog from '../LayoutPlugin/InsertLayoutDialog'; import {INSERT_PAGE_BREAK} from '../PageBreakPlugin'; import {InsertPollDialog} from '../PollPlugin'; +import {SHORTCUTS} from '../ShortcutsPlugin/shortcuts'; import {InsertTableDialog} from '../TablePlugin'; import FontSize from './fontSize'; - -const blockTypeToBlockName = { - bullet: 'Bulleted List', - check: 'Check List', - code: 'Code Block', - h1: 'Heading 1', - h2: 'Heading 2', - h3: 'Heading 3', - h4: 'Heading 4', - h5: 'Heading 5', - h6: 'Heading 6', - number: 'Numbered List', - paragraph: 'Normal', - quote: 'Quote', -}; +import { + clearFormatting, + formatBulletList, + formatCheckList, + formatCode, + formatHeading, + formatNumberedList, + formatParagraph, + formatQuote, +} from './utils'; const rootTypeToRootName = { root: 'Root', @@ -213,79 +192,6 @@ function BlockFormatDropDown({ editor: LexicalEditor; disabled?: boolean; }): JSX.Element { - const formatParagraph = () => { - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $setBlocksType(selection, () => $createParagraphNode()); - } - }); - }; - - const formatHeading = (headingSize: HeadingTagType) => { - if (blockType !== headingSize) { - editor.update(() => { - const selection = $getSelection(); - $setBlocksType(selection, () => $createHeadingNode(headingSize)); - }); - } - }; - - const formatBulletList = () => { - if (blockType !== 'bullet') { - editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); - } else { - formatParagraph(); - } - }; - - const formatCheckList = () => { - if (blockType !== 'check') { - editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined); - } else { - formatParagraph(); - } - }; - - const formatNumberedList = () => { - if (blockType !== 'number') { - editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined); - } else { - formatParagraph(); - } - }; - - const formatQuote = () => { - if (blockType !== 'quote') { - editor.update(() => { - const selection = $getSelection(); - $setBlocksType(selection, () => $createQuoteNode()); - }); - } - }; - - const formatCode = () => { - if (blockType !== 'code') { - editor.update(() => { - let selection = $getSelection(); - - if (selection !== null) { - if (selection.isCollapsed()) { - $setBlocksType(selection, () => $createCodeNode()); - } else { - const textContent = selection.getTextContent(); - const codeNode = $createCodeNode(); - selection.insertNodes([codeNode]); - selection = $getSelection(); - if ($isRangeSelection(selection)) { - selection.insertRawText(textContent); - } - } - } - }); - } - }; - return ( - - Normal + className={ + 'item wide ' + dropDownActiveClass(blockType === 'paragraph') + } + onClick={() => formatParagraph(editor)}> +
    + + Normal +
    + {SHORTCUTS.NORMAL}
    formatHeading('h1')}> - - Heading 1 + className={'item wide ' + dropDownActiveClass(blockType === 'h1')} + onClick={() => formatHeading(editor, blockType, 'h1')}> +
    + + Heading 1 +
    + {SHORTCUTS.HEADING1}
    formatHeading('h2')}> - - Heading 2 + className={'item wide ' + dropDownActiveClass(blockType === 'h2')} + onClick={() => formatHeading(editor, blockType, 'h2')}> +
    + + Heading 2 +
    + {SHORTCUTS.HEADING2}
    formatHeading('h3')}> - - Heading 3 + className={'item wide ' + dropDownActiveClass(blockType === 'h3')} + onClick={() => formatHeading(editor, blockType, 'h3')}> +
    + + Heading 3 +
    + {SHORTCUTS.HEADING3}
    - - Bullet List + className={'item wide ' + dropDownActiveClass(blockType === 'bullet')} + onClick={() => formatBulletList(editor, blockType)}> +
    + + Bullet List +
    + {SHORTCUTS.BULLET_LIST}
    - - Numbered List + className={'item wide ' + dropDownActiveClass(blockType === 'number')} + onClick={() => formatNumberedList(editor, blockType)}> +
    + + Numbered List +
    + {SHORTCUTS.NUMBERED_LIST}
    - - Check List + className={'item wide ' + dropDownActiveClass(blockType === 'check')} + onClick={() => formatCheckList(editor, blockType)}> +
    + + Check List +
    + {SHORTCUTS.CHECK_LIST}
    - - Quote + className={'item wide ' + dropDownActiveClass(blockType === 'quote')} + onClick={() => formatQuote(editor, blockType)}> +
    + + Quote +
    + {SHORTCUTS.QUOTE}
    - - Code Block + className={'item wide ' + dropDownActiveClass(blockType === 'code')} + onClick={() => formatCode(editor, blockType)}> +
    + + Code Block +
    + {SHORTCUTS.CODE_BLOCK}
    ); @@ -436,39 +371,51 @@ function ElementFormatDropdown({ onClick={() => { editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left'); }} - className="item"> - - Left Align + className="item wide"> +
    + + Left Align +
    + {SHORTCUTS.LEFT_ALIGN} { editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center'); }} - className="item"> - - Center Align + className="item wide"> +
    + + Center Align +
    + {SHORTCUTS.CENTER_ALIGN}
    { editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right'); }} - className="item"> - - Right Align + className="item wide"> +
    + + Right Align +
    + {SHORTCUTS.RIGHT_ALIGN}
    { editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify'); }} - className="item"> - - Justify Align + className="item wide"> +
    + + Justify Align +
    + {SHORTCUTS.JUSTIFY_ALIGN}
    { editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'start'); }} - className="item"> + className="item wide"> { editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'end'); }} - className="item"> + className="item wide"> { editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined); }} - className="item"> - - Outdent + className="item wide"> +
    + + Outdent +
    + {SHORTCUTS.OUTDENT}
    { editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined); }} - className="item"> - - Indent + className="item wide"> +
    + + Indent +
    + {SHORTCUTS.INDENT}
    ); } export default function ToolbarPlugin({ + editor, + activeEditor, + setActiveEditor, setIsLinkEditMode, }: { + editor: LexicalEditor; + activeEditor: LexicalEditor; + setActiveEditor: Dispatch; setIsLinkEditMode: Dispatch; }): JSX.Element { - const [editor] = useLexicalComposerContext(); - const [activeEditor, setActiveEditor] = useState(editor); - const [blockType, setBlockType] = - useState('paragraph'); - const [rootType, setRootType] = - useState('root'); const [selectedElementKey, setSelectedElementKey] = useState( null, ); - const [fontSize, setFontSize] = useState('15px'); - const [fontColor, setFontColor] = useState('#000'); - const [bgColor, setBgColor] = useState('#fff'); - const [fontFamily, setFontFamily] = useState('Arial'); - const [elementFormat, setElementFormat] = useState('left'); - const [isLink, setIsLink] = useState(false); - const [isBold, setIsBold] = useState(false); - const [isItalic, setIsItalic] = useState(false); - const [isUnderline, setIsUnderline] = useState(false); - const [isStrikethrough, setIsStrikethrough] = useState(false); - const [isSubscript, setIsSubscript] = useState(false); - const [isSuperscript, setIsSuperscript] = useState(false); - const [isCode, setIsCode] = useState(false); - const [canUndo, setCanUndo] = useState(false); - const [canRedo, setCanRedo] = useState(false); const [modal, showModal] = useModal(); - const [isRTL, setIsRTL] = useState(false); - const [codeLanguage, setCodeLanguage] = useState(''); const [isEditable, setIsEditable] = useState(() => editor.isEditable()); - const [isImageCaption, setIsImageCaption] = useState(false); + const {toolbarState, updateToolbarState} = useToolbarState(); const $updateToolbar = useCallback(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { if (activeEditor !== editor && $isEditorIsNestedEditor(activeEditor)) { const rootElement = activeEditor.getRootElement(); - setIsImageCaption( + updateToolbarState( + 'isImageCaption', !!rootElement?.parentElement?.classList.contains( 'image-caption-container', ), ); } else { - setIsImageCaption(false); + updateToolbarState('isImageCaption', false); } const anchorNode = selection.anchor.getNode(); @@ -578,22 +515,19 @@ export default function ToolbarPlugin({ const elementKey = element.getKey(); const elementDOM = activeEditor.getElementByKey(elementKey); - setIsRTL($isParentElementRTL(selection)); + updateToolbarState('isRTL', $isParentElementRTL(selection)); // Update links const node = getSelectedNode(selection); const parent = node.getParent(); - if ($isLinkNode(parent) || $isLinkNode(node)) { - setIsLink(true); - } else { - setIsLink(false); - } + const isLink = $isLinkNode(parent) || $isLinkNode(node); + updateToolbarState('isLink', isLink); const tableNode = $findMatchingParent(node, $isTableNode); if ($isTableNode(tableNode)) { - setRootType('table'); + updateToolbarState('rootType', 'table'); } else { - setRootType('root'); + updateToolbarState('rootType', 'root'); } if (elementDOM !== null) { @@ -606,18 +540,23 @@ export default function ToolbarPlugin({ const type = parentList ? parentList.getListType() : element.getListType(); - setBlockType(type); + + updateToolbarState('blockType', type); } else { const type = $isHeadingNode(element) ? element.getTag() : element.getType(); if (type in blockTypeToBlockName) { - setBlockType(type as keyof typeof blockTypeToBlockName); + updateToolbarState( + 'blockType', + type as keyof typeof blockTypeToBlockName, + ); } if ($isCodeNode(element)) { const language = element.getLanguage() as keyof typeof CODE_LANGUAGE_MAP; - setCodeLanguage( + updateToolbarState( + 'codeLanguage', language ? CODE_LANGUAGE_MAP[language] || language : '', ); return; @@ -625,17 +564,20 @@ export default function ToolbarPlugin({ } } // Handle buttons - setFontColor( + updateToolbarState( + 'fontColor', $getSelectionStyleValueForProperty(selection, 'color', '#000'), ); - setBgColor( + updateToolbarState( + 'bgColor', $getSelectionStyleValueForProperty( selection, 'background-color', '#fff', ), ); - setFontFamily( + updateToolbarState( + 'fontFamily', $getSelectionStyleValueForProperty(selection, 'font-family', 'Arial'), ); let matchingParent; @@ -648,7 +590,8 @@ export default function ToolbarPlugin({ } // If matchingParent is a valid node, pass it's format type - setElementFormat( + updateToolbarState( + 'elementFormat', $isElementNode(matchingParent) ? matchingParent.getFormatType() : $isElementNode(node) @@ -658,19 +601,22 @@ export default function ToolbarPlugin({ } if ($isRangeSelection(selection) || $isTableSelection(selection)) { // Update text format - setIsBold(selection.hasFormat('bold')); - setIsItalic(selection.hasFormat('italic')); - setIsUnderline(selection.hasFormat('underline')); - setIsStrikethrough(selection.hasFormat('strikethrough')); - setIsSubscript(selection.hasFormat('subscript')); - setIsSuperscript(selection.hasFormat('superscript')); - setIsCode(selection.hasFormat('code')); - - setFontSize( + updateToolbarState('isBold', selection.hasFormat('bold')); + updateToolbarState('isItalic', selection.hasFormat('italic')); + updateToolbarState('isUnderline', selection.hasFormat('underline')); + updateToolbarState( + 'isStrikethrough', + selection.hasFormat('strikethrough'), + ); + updateToolbarState('isSubscript', selection.hasFormat('subscript')); + updateToolbarState('isSuperscript', selection.hasFormat('superscript')); + updateToolbarState('isCode', selection.hasFormat('code')); + updateToolbarState( + 'fontSize', $getSelectionStyleValueForProperty(selection, 'font-size', '15px'), ); } - }, [activeEditor, editor]); + }, [activeEditor, editor, updateToolbarState]); useEffect(() => { return editor.registerCommand( @@ -682,7 +628,7 @@ export default function ToolbarPlugin({ }, COMMAND_PRIORITY_CRITICAL, ); - }, [editor, $updateToolbar]); + }, [editor, $updateToolbar, setActiveEditor]); useEffect(() => { activeEditor.getEditorState().read(() => { @@ -703,7 +649,7 @@ export default function ToolbarPlugin({ activeEditor.registerCommand( CAN_UNDO_COMMAND, (payload) => { - setCanUndo(payload); + updateToolbarState('canUndo', payload); return false; }, COMMAND_PRIORITY_CRITICAL, @@ -711,38 +657,13 @@ export default function ToolbarPlugin({ activeEditor.registerCommand( CAN_REDO_COMMAND, (payload) => { - setCanRedo(payload); + updateToolbarState('canRedo', payload); return false; }, COMMAND_PRIORITY_CRITICAL, ), ); - }, [$updateToolbar, activeEditor, editor]); - - useEffect(() => { - return activeEditor.registerCommand( - KEY_MODIFIER_COMMAND, - (payload) => { - const event: KeyboardEvent = payload; - const {code, ctrlKey, metaKey} = event; - - if (code === 'KeyK' && (ctrlKey || metaKey)) { - event.preventDefault(); - let url: string | null; - if (!isLink) { - setIsLinkEditMode(true); - url = sanitizeUrl('https://'); - } else { - setIsLinkEditMode(false); - url = null; - } - return activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, url); - } - return false; - }, - COMMAND_PRIORITY_NORMAL, - ); - }, [activeEditor, isLink, setIsLinkEditMode]); + }, [$updateToolbar, activeEditor, editor, updateToolbarState]); const applyStyleText = useCallback( (styles: Record, skipHistoryStack?: boolean) => { @@ -759,62 +680,6 @@ export default function ToolbarPlugin({ [activeEditor], ); - const clearFormatting = useCallback(() => { - activeEditor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection) || $isTableSelection(selection)) { - const anchor = selection.anchor; - const focus = selection.focus; - const nodes = selection.getNodes(); - const extractedNodes = selection.extract(); - - if (anchor.key === focus.key && anchor.offset === focus.offset) { - return; - } - - nodes.forEach((node, idx) => { - // We split the first and last node by the selection - // So that we don't format unselected text inside those nodes - if ($isTextNode(node)) { - // Use a separate variable to ensure TS does not lose the refinement - let textNode = node; - if (idx === 0 && anchor.offset !== 0) { - textNode = textNode.splitText(anchor.offset)[1] || textNode; - } - if (idx === nodes.length - 1) { - textNode = textNode.splitText(focus.offset)[0] || textNode; - } - /** - * If the selected text has one format applied - * selecting a portion of the text, could - * clear the format to the wrong portion of the text. - * - * The cleared text is based on the length of the selected text. - */ - // We need this in case the selected text only has one format - const extractedTextNode = extractedNodes[0]; - if (nodes.length === 1 && $isTextNode(extractedTextNode)) { - textNode = extractedTextNode; - } - - if (textNode.__style !== '') { - textNode.setStyle(''); - } - if (textNode.__format !== 0) { - textNode.setFormat(0); - $getNearestBlockElementAncestorOrThrow(textNode).setFormat(''); - } - node = textNode; - } else if ($isHeadingNode(node) || $isQuoteNode(node)) { - node.replace($createParagraphNode(), true); - } else if ($isDecoratorBlockNode(node)) { - node.setFormat(''); - } - }); - } - }); - }, [activeEditor]); - const onFontColorSelect = useCallback( (value: string, skipHistoryStack: boolean) => { applyStyleText({color: value}, skipHistoryStack); @@ -830,7 +695,7 @@ export default function ToolbarPlugin({ ); const insertLink = useCallback(() => { - if (!isLink) { + if (!toolbarState.isLink) { setIsLinkEditMode(true); activeEditor.dispatchCommand( TOGGLE_LINK_COMMAND, @@ -840,7 +705,7 @@ export default function ToolbarPlugin({ setIsLinkEditMode(false); activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null); } - }, [activeEditor, isLink, setIsLinkEditMode]); + }, [activeEditor, setIsLinkEditMode, toolbarState.isLink]); const onCodeLanguageSelect = useCallback( (value: string) => { @@ -859,13 +724,13 @@ export default function ToolbarPlugin({ activeEditor.dispatchCommand(INSERT_IMAGE_COMMAND, payload); }; - const canViewerSeeInsertDropdown = !isImageCaption; - const canViewerSeeInsertCodeButton = !isImageCaption; + const canViewerSeeInsertDropdown = !toolbarState.isImageCaption; + const canViewerSeeInsertCodeButton = !toolbarState.isImageCaption; return (
    - {blockType in blockTypeToBlockName && activeEditor === editor && ( - <> - - - - )} - {blockType === 'code' ? ( + {toolbarState.blockType in blockTypeToBlockName && + activeEditor === editor && ( + <> + + + + )} + {toolbarState.blockType === 'code' ? ( {CODE_LANGUAGE_OPTIONS.map(([value, name]) => { return ( onCodeLanguageSelect(value)} key={value}> @@ -922,12 +788,12 @@ export default function ToolbarPlugin({ @@ -937,12 +803,12 @@ export default function ToolbarPlugin({ onClick={() => { activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold'); }} - className={'toolbar-item spaced ' + (isBold ? 'active' : '')} - title={IS_APPLE ? 'Bold (⌘B)' : 'Bold (Ctrl+B)'} + className={ + 'toolbar-item spaced ' + (toolbarState.isBold ? 'active' : '') + } + title={`Bold (${SHORTCUTS.BOLD})`} type="button" - aria-label={`Format text as bold. Shortcut: ${ - IS_APPLE ? '⌘B' : 'Ctrl+B' - }`}> + aria-label={`Format text as bold. Shortcut: ${SHORTCUTS.BOLD}`}> {canViewerSeeInsertCodeButton && ( @@ -977,8 +844,10 @@ export default function ToolbarPlugin({ onClick={() => { activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code'); }} - className={'toolbar-item spaced ' + (isCode ? 'active' : '')} - title="Insert code block" + className={ + 'toolbar-item spaced ' + (toolbarState.isCode ? 'active' : '') + } + title={`Insert code block (${SHORTCUTS.INSERT_CODE_BLOCK})`} type="button" aria-label="Insert code block"> @@ -987,9 +856,11 @@ export default function ToolbarPlugin({ @@ -998,7 +869,7 @@ export default function ToolbarPlugin({ buttonClassName="toolbar-item color-picker" buttonAriaLabel="Formatting text color" buttonIconClassName="icon font-color" - color={fontColor} + color={toolbarState.fontColor} onChange={onFontColorSelect} title="text color" /> @@ -1007,7 +878,7 @@ export default function ToolbarPlugin({ buttonClassName="toolbar-item color-picker" buttonAriaLabel="Formatting background color" buttonIconClassName="icon bg-color" - color={bgColor} + color={toolbarState.bgColor} onChange={onBgColorSelect} title="bg color" /> @@ -1024,21 +895,31 @@ export default function ToolbarPlugin({ 'strikethrough', ); }} - className={'item ' + dropDownActiveClass(isStrikethrough)} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isStrikethrough) + } title="Strikethrough" aria-label="Format text with a strikethrough"> - - Strikethrough +
    + + Strikethrough +
    + {SHORTCUTS.STRIKETHROUGH}
    { activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript'); }} - className={'item ' + dropDownActiveClass(isSubscript)} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isSubscript) + } title="Subscript" aria-label="Format text with a subscript"> - - Subscript +
    + + Subscript +
    + {SHORTCUTS.SUBSCRIPT}
    { @@ -1047,19 +928,27 @@ export default function ToolbarPlugin({ 'superscript', ); }} - className={'item ' + dropDownActiveClass(isSuperscript)} + className={ + 'item wide ' + dropDownActiveClass(toolbarState.isSuperscript) + } title="Superscript" aria-label="Format text with a superscript"> - - Superscript +
    + + Superscript +
    + {SHORTCUTS.SUPERSCRIPT}
    clearFormatting(activeEditor)} + className="item wide" title="Clear text formatting" aria-label="Clear all text formatting"> - - Clear Formatting +
    + + Clear Formatting +
    + {SHORTCUTS.CLEAR_FORMATTING}
    {canViewerSeeInsertDropdown && ( @@ -1236,9 +1125,9 @@ export default function ToolbarPlugin({ {modal} diff --git a/packages/lexical-playground/src/plugins/ToolbarPlugin/utils.ts b/packages/lexical-playground/src/plugins/ToolbarPlugin/utils.ts new file mode 100644 index 00000000000..8d0ca04d5e3 --- /dev/null +++ b/packages/lexical-playground/src/plugins/ToolbarPlugin/utils.ts @@ -0,0 +1,292 @@ +/** + * 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 {$createCodeNode} from '@lexical/code'; +import { + INSERT_CHECK_LIST_COMMAND, + INSERT_ORDERED_LIST_COMMAND, + INSERT_UNORDERED_LIST_COMMAND, +} from '@lexical/list'; +import {$isDecoratorBlockNode} from '@lexical/react/LexicalDecoratorBlockNode'; +import { + $createHeadingNode, + $createQuoteNode, + $isHeadingNode, + $isQuoteNode, + HeadingTagType, +} from '@lexical/rich-text'; +import {$patchStyleText, $setBlocksType} from '@lexical/selection'; +import {$isTableSelection} from '@lexical/table'; +import {$getNearestBlockElementAncestorOrThrow} from '@lexical/utils'; +import { + $createParagraphNode, + $getSelection, + $isRangeSelection, + $isTextNode, + LexicalEditor, +} from 'lexical'; + +import { + DEFAULT_FONT_SIZE, + MAX_ALLOWED_FONT_SIZE, + MIN_ALLOWED_FONT_SIZE, +} from '../../context/ToolbarContext'; + +// eslint-disable-next-line no-shadow +export enum UpdateFontSizeType { + increment = 1, + decrement, +} + +/** + * Calculates the new font size based on the update type. + * @param currentFontSize - The current font size + * @param updateType - The type of change, either increment or decrement + * @returns the next font size + */ +export const calculateNextFontSize = ( + currentFontSize: number, + updateType: UpdateFontSizeType | null, +) => { + if (!updateType) { + return currentFontSize; + } + + let updatedFontSize: number = currentFontSize; + switch (updateType) { + case UpdateFontSizeType.decrement: + switch (true) { + case currentFontSize > MAX_ALLOWED_FONT_SIZE: + updatedFontSize = MAX_ALLOWED_FONT_SIZE; + break; + case currentFontSize >= 48: + updatedFontSize -= 12; + break; + case currentFontSize >= 24: + updatedFontSize -= 4; + break; + case currentFontSize >= 14: + updatedFontSize -= 2; + break; + case currentFontSize >= 9: + updatedFontSize -= 1; + break; + default: + updatedFontSize = MIN_ALLOWED_FONT_SIZE; + break; + } + break; + + case UpdateFontSizeType.increment: + switch (true) { + case currentFontSize < MIN_ALLOWED_FONT_SIZE: + updatedFontSize = MIN_ALLOWED_FONT_SIZE; + break; + case currentFontSize < 12: + updatedFontSize += 1; + break; + case currentFontSize < 20: + updatedFontSize += 2; + break; + case currentFontSize < 36: + updatedFontSize += 4; + break; + case currentFontSize <= 60: + updatedFontSize += 12; + break; + default: + updatedFontSize = MAX_ALLOWED_FONT_SIZE; + break; + } + break; + + default: + break; + } + return updatedFontSize; +}; + +/** + * Patches the selection with the updated font size. + */ +export const updateFontSizeInSelection = ( + editor: LexicalEditor, + newFontSize: string | null, + updateType: UpdateFontSizeType | null, +) => { + const getNextFontSize = (prevFontSize: string | null): string => { + if (!prevFontSize) { + prevFontSize = `${DEFAULT_FONT_SIZE}px`; + } + prevFontSize = prevFontSize.slice(0, -2); + const nextFontSize = calculateNextFontSize( + Number(prevFontSize), + updateType, + ); + return `${nextFontSize}px`; + }; + + editor.update(() => { + if (editor.isEditable()) { + const selection = $getSelection(); + if (selection !== null) { + $patchStyleText(selection, { + 'font-size': newFontSize || getNextFontSize, + }); + } + } + }); +}; + +export const updateFontSize = ( + editor: LexicalEditor, + updateType: UpdateFontSizeType, + inputValue: string, +) => { + if (inputValue !== '') { + const nextFontSize = calculateNextFontSize(Number(inputValue), updateType); + updateFontSizeInSelection(editor, String(nextFontSize) + 'px', null); + } else { + updateFontSizeInSelection(editor, null, updateType); + } +}; + +export const formatParagraph = (editor: LexicalEditor) => { + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + $setBlocksType(selection, () => $createParagraphNode()); + } + }); +}; + +export const formatHeading = ( + editor: LexicalEditor, + blockType: string, + headingSize: HeadingTagType, +) => { + if (blockType !== headingSize) { + editor.update(() => { + const selection = $getSelection(); + $setBlocksType(selection, () => $createHeadingNode(headingSize)); + }); + } +}; + +export const formatBulletList = (editor: LexicalEditor, blockType: string) => { + if (blockType !== 'bullet') { + editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); + } else { + formatParagraph(editor); + } +}; + +export const formatCheckList = (editor: LexicalEditor, blockType: string) => { + if (blockType !== 'check') { + editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined); + } else { + formatParagraph(editor); + } +}; + +export const formatNumberedList = ( + editor: LexicalEditor, + blockType: string, +) => { + if (blockType !== 'number') { + editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined); + } else { + formatParagraph(editor); + } +}; + +export const formatQuote = (editor: LexicalEditor, blockType: string) => { + if (blockType !== 'quote') { + editor.update(() => { + const selection = $getSelection(); + $setBlocksType(selection, () => $createQuoteNode()); + }); + } +}; + +export const formatCode = (editor: LexicalEditor, blockType: string) => { + if (blockType !== 'code') { + editor.update(() => { + let selection = $getSelection(); + + if (selection !== null) { + if (selection.isCollapsed()) { + $setBlocksType(selection, () => $createCodeNode()); + } else { + const textContent = selection.getTextContent(); + const codeNode = $createCodeNode(); + selection.insertNodes([codeNode]); + selection = $getSelection(); + if ($isRangeSelection(selection)) { + selection.insertRawText(textContent); + } + } + } + }); + } +}; + +export const clearFormatting = (editor: LexicalEditor) => { + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection) || $isTableSelection(selection)) { + const anchor = selection.anchor; + const focus = selection.focus; + const nodes = selection.getNodes(); + const extractedNodes = selection.extract(); + + if (anchor.key === focus.key && anchor.offset === focus.offset) { + return; + } + + nodes.forEach((node, idx) => { + // We split the first and last node by the selection + // So that we don't format unselected text inside those nodes + if ($isTextNode(node)) { + // Use a separate variable to ensure TS does not lose the refinement + let textNode = node; + if (idx === 0 && anchor.offset !== 0) { + textNode = textNode.splitText(anchor.offset)[1] || textNode; + } + if (idx === nodes.length - 1) { + textNode = textNode.splitText(focus.offset)[0] || textNode; + } + /** + * If the selected text has one format applied + * selecting a portion of the text, could + * clear the format to the wrong portion of the text. + * + * The cleared text is based on the length of the selected text. + */ + // We need this in case the selected text only has one format + const extractedTextNode = extractedNodes[0]; + if (nodes.length === 1 && $isTextNode(extractedTextNode)) { + textNode = extractedTextNode; + } + + if (textNode.__style !== '') { + textNode.setStyle(''); + } + if (textNode.__format !== 0) { + textNode.setFormat(0); + $getNearestBlockElementAncestorOrThrow(textNode).setFormat(''); + } + node = textNode; + } else if ($isHeadingNode(node) || $isQuoteNode(node)) { + node.replace($createParagraphNode(), true); + } else if ($isDecoratorBlockNode(node)) { + node.setFormat(''); + } + }); + } + }); +}; From a4e70168a13271dfdaa8e2684f5fc99658f3236c Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Wed, 6 Nov 2024 02:01:45 +0000 Subject: [PATCH 07/12] Fix importDOM for Layout plugin (#6799) --- .../src/nodes/LayoutItemNode.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/lexical-playground/src/nodes/LayoutItemNode.ts b/packages/lexical-playground/src/nodes/LayoutItemNode.ts index d579b4ad6d8..3d227976d64 100644 --- a/packages/lexical-playground/src/nodes/LayoutItemNode.ts +++ b/packages/lexical-playground/src/nodes/LayoutItemNode.ts @@ -8,6 +8,7 @@ import type { DOMConversionMap, + DOMConversionOutput, EditorConfig, LexicalNode, SerializedElementNode, @@ -18,6 +19,10 @@ import {ElementNode} from 'lexical'; export type SerializedLayoutItemNode = SerializedElementNode; +function $convertLayoutItemElement(): DOMConversionOutput | null { + return {node: $createLayoutItemNode()}; +} + export class LayoutItemNode extends ElementNode { static getType(): string { return 'layout-item'; @@ -29,6 +34,7 @@ export class LayoutItemNode extends ElementNode { createDOM(config: EditorConfig): HTMLElement { const dom = document.createElement('div'); + dom.setAttribute('data-lexical-layout-item', 'true'); if (typeof config.theme.layoutItem === 'string') { addClassNamesToElement(dom, config.theme.layoutItem); } @@ -40,7 +46,17 @@ export class LayoutItemNode extends ElementNode { } static importDOM(): DOMConversionMap | null { - return {}; + return { + div: (domNode: HTMLElement) => { + if (!domNode.hasAttribute('data-lexical-layout-item')) { + return null; + } + return { + conversion: $convertLayoutItemElement, + priority: 2, + }; + }, + }; } static importJSON(): LayoutItemNode { From 8eae296ea39ff0dd707c901493553f1d889e9174 Mon Sep 17 00:00:00 2001 From: Rajiv Anisetti Date: Wed, 6 Nov 2024 18:38:32 -0500 Subject: [PATCH 08/12] Add optional selection argument to $getHtmlContent flow type (#6803) --- packages/lexical-clipboard/flow/LexicalClipboard.js.flow | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/lexical-clipboard/flow/LexicalClipboard.js.flow b/packages/lexical-clipboard/flow/LexicalClipboard.js.flow index db534fbbe43..6ca0ee622b0 100644 --- a/packages/lexical-clipboard/flow/LexicalClipboard.js.flow +++ b/packages/lexical-clipboard/flow/LexicalClipboard.js.flow @@ -24,7 +24,11 @@ declare export function $insertDataTransferForRichText( editor: LexicalEditor, ): void; -declare export function $getHtmlContent(editor: LexicalEditor): string | null; +declare export function $getHtmlContent( + editor: LexicalEditor, + selection?: BaseSelection, +): string | null; + declare export function $getLexicalContent( editor: LexicalEditor, ): string | null; From 76f8569211780ee7e913176d99d23eddd1cd3b6c Mon Sep 17 00:00:00 2001 From: Ajaezo Kingsley <54126417+Kingscliq@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:54:12 +0100 Subject: [PATCH 09/12] [lexical] Chore: Add export of DOMExportOutputMap from lexical (#6805) --- packages/lexical/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lexical/src/index.ts b/packages/lexical/src/index.ts index 0bc18239b37..d5c94e23453 100644 --- a/packages/lexical/src/index.ts +++ b/packages/lexical/src/index.ts @@ -42,6 +42,7 @@ export type { DOMConversionMap, DOMConversionOutput, DOMExportOutput, + DOMExportOutputMap, LexicalNode, NodeKey, NodeMap, From 29611d22b4bbb5cec41c625a03d2abda738d5e65 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Thu, 7 Nov 2024 15:59:38 -0800 Subject: [PATCH 10/12] v0.20.0 (#6809) Co-authored-by: Lexical GitHub Actions Bot <> --- CHANGELOG.md | 18 + examples/react-plain-text/package.json | 6 +- examples/react-rich-collab/package.json | 8 +- examples/react-rich/package.json | 6 +- examples/react-table/package.json | 6 +- examples/vanilla-js-plugin/package.json | 12 +- examples/vanilla-js/package.json | 12 +- package-lock.json | 446 +++++++++--------- package.json | 2 +- packages/lexical-clipboard/package.json | 12 +- packages/lexical-code/package.json | 6 +- packages/lexical-devtools-core/package.json | 14 +- packages/lexical-devtools/package.json | 6 +- packages/lexical-dragon/package.json | 4 +- packages/lexical-eslint-plugin/package.json | 2 +- packages/lexical-file/package.json | 4 +- packages/lexical-hashtag/package.json | 6 +- packages/lexical-headless/package.json | 4 +- packages/lexical-history/package.json | 6 +- packages/lexical-html/package.json | 8 +- packages/lexical-link/package.json | 6 +- packages/lexical-list/package.json | 6 +- packages/lexical-mark/package.json | 6 +- packages/lexical-markdown/package.json | 16 +- packages/lexical-offset/package.json | 4 +- packages/lexical-overflow/package.json | 4 +- packages/lexical-plain-text/package.json | 10 +- packages/lexical-playground/package.json | 32 +- packages/lexical-react/package.json | 40 +- packages/lexical-rich-text/package.json | 10 +- packages/lexical-selection/package.json | 4 +- packages/lexical-table/package.json | 8 +- packages/lexical-text/package.json | 4 +- packages/lexical-utils/package.json | 10 +- packages/lexical-website/package.json | 2 +- packages/lexical-yjs/package.json | 8 +- packages/lexical/package.json | 2 +- packages/shared/package.json | 4 +- .../lexical-esm-astro-react/package.json | 8 +- .../fixtures/lexical-esm-nextjs/package.json | 8 +- .../package.json | 12 +- 41 files changed, 405 insertions(+), 387 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1618f50e10d..e8614e6be2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## v0.20.0 (2024-11-07) + +- Add optional selection argument to getHtmlContent flow type (#6803) Rajiv Anisetti +- Fix importDOM for Layout plugin (#6799) Ivaylo Pavlov +- lexical-playground Feature Add more keyboard shortcuts (#6754) Bedru Umer +- lexical-website Documentation Update Add Documentation for html Property in Lexical Editor Configuration (#6770) Ajaezo Kingsley +- lexical-yjs Bug Fix clean up dangling text after undo in collaboration (#6670) Michael Shafer +- Lexical-website BugFix Change button text colour to improve visibility (#6796) Fadekemi Adebayo +- lexical-markdown Feature add ability to control finding the end of a node matched by TextMatchTransformer (#6681) Alessio Gravili +- lexical-react Bug Fix LexicalTypeaheadMenuPlugin SSR error ReferenceError document is not defined (#6794) Bedru Umer +- lexical-website Chore upgrade to Docusaurus v3.6 - Docusaurus Faster (#6761) Sbastien Lorber +- Bug Fix ContextMenu Paste option not preserving style (#6780) C. +- lexical-playground Fix the placement of the fontSize button in the ToolbarPlugin and hide the vertical scroll (Bug Fix) (#6786) Oluwasanya Olaoluwa +- #6768 fix to avoid infinite markdown shortcut matchers run (#6778) Maksim Horbachevsky +- lexical Feature Add onUpdate function during update with onUpdate (correct baselline) (#6773) Michael Landis +- v0.19.0 (#6774) Sherry +- v0.19.0 Lexical GitHub Actions Bot + ## v0.19.0 (2024-10-28) - lexical Add missing commands to Lexical.js.flow (#6769) Sherry diff --git a/examples/react-plain-text/package.json b/examples/react-plain-text/package.json index 09efd494a71..fde9b24aacf 100644 --- a/examples/react-plain-text/package.json +++ b/examples/react-plain-text/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-plain-text-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.19.0", - "lexical": "0.19.0", + "@lexical/react": "0.20.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react-rich-collab/package.json b/examples/react-rich-collab/package.json index a6255202a9e..da5cf1bef88 100644 --- a/examples/react-rich-collab/package.json +++ b/examples/react-rich-collab/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-rich-collab-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -12,9 +12,9 @@ "server:webrtc": "cross-env HOST=localhost PORT=1235 npx y-webrtc" }, "dependencies": { - "@lexical/react": "0.19.0", - "@lexical/yjs": "0.19.0", - "lexical": "0.19.0", + "@lexical/react": "0.20.0", + "@lexical/yjs": "0.20.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", "y-webrtc": "^10.3.0", diff --git a/examples/react-rich/package.json b/examples/react-rich/package.json index 994710af1a1..35918ba999d 100644 --- a/examples/react-rich/package.json +++ b/examples/react-rich/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-rich-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.19.0", - "lexical": "0.19.0", + "@lexical/react": "0.20.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/react-table/package.json b/examples/react-table/package.json index 9ae92f1d043..037fbd8e494 100644 --- a/examples/react-table/package.json +++ b/examples/react-table/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/react-table-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/react": "0.19.0", - "lexical": "0.19.0", + "@lexical/react": "0.20.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/vanilla-js-plugin/package.json b/examples/vanilla-js-plugin/package.json index dac39f57f09..403d0199954 100644 --- a/examples/vanilla-js-plugin/package.json +++ b/examples/vanilla-js-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/vanilla-js-plugin-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -9,12 +9,12 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/dragon": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/utils": "0.20.0", "emoji-datasource-facebook": "15.1.2", - "lexical": "0.19.0" + "lexical": "0.20.0" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/examples/vanilla-js/package.json b/examples/vanilla-js/package.json index 69930c7e448..89982850634 100644 --- a/examples/vanilla-js/package.json +++ b/examples/vanilla-js/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/vanilla-js-example", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", @@ -9,11 +9,11 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/dragon": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/dragon": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "devDependencies": { "typescript": "^5.2.2", diff --git a/package-lock.json b/package-lock.json index a296b7da9a4..b147a804740 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lexical/monorepo", - "version": "0.19.0", + "version": "0.20.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@lexical/monorepo", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "workspaces": [ "packages/*" @@ -38886,28 +38886,28 @@ } }, "packages/lexical": { - "version": "0.19.0", + "version": "0.20.0", "license": "MIT" }, "packages/lexical-clipboard": { "name": "@lexical/clipboard", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/html": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-code": { "name": "@lexical/code", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0", "prismjs": "^1.27.0" }, "devDependencies": { @@ -38916,7 +38916,7 @@ }, "packages/lexical-devtools": { "name": "@lexical/devtools", - "version": "0.19.0", + "version": "0.20.0", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^2.8.2", @@ -38933,12 +38933,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.19.0", + "@lexical/devtools-core": "0.20.0", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", - "lexical": "0.19.0", + "lexical": "0.20.0", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" @@ -38946,15 +38946,15 @@ }, "packages/lexical-devtools-core": { "name": "@lexical/devtools-core", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/html": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "react": ">=17.x", @@ -38963,15 +38963,15 @@ }, "packages/lexical-dragon": { "name": "@lexical/dragon", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-eslint-plugin": { "name": "@lexical/eslint-plugin", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "devDependencies": { "@types/eslint": "^8.56.9" @@ -38982,136 +38982,136 @@ }, "packages/lexical-file": { "name": "@lexical/file", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-hashtag": { "name": "@lexical/hashtag", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-headless": { "name": "@lexical/headless", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-history": { "name": "@lexical/history", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-html": { "name": "@lexical/html", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-link": { "name": "@lexical/link", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-list": { "name": "@lexical/list", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-mark": { "name": "@lexical/mark", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-markdown": { "name": "@lexical/markdown", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/code": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/code": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-offset": { "name": "@lexical/offset", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-overflow": { "name": "@lexical/overflow", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-plain-text": { "name": "@lexical/plain-text", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-playground": { - "version": "0.19.0", + "version": "0.20.0", "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/file": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/react": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/file": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/react": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", "katex": "^0.16.10", - "lexical": "0.19.0", + "lexical": "0.20.0", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -39134,28 +39134,28 @@ }, "packages/lexical-react": { "name": "@lexical/react", - "version": "0.19.0", - "license": "MIT", - "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/devtools-core": "0.19.0", - "@lexical/dragon": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/markdown": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "@lexical/yjs": "0.19.0", - "lexical": "0.19.0", + "version": "0.20.0", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/devtools-core": "0.20.0", + "@lexical/dragon": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/markdown": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "@lexical/yjs": "0.20.0", + "lexical": "0.20.0", "react-error-boundary": "^3.1.4" }, "peerDependencies": { @@ -39165,55 +39165,55 @@ }, "packages/lexical-rich-text": { "name": "@lexical/rich-text", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-selection": { "name": "@lexical/selection", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-table": { "name": "@lexical/table", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-text": { "name": "@lexical/text", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "packages/lexical-utils": { "name": "@lexical/utils", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "lexical": "0.19.0" + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "lexical": "0.20.0" } }, "packages/lexical-website": { "name": "@lexical/website", - "version": "0.19.0", + "version": "0.20.0", "dependencies": { "@docusaurus/core": "3.6.0", "@docusaurus/faster": "3.6.0", @@ -39243,12 +39243,12 @@ }, "packages/lexical-yjs": { "name": "@lexical/yjs", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "@lexical/offset": "0.19.0", - "@lexical/selection": "0.19.0", - "lexical": "0.19.0" + "@lexical/offset": "0.20.0", + "@lexical/selection": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -39281,10 +39281,10 @@ } }, "packages/shared": { - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } }, @@ -43914,19 +43914,19 @@ "@lexical/clipboard": { "version": "file:packages/lexical-clipboard", "requires": { - "@lexical/html": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/code": { "version": "file:packages/lexical-code", "requires": { - "@lexical/utils": "0.19.0", + "@lexical/utils": "0.20.0", "@types/prismjs": "^1.26.0", - "lexical": "0.19.0", + "lexical": "0.20.0", "prismjs": "^1.27.0" } }, @@ -43938,7 +43938,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@lexical/devtools-core": "0.19.0", + "@lexical/devtools-core": "0.20.0", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", @@ -43947,7 +43947,7 @@ "@webext-pegasus/store-zustand": "^0.3.0", "@webext-pegasus/transport": "^0.3.0", "framer-motion": "^11.1.5", - "lexical": "0.19.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.5", @@ -43959,18 +43959,18 @@ "@lexical/devtools-core": { "version": "file:packages/lexical-devtools-core", "requires": { - "@lexical/html": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/dragon": { "version": "file:packages/lexical-dragon", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/eslint-plugin": { @@ -43982,152 +43982,152 @@ "@lexical/file": { "version": "file:packages/lexical-file", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/hashtag": { "version": "file:packages/lexical-hashtag", "requires": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/headless": { "version": "file:packages/lexical-headless", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/history": { "version": "file:packages/lexical-history", "requires": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/html": { "version": "file:packages/lexical-html", "requires": { - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/link": { "version": "file:packages/lexical-link", "requires": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/list": { "version": "file:packages/lexical-list", "requires": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/mark": { "version": "file:packages/lexical-mark", "requires": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/markdown": { "version": "file:packages/lexical-markdown", "requires": { - "@lexical/code": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/code": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/offset": { "version": "file:packages/lexical-offset", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/overflow": { "version": "file:packages/lexical-overflow", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/plain-text": { "version": "file:packages/lexical-plain-text", "requires": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/react": { "version": "file:packages/lexical-react", "requires": { - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/devtools-core": "0.19.0", - "@lexical/dragon": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/markdown": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "@lexical/yjs": "0.19.0", - "lexical": "0.19.0", + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/devtools-core": "0.20.0", + "@lexical/dragon": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/markdown": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "@lexical/yjs": "0.20.0", + "lexical": "0.20.0", "react-error-boundary": "^3.1.4" } }, "@lexical/rich-text": { "version": "file:packages/lexical-rich-text", "requires": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/selection": { "version": "file:packages/lexical-selection", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/table": { "version": "file:packages/lexical-table", "requires": { - "@lexical/clipboard": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/text": { "version": "file:packages/lexical-text", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "@lexical/utils": { "version": "file:packages/lexical-utils", "requires": { - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "lexical": "0.19.0" + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "lexical": "0.20.0" } }, "@lexical/website": { @@ -44160,9 +44160,9 @@ "@lexical/yjs": { "version": "file:packages/lexical-yjs", "requires": { - "@lexical/offset": "0.19.0", - "@lexical/selection": "0.19.0", - "lexical": "0.19.0" + "@lexical/offset": "0.20.0", + "@lexical/selection": "0.20.0", + "lexical": "0.20.0" } }, "@mdx-js/mdx": { @@ -56203,26 +56203,26 @@ "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/file": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/react": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/file": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/react": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^25.0.7", "@types/lodash-es": "^4.14.182", "@vitejs/plugin-react": "^4.2.1", "katex": "^0.16.10", - "lexical": "0.19.0", + "lexical": "0.20.0", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", @@ -62509,7 +62509,7 @@ "shared": { "version": "file:packages/shared", "requires": { - "lexical": "0.19.0" + "lexical": "0.20.0" } }, "shebang-command": { diff --git a/package.json b/package.json index 8e1061ae939..ee24db444cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@lexical/monorepo", "description": "Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.", - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "private": true, "workspaces": [ diff --git a/packages/lexical-clipboard/package.json b/packages/lexical-clipboard/package.json index 59da212fb4d..f515b440818 100644 --- a/packages/lexical-clipboard/package.json +++ b/packages/lexical-clipboard/package.json @@ -9,15 +9,15 @@ "paste" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalClipboard.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-code/package.json b/packages/lexical-code/package.json index e436a96468e..7265a938997 100644 --- a/packages/lexical-code/package.json +++ b/packages/lexical-code/package.json @@ -8,12 +8,12 @@ "code" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalCode.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0", "prismjs": "^1.27.0" }, "repository": { diff --git a/packages/lexical-devtools-core/package.json b/packages/lexical-devtools-core/package.json index fc8e4f3c714..9361a8c699c 100644 --- a/packages/lexical-devtools-core/package.json +++ b/packages/lexical-devtools-core/package.json @@ -8,16 +8,16 @@ "utils" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalDevtoolsCore.js", "types": "index.d.ts", "dependencies": { - "@lexical/html": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/html": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "react": ">=17.x", diff --git a/packages/lexical-devtools/package.json b/packages/lexical-devtools/package.json index 406b826393c..ed014eed61f 100644 --- a/packages/lexical-devtools/package.json +++ b/packages/lexical-devtools/package.json @@ -2,7 +2,7 @@ "name": "@lexical/devtools", "description": "Lexical DevTools browser extension", "private": true, - "version": "0.19.0", + "version": "0.20.0", "type": "module", "scripts": { "dev": "wxt", @@ -41,12 +41,12 @@ "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@lexical/devtools-core": "0.19.0", + "@lexical/devtools-core": "0.20.0", "@rollup/plugin-babel": "^6.0.4", "@types/react": "^18.2.46", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", - "lexical": "0.19.0", + "lexical": "0.20.0", "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" diff --git a/packages/lexical-dragon/package.json b/packages/lexical-dragon/package.json index d446fa6e700..9d4c1e67857 100644 --- a/packages/lexical-dragon/package.json +++ b/packages/lexical-dragon/package.json @@ -9,7 +9,7 @@ "accessibility" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalDragon.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-eslint-plugin/package.json b/packages/lexical-eslint-plugin/package.json index d8311110bb0..01c8727bc8e 100644 --- a/packages/lexical-eslint-plugin/package.json +++ b/packages/lexical-eslint-plugin/package.json @@ -8,7 +8,7 @@ "lexical", "editor" ], - "version": "0.19.0", + "version": "0.20.0", "license": "MIT", "repository": { "type": "git", diff --git a/packages/lexical-file/package.json b/packages/lexical-file/package.json index 28e08173e4a..a19c36867f7 100644 --- a/packages/lexical-file/package.json +++ b/packages/lexical-file/package.json @@ -10,7 +10,7 @@ "export" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalFile.js", "types": "index.d.ts", "repository": { @@ -38,6 +38,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-hashtag/package.json b/packages/lexical-hashtag/package.json index 2a3b0cb6d42..e591e17fac4 100644 --- a/packages/lexical-hashtag/package.json +++ b/packages/lexical-hashtag/package.json @@ -8,12 +8,12 @@ "hashtag" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalHashtag.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-headless/package.json b/packages/lexical-headless/package.json index ac5bfbc81b0..100dcbbc90c 100644 --- a/packages/lexical-headless/package.json +++ b/packages/lexical-headless/package.json @@ -8,7 +8,7 @@ "headless" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalHeadless.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-history/package.json b/packages/lexical-history/package.json index 8c5fea30560..75cfee062ab 100644 --- a/packages/lexical-history/package.json +++ b/packages/lexical-history/package.json @@ -8,12 +8,12 @@ "history" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalHistory.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-html/package.json b/packages/lexical-html/package.json index b7310fcded0..77fbe5e4b88 100644 --- a/packages/lexical-html/package.json +++ b/packages/lexical-html/package.json @@ -8,7 +8,7 @@ "html" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalHtml.js", "types": "index.d.ts", "repository": { @@ -17,9 +17,9 @@ "directory": "packages/lexical-html" }, "dependencies": { - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "module": "LexicalHtml.mjs", "sideEffects": false, diff --git a/packages/lexical-link/package.json b/packages/lexical-link/package.json index 6bbc2f9f1d0..8c2363d556e 100644 --- a/packages/lexical-link/package.json +++ b/packages/lexical-link/package.json @@ -8,12 +8,12 @@ "link" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalLink.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-list/package.json b/packages/lexical-list/package.json index c75ffb5d8bd..b190078c9fb 100644 --- a/packages/lexical-list/package.json +++ b/packages/lexical-list/package.json @@ -8,12 +8,12 @@ "list" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalList.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-mark/package.json b/packages/lexical-mark/package.json index 3a81b001412..51dbfa99949 100644 --- a/packages/lexical-mark/package.json +++ b/packages/lexical-mark/package.json @@ -8,12 +8,12 @@ "mark" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalMark.js", "types": "index.d.ts", "dependencies": { - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-markdown/package.json b/packages/lexical-markdown/package.json index 796ba318519..a14fd94e555 100644 --- a/packages/lexical-markdown/package.json +++ b/packages/lexical-markdown/package.json @@ -8,17 +8,17 @@ "markdown" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalMarkdown.js", "types": "index.d.ts", "dependencies": { - "@lexical/code": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/code": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-offset/package.json b/packages/lexical-offset/package.json index bbfdee16989..46954e7f762 100644 --- a/packages/lexical-offset/package.json +++ b/packages/lexical-offset/package.json @@ -8,7 +8,7 @@ "offset" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalOffset.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-overflow/package.json b/packages/lexical-overflow/package.json index 26fbd14b150..faf7c4073ae 100644 --- a/packages/lexical-overflow/package.json +++ b/packages/lexical-overflow/package.json @@ -8,7 +8,7 @@ "overflow" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalOverflow.js", "types": "index.d.ts", "repository": { @@ -36,6 +36,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-plain-text/package.json b/packages/lexical-plain-text/package.json index ecaf6a53f53..e8045945b1e 100644 --- a/packages/lexical-plain-text/package.json +++ b/packages/lexical-plain-text/package.json @@ -7,7 +7,7 @@ "plain-text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalPlainText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } } diff --git a/packages/lexical-playground/package.json b/packages/lexical-playground/package.json index 32911a5d25d..244dbe2a692 100644 --- a/packages/lexical-playground/package.json +++ b/packages/lexical-playground/package.json @@ -1,6 +1,6 @@ { "name": "lexical-playground", - "version": "0.19.0", + "version": "0.20.0", "private": true, "type": "module", "scripts": { @@ -12,22 +12,22 @@ }, "dependencies": { "@excalidraw/excalidraw": "^0.17.0", - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/file": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/react": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/file": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/react": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/utils": "0.20.0", "katex": "^0.16.10", - "lexical": "0.19.0", + "lexical": "0.20.0", "lodash-es": "^4.17.21", "prettier": "^2.3.2", "react": "^18.2.0", diff --git a/packages/lexical-react/package.json b/packages/lexical-react/package.json index 24abcc61c8a..c01c40fd23d 100644 --- a/packages/lexical-react/package.json +++ b/packages/lexical-react/package.json @@ -8,27 +8,27 @@ "rich-text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/code": "0.19.0", - "@lexical/devtools-core": "0.19.0", - "@lexical/dragon": "0.19.0", - "@lexical/hashtag": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/link": "0.19.0", - "@lexical/list": "0.19.0", - "@lexical/mark": "0.19.0", - "@lexical/markdown": "0.19.0", - "@lexical/overflow": "0.19.0", - "@lexical/plain-text": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "@lexical/text": "0.19.0", - "@lexical/utils": "0.19.0", - "@lexical/yjs": "0.19.0", - "lexical": "0.19.0", + "@lexical/clipboard": "0.20.0", + "@lexical/code": "0.20.0", + "@lexical/devtools-core": "0.20.0", + "@lexical/dragon": "0.20.0", + "@lexical/hashtag": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/link": "0.20.0", + "@lexical/list": "0.20.0", + "@lexical/mark": "0.20.0", + "@lexical/markdown": "0.20.0", + "@lexical/overflow": "0.20.0", + "@lexical/plain-text": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "@lexical/text": "0.20.0", + "@lexical/utils": "0.20.0", + "@lexical/yjs": "0.20.0", + "lexical": "0.20.0", "react-error-boundary": "^3.1.4" }, "peerDependencies": { diff --git a/packages/lexical-rich-text/package.json b/packages/lexical-rich-text/package.json index 4745a432de2..972c69af447 100644 --- a/packages/lexical-rich-text/package.json +++ b/packages/lexical-rich-text/package.json @@ -7,7 +7,7 @@ "rich-text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalRichText.js", "types": "index.d.ts", "repository": { @@ -35,9 +35,9 @@ } }, "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" } } diff --git a/packages/lexical-selection/package.json b/packages/lexical-selection/package.json index 08c1002dd52..88fe2149704 100644 --- a/packages/lexical-selection/package.json +++ b/packages/lexical-selection/package.json @@ -9,7 +9,7 @@ "selection" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalSelection.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-table/package.json b/packages/lexical-table/package.json index d2164872321..8aba5552fe6 100644 --- a/packages/lexical-table/package.json +++ b/packages/lexical-table/package.json @@ -8,13 +8,13 @@ "table" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalTable.js", "types": "index.d.ts", "dependencies": { - "@lexical/clipboard": "0.19.0", - "@lexical/utils": "0.19.0", - "lexical": "0.19.0" + "@lexical/clipboard": "0.20.0", + "@lexical/utils": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-text/package.json b/packages/lexical-text/package.json index 1075e5979d8..bf8042fea89 100644 --- a/packages/lexical-text/package.json +++ b/packages/lexical-text/package.json @@ -9,7 +9,7 @@ "text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalText.js", "types": "index.d.ts", "repository": { @@ -37,6 +37,6 @@ } }, "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" } } diff --git a/packages/lexical-utils/package.json b/packages/lexical-utils/package.json index 4b10fbb739c..103f06f7ed3 100644 --- a/packages/lexical-utils/package.json +++ b/packages/lexical-utils/package.json @@ -8,14 +8,14 @@ "utils" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalUtils.js", "types": "index.d.ts", "dependencies": { - "@lexical/list": "0.19.0", - "@lexical/selection": "0.19.0", - "@lexical/table": "0.19.0", - "lexical": "0.19.0" + "@lexical/list": "0.20.0", + "@lexical/selection": "0.20.0", + "@lexical/table": "0.20.0", + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/packages/lexical-website/package.json b/packages/lexical-website/package.json index dd621c118f7..020be5e1ba7 100644 --- a/packages/lexical-website/package.json +++ b/packages/lexical-website/package.json @@ -1,6 +1,6 @@ { "name": "@lexical/website", - "version": "0.19.0", + "version": "0.20.0", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/lexical-yjs/package.json b/packages/lexical-yjs/package.json index d281b9c9393..497b72af328 100644 --- a/packages/lexical-yjs/package.json +++ b/packages/lexical-yjs/package.json @@ -11,13 +11,13 @@ "crdt" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "LexicalYjs.js", "types": "index.d.ts", "dependencies": { - "@lexical/offset": "0.19.0", - "@lexical/selection": "0.19.0", - "lexical": "0.19.0" + "@lexical/offset": "0.20.0", + "@lexical/selection": "0.20.0", + "lexical": "0.20.0" }, "peerDependencies": { "yjs": ">=13.5.22" diff --git a/packages/lexical/package.json b/packages/lexical/package.json index 35711998c37..385584c84a0 100644 --- a/packages/lexical/package.json +++ b/packages/lexical/package.json @@ -9,7 +9,7 @@ "rich-text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "main": "Lexical.js", "types": "index.d.ts", "repository": { diff --git a/packages/shared/package.json b/packages/shared/package.json index 3f145586bf2..c59c3f6537c 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -8,9 +8,9 @@ "rich-text" ], "license": "MIT", - "version": "0.19.0", + "version": "0.20.0", "dependencies": { - "lexical": "0.19.0" + "lexical": "0.20.0" }, "repository": { "type": "git", diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json index d8ecd2945c4..4cb6414e571 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-astro-react/package.json @@ -1,7 +1,7 @@ { "name": "lexical-esm-astro-react", "type": "module", - "version": "0.19.0", + "version": "0.20.0", "scripts": { "dev": "astro dev", "start": "astro dev", @@ -13,12 +13,12 @@ "dependencies": { "@astrojs/check": "^0.9.3", "@astrojs/react": "^3.1.0", - "@lexical/react": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/react": "0.20.0", + "@lexical/utils": "0.20.0", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "astro": "^4.5.4", - "lexical": "0.19.0", + "lexical": "0.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.4.2" diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json index a524ec4e1e1..f5d0384fc65 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-nextjs/package.json @@ -1,6 +1,6 @@ { "name": "lexical-esm-nextjs", - "version": "0.19.0", + "version": "0.20.0", "private": true, "scripts": { "dev": "next dev", @@ -9,9 +9,9 @@ "test": "playwright test" }, "dependencies": { - "@lexical/plain-text": "0.19.0", - "@lexical/react": "0.19.0", - "lexical": "0.19.0", + "@lexical/plain-text": "0.20.0", + "@lexical/react": "0.20.0", + "lexical": "0.20.0", "next": "^14.2.1", "react": "^18", "react-dom": "^18" diff --git a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json index 0a59b0510b6..df0007067f7 100644 --- a/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json +++ b/scripts/__tests__/integration/fixtures/lexical-esm-sveltekit-vanilla-js/package.json @@ -1,6 +1,6 @@ { "name": "lexical-sveltekit-vanilla-js", - "version": "0.19.0", + "version": "0.20.0", "private": true, "scripts": { "dev": "vite dev", @@ -9,17 +9,17 @@ "test": "playwright test" }, "devDependencies": { - "@lexical/dragon": "0.19.0", - "@lexical/history": "0.19.0", - "@lexical/rich-text": "0.19.0", - "@lexical/utils": "0.19.0", + "@lexical/dragon": "0.20.0", + "@lexical/history": "0.20.0", + "@lexical/rich-text": "0.20.0", + "@lexical/utils": "0.20.0", "@playwright/test": "^1.28.1", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^5.0.1", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", - "lexical": "0.19.0", + "lexical": "0.20.0", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", "svelte": "^4.2.19", From fd4aca766f74ce3a4cf95230b8a95169426c0e9d Mon Sep 17 00:00:00 2001 From: Ebad Date: Fri, 8 Nov 2024 16:14:32 +0000 Subject: [PATCH 11/12] [lexical-mark] Bug Fix: Stop MarkNode __ids array deep copy in clone (#6810) Co-authored-by: Ebad Salehi --- packages/lexical-mark/src/MarkNode.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/lexical-mark/src/MarkNode.ts b/packages/lexical-mark/src/MarkNode.ts index 3a91f1c30e1..a19bd626fc7 100644 --- a/packages/lexical-mark/src/MarkNode.ts +++ b/packages/lexical-mark/src/MarkNode.ts @@ -32,14 +32,14 @@ export type SerializedMarkNode = Spread< /** @noInheritDoc */ export class MarkNode extends ElementNode { /** @internal */ - __ids: Array; + __ids: readonly string[]; static getType(): string { return 'mark'; } static clone(node: MarkNode): MarkNode { - return new MarkNode(Array.from(node.__ids), node.__key); + return new MarkNode(node.__ids, node.__key); } static importDOM(): null { @@ -57,13 +57,13 @@ export class MarkNode extends ElementNode { exportJSON(): SerializedMarkNode { return { ...super.exportJSON(), - ids: this.getIDs(), + ids: Array.from(this.getIDs()), type: 'mark', version: 1, }; } - constructor(ids: Array, key?: NodeKey) { + constructor(ids: readonly string[], key?: NodeKey) { super(key); this.__ids = ids || []; } @@ -112,13 +112,13 @@ export class MarkNode extends ElementNode { getIDs(): Array { const self = this.getLatest(); - return $isMarkNode(self) ? self.__ids : []; + return $isMarkNode(self) ? Array.from(self.__ids) : []; } addID(id: string): void { const self = this.getWritable(); if ($isMarkNode(self)) { - const ids = self.__ids; + const ids = Array.from(self.__ids); self.__ids = ids; for (let i = 0; i < ids.length; i++) { // If we already have it, don't add again @@ -133,7 +133,7 @@ export class MarkNode extends ElementNode { deleteID(id: string): void { const self = this.getWritable(); if ($isMarkNode(self)) { - const ids = self.__ids; + const ids = Array.from(self.__ids); self.__ids = ids; for (let i = 0; i < ids.length; i++) { if (id === ids[i]) { @@ -197,7 +197,7 @@ export class MarkNode extends ElementNode { } } -export function $createMarkNode(ids: Array): MarkNode { +export function $createMarkNode(ids: readonly string[]): MarkNode { return $applyNodeReplacement(new MarkNode(ids)); } From 2bdaed5e434cc0ab33d417a2a7f27dbba5b2611e Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Mon, 11 Nov 2024 15:03:47 +0530 Subject: [PATCH 12/12] [lexical-list] Bug Fix: Handle appending inline element nodes in ListNode.append (#6791) --- packages/lexical-list/src/LexicalListNode.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/lexical-list/src/LexicalListNode.ts b/packages/lexical-list/src/LexicalListNode.ts index 45a87f30d2a..2af911c7a8e 100644 --- a/packages/lexical-list/src/LexicalListNode.ts +++ b/packages/lexical-list/src/LexicalListNode.ts @@ -200,8 +200,12 @@ export class ListNode extends ElementNode { if ($isListNode(currentNode)) { listItemNode.append(currentNode); } else if ($isElementNode(currentNode)) { - const textNode = $createTextNode(currentNode.getTextContent()); - listItemNode.append(textNode); + if (currentNode.isInline()) { + listItemNode.append(currentNode); + } else { + const textNode = $createTextNode(currentNode.getTextContent()); + listItemNode.append(textNode); + } } else { listItemNode.append(currentNode); }