From 05fb71bbf2c99a20efd064deaa6d87e7dea4f139 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Thu, 7 Sep 2023 15:55:40 +0300 Subject: [PATCH 01/54] [CARE-1802] Provide infra for extensions definitions --- .../extensions/ExtensionManagerProvider.tsx | 35 +++++++++++++++++++ .../slate-commons/src/extensions/context.tsx | 16 +++++++++ .../slate-commons/src/extensions/index.ts | 7 ++++ .../slate-commons/src/extensions/types.ts | 7 ++++ .../src/extensions/useExtendEditor.ts | 11 ++++++ .../src/extensions/useExtensions.ts | 9 +++++ .../src/extensions/useExtensionsManager.ts | 8 +++++ packages/slate-commons/src/index.ts | 1 + 8 files changed, 94 insertions(+) create mode 100644 packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx create mode 100644 packages/slate-commons/src/extensions/context.tsx create mode 100644 packages/slate-commons/src/extensions/index.ts create mode 100644 packages/slate-commons/src/extensions/types.ts create mode 100644 packages/slate-commons/src/extensions/useExtendEditor.ts create mode 100644 packages/slate-commons/src/extensions/useExtensions.ts create mode 100644 packages/slate-commons/src/extensions/useExtensionsManager.ts diff --git a/packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx b/packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx new file mode 100644 index 000000000..4a2289b21 --- /dev/null +++ b/packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx @@ -0,0 +1,35 @@ +import React, { useMemo, useState, type ReactNode } from 'react'; + +import type { Extension } from '../types'; + +import { ExtensionsContext, ExtensionsManagerContext } from './context'; +import type { ExtensionsManager } from './types'; + +interface Props { + children: ReactNode; +} + +export function ExtensionsManagerProvider({ children }: Props) { + type Entry = { extension: Extension }; + + const [entries, setEntries] = useState([]); + const [manager] = useState(() => ({ + register(extension) { + const entry = { extension }; + + setEntries((es) => [...es, entry]); + + return () => { + setEntries((es) => es.filter((e) => e !== entry)); + }; + }, + })); + + const extensions = useMemo(() => entries.map(({ extension }) => extension), [entries]); + + return ( + + {children} + + ); +} diff --git a/packages/slate-commons/src/extensions/context.tsx b/packages/slate-commons/src/extensions/context.tsx new file mode 100644 index 000000000..9c7dfc091 --- /dev/null +++ b/packages/slate-commons/src/extensions/context.tsx @@ -0,0 +1,16 @@ +import { createContext } from 'react'; + +import type { Extension } from '../types'; + +import type { ExtensionsManager } from './types'; + +const NULL_MANAGER: ExtensionsManager = { + register() { + throw new Error( + 'It is required to wrap code using ExtensionsManager into ExtensionsManagerProvider.', + ); + }, +}; + +export const ExtensionsManagerContext = createContext(NULL_MANAGER); +export const ExtensionsContext = createContext([]); diff --git a/packages/slate-commons/src/extensions/index.ts b/packages/slate-commons/src/extensions/index.ts new file mode 100644 index 000000000..808b93edd --- /dev/null +++ b/packages/slate-commons/src/extensions/index.ts @@ -0,0 +1,7 @@ +export type { ExtensionsManager } from './types'; + +export { useExtendEditor } from './useExtendEditor'; +export { useExtensions } from './useExtensions'; +export { useExtensionsManager } from './useExtensionsManager'; + +export { ExtensionsManagerProvider } from './ExtensionManagerProvider'; diff --git a/packages/slate-commons/src/extensions/types.ts b/packages/slate-commons/src/extensions/types.ts new file mode 100644 index 000000000..e470c869d --- /dev/null +++ b/packages/slate-commons/src/extensions/types.ts @@ -0,0 +1,7 @@ +import type { Extension } from '../types'; + +export interface ExtensionsManager { + register(extension: Extension): UnregisterFn; +} + +export type UnregisterFn = () => void; diff --git a/packages/slate-commons/src/extensions/useExtendEditor.ts b/packages/slate-commons/src/extensions/useExtendEditor.ts new file mode 100644 index 000000000..4b8290180 --- /dev/null +++ b/packages/slate-commons/src/extensions/useExtendEditor.ts @@ -0,0 +1,11 @@ +import { useEffect } from 'react'; + +import type { Extension } from '../types'; + +import { useExtensionsManager } from './useExtensionsManager'; + +export function useExtendEditor(extension: Extension): void { + const manager = useExtensionsManager(); + + useEffect(() => manager.register(extension), [manager, extension]); +} diff --git a/packages/slate-commons/src/extensions/useExtensions.ts b/packages/slate-commons/src/extensions/useExtensions.ts new file mode 100644 index 000000000..b3c7dac51 --- /dev/null +++ b/packages/slate-commons/src/extensions/useExtensions.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react'; + +import type { Extension } from '../types'; + +import { ExtensionsContext } from './context'; + +export function useExtensions(): Extension[] { + return useContext(ExtensionsContext); +} diff --git a/packages/slate-commons/src/extensions/useExtensionsManager.ts b/packages/slate-commons/src/extensions/useExtensionsManager.ts new file mode 100644 index 000000000..0c9cf234c --- /dev/null +++ b/packages/slate-commons/src/extensions/useExtensionsManager.ts @@ -0,0 +1,8 @@ +import { useContext } from 'react'; + +import { ExtensionsManagerContext } from './context'; +import type { ExtensionsManager } from './types'; + +export function useExtensionsManager(): ExtensionsManager { + return useContext(ExtensionsManagerContext); +} diff --git a/packages/slate-commons/src/index.ts b/packages/slate-commons/src/index.ts index dee8b75a7..bc95125c3 100644 --- a/packages/slate-commons/src/index.ts +++ b/packages/slate-commons/src/index.ts @@ -1,6 +1,7 @@ export * as EditorCommands from './commands'; export { createDeserializeElement, useSavedSelection, withoutNodes } from './lib'; export * from './constants'; +export * from './extensions'; export * from './plugins'; export * as Selection from './selection'; export * from './types'; From d5dc2c858c72e8b0fd20af05b6859d2cdfe4da4a Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Thu, 7 Sep 2023 16:02:21 +0300 Subject: [PATCH 02/54] [CARE-1802] Allow passing multiple extensions to `register()` and `useExtendEditor()` --- .../src/extensions/ExtensionManagerProvider.tsx | 8 +++++--- packages/slate-commons/src/extensions/types.ts | 2 +- packages/slate-commons/src/extensions/useExtendEditor.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx b/packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx index 4a2289b21..4c31adf50 100644 --- a/packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx +++ b/packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx @@ -15,12 +15,14 @@ export function ExtensionsManagerProvider({ children }: Props) { const [entries, setEntries] = useState([]); const [manager] = useState(() => ({ register(extension) { - const entry = { extension }; + const entries: Entry[] = (Array.isArray(extension) ? extension : [extension]).map( + (extension) => ({ extension }), + ); - setEntries((es) => [...es, entry]); + setEntries((es) => [...es, ...entries]); return () => { - setEntries((es) => es.filter((e) => e !== entry)); + setEntries((es) => es.filter((e) => !entries.includes(e))); }; }, })); diff --git a/packages/slate-commons/src/extensions/types.ts b/packages/slate-commons/src/extensions/types.ts index e470c869d..fd00c23ff 100644 --- a/packages/slate-commons/src/extensions/types.ts +++ b/packages/slate-commons/src/extensions/types.ts @@ -1,7 +1,7 @@ import type { Extension } from '../types'; export interface ExtensionsManager { - register(extension: Extension): UnregisterFn; + register(extension: Extension | Extension[]): UnregisterFn; } export type UnregisterFn = () => void; diff --git a/packages/slate-commons/src/extensions/useExtendEditor.ts b/packages/slate-commons/src/extensions/useExtendEditor.ts index 4b8290180..c824ab7fc 100644 --- a/packages/slate-commons/src/extensions/useExtendEditor.ts +++ b/packages/slate-commons/src/extensions/useExtendEditor.ts @@ -4,7 +4,7 @@ import type { Extension } from '../types'; import { useExtensionsManager } from './useExtensionsManager'; -export function useExtendEditor(extension: Extension): void { +export function useExtendEditor(extension: Extension | Extension[]): void { const manager = useExtensionsManager(); useEffect(() => manager.register(extension), [manager, extension]); From 0c38071594fda2700bbd40a91f63818829f8a770 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Thu, 7 Sep 2023 16:15:11 +0300 Subject: [PATCH 03/54] [CARE-1802] Use shallow compare for registering extensions --- .../slate-commons/src/extensions/index.ts | 2 +- .../src/extensions/useExtendEditor.ts | 11 ---- .../src/extensions/useRegisterExtension.ts | 52 +++++++++++++++++++ 3 files changed, 53 insertions(+), 12 deletions(-) delete mode 100644 packages/slate-commons/src/extensions/useExtendEditor.ts create mode 100644 packages/slate-commons/src/extensions/useRegisterExtension.ts diff --git a/packages/slate-commons/src/extensions/index.ts b/packages/slate-commons/src/extensions/index.ts index 808b93edd..47d749f05 100644 --- a/packages/slate-commons/src/extensions/index.ts +++ b/packages/slate-commons/src/extensions/index.ts @@ -1,7 +1,7 @@ export type { ExtensionsManager } from './types'; -export { useExtendEditor } from './useExtendEditor'; export { useExtensions } from './useExtensions'; export { useExtensionsManager } from './useExtensionsManager'; +export { useRegisterExtension } from './useRegisterExtension'; export { ExtensionsManagerProvider } from './ExtensionManagerProvider'; diff --git a/packages/slate-commons/src/extensions/useExtendEditor.ts b/packages/slate-commons/src/extensions/useExtendEditor.ts deleted file mode 100644 index c824ab7fc..000000000 --- a/packages/slate-commons/src/extensions/useExtendEditor.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useEffect } from 'react'; - -import type { Extension } from '../types'; - -import { useExtensionsManager } from './useExtensionsManager'; - -export function useExtendEditor(extension: Extension | Extension[]): void { - const manager = useExtensionsManager(); - - useEffect(() => manager.register(extension), [manager, extension]); -} diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts new file mode 100644 index 000000000..0fe0496f5 --- /dev/null +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -0,0 +1,52 @@ +import { useEffect } from 'react'; + +import type { Extension } from '../types'; + +import { useExtensionsManager } from './useExtensionsManager'; + +export function useRegisterExtension(extension: Extension): void { + const manager = useExtensionsManager(); + + const { + id, + decorate, + deserialize, + isElementEqual, + isInline, + isRichBlock, + isVoid, + normalizeNode, + onDOMBeforeInput, + onKeyDown, + renderElement, + renderLeaf, + serialize, + withOverrides, + ...rest + } = extension; + + if (Object.keys(rest).length > 0) { + throw new Error(`Unsupported keys passed: ${Object.keys(rest).join(', ')}.`); + } + + useEffect( + () => manager.register(extension), + [ + manager, + id, + decorate, + deserialize, + isElementEqual, + isInline, + isRichBlock, + isVoid, + normalizeNode, + onDOMBeforeInput, + onKeyDown, + renderElement, + renderLeaf, + serialize, + withOverrides, + ], + ); +} From 4770f5fc603f11e3bd3f5f1866894a4c030438d0 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 00:32:07 +0300 Subject: [PATCH 04/54] [CARE-1802] Convert existing extensions to React components Each using the `useRegisterExtension()` hook to wire into the editor functionality.  Conflicts:  packages/slate-editor/src/extensions/file-attachment/FileAttachmentExtension.tsx  packages/slate-editor/src/extensions/galleries/GalleriesExtension.tsx  packages/slate-editor/src/extensions/image/ImageExtension.tsx  packages/slate-editor/src/extensions/loader/LoaderExtension.tsx --- .../src/extensions/useRegisterExtension.ts | 4 +- .../allowed-blocks/AllowedBlocksExtension.ts | 8 +- .../autoformat/AutoformatExtension.ts | 8 +- .../blockquote/BlockquoteExtension.tsx | 8 +- .../button-block/ButtonBlockExtension.tsx | 8 +- .../extensions/coverage/CoverageExtension.tsx | 79 ++--- .../CustomNormalizationExtension.ts | 7 +- .../DecorateSelectionExtension.tsx | 8 +- .../extensions/divider/DividerExtension.tsx | 57 ++-- .../src/extensions/embed/EmbedExtension.tsx | 93 +++--- .../FileAttachmentExtension.tsx | 94 +++--- .../flash-nodes/FlashNodesExtension.tsx | 8 +- .../FloatingAddMenuExtension.tsx | 8 +- .../galleries/GalleriesExtension.tsx | 87 +++--- .../extensions/heading/HeadingExtension.tsx | 9 +- .../extensions/hotkeys/HotkeysExtension.ts | 8 +- .../src/extensions/html/HtmlExtension.tsx | 45 +-- .../src/extensions/image/ImageExtension.tsx | 284 +++++++++--------- .../InlineContactsExtension.tsx | 9 +- .../inline-links/InlineLinksExtension.tsx | 75 ++--- .../InsertBlockHotkeyExtension.ts | 8 +- .../src/extensions/list/ListExtension.tsx | 11 +- .../extensions/mentions/MentionsExtension.ts | 9 +- .../paragraphs/ParagraphsExtension.tsx | 55 ++-- .../paste-files/PasteFilesExtension.ts | 8 +- .../paste-images/PasteImagesExtension.ts | 8 +- .../PasteSlateContentExtension.ts | 10 +- .../placeholders/PlaceholdersExtension.tsx | 18 +- .../press-contacts/PressContactsExtension.tsx | 76 ++--- .../extensions/snippet/SnippetsExtension.tsx | 11 +- .../soft-break/SoftBreakExtension.ts | 8 +- .../story-bookmark/StoryBookmarkExtension.tsx | 71 ++--- .../story-embed/StoryEmbedExtension.tsx | 77 ++--- .../src/extensions/tables/TablesExtension.tsx | 26 +- .../text-styling/TextStylingExtension.tsx | 8 +- .../user-mentions/UserMentionsExtension.tsx | 42 +-- .../variables/VariablesExtension.tsx | 48 +-- .../src/extensions/video/VideoExtension.tsx | 9 +- .../src/extensions/void/VoidExtension.tsx | 9 +- .../web-bookmark/WebBookmarkExtension.tsx | 89 +++--- 40 files changed, 771 insertions(+), 737 deletions(-) diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts index 0fe0496f5..0b8708612 100644 --- a/packages/slate-commons/src/extensions/useRegisterExtension.ts +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -4,7 +4,7 @@ import type { Extension } from '../types'; import { useExtensionsManager } from './useExtensionsManager'; -export function useRegisterExtension(extension: Extension): void { +export function useRegisterExtension(extension: Extension): null { const manager = useExtensionsManager(); const { @@ -49,4 +49,6 @@ export function useRegisterExtension(extension: Extension): void { withOverrides, ], ); + + return null; } diff --git a/packages/slate-editor/src/extensions/allowed-blocks/AllowedBlocksExtension.ts b/packages/slate-editor/src/extensions/allowed-blocks/AllowedBlocksExtension.ts index 7f427a2c7..ace1a4494 100644 --- a/packages/slate-editor/src/extensions/allowed-blocks/AllowedBlocksExtension.ts +++ b/packages/slate-editor/src/extensions/allowed-blocks/AllowedBlocksExtension.ts @@ -1,4 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import { isElementNode } from '@prezly/slate-types'; import { type Editor, type NodeEntry, Transforms } from 'slate'; @@ -6,8 +6,8 @@ import type { AllowedBlocksExtensionConfiguration } from './types'; export const EXTENSION_ID = 'AllowedBlocksExtension'; -export function AllowedBlocksExtension({ check }: AllowedBlocksExtensionConfiguration): Extension { - return { +export function AllowedBlocksExtension({ check }: AllowedBlocksExtensionConfiguration) { + return useRegisterExtension({ id: EXTENSION_ID, normalizeNode(editor: Editor, [node, path]: NodeEntry) { if (path.length === 0) { @@ -28,5 +28,5 @@ export function AllowedBlocksExtension({ check }: AllowedBlocksExtensionConfigur return false; }, - }; + }); } diff --git a/packages/slate-editor/src/extensions/autoformat/AutoformatExtension.ts b/packages/slate-editor/src/extensions/autoformat/AutoformatExtension.ts index 700e65520..baa7cb0af 100644 --- a/packages/slate-editor/src/extensions/autoformat/AutoformatExtension.ts +++ b/packages/slate-editor/src/extensions/autoformat/AutoformatExtension.ts @@ -1,13 +1,13 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import type { AutoformatParameters } from './types'; import { withAutoformat } from './withAutoformat'; export const EXTENSION_ID = 'AutoformatExtension'; -export function AutoformatExtension(params: AutoformatParameters): Extension { - return { +export function AutoformatExtension(params: AutoformatParameters) { + return useRegisterExtension({ id: EXTENSION_ID, withOverrides: (editor) => withAutoformat(editor, params.rules), - }; + }); } diff --git a/packages/slate-editor/src/extensions/blockquote/BlockquoteExtension.tsx b/packages/slate-editor/src/extensions/blockquote/BlockquoteExtension.tsx index bb11b7f30..341f39117 100644 --- a/packages/slate-editor/src/extensions/blockquote/BlockquoteExtension.tsx +++ b/packages/slate-editor/src/extensions/blockquote/BlockquoteExtension.tsx @@ -1,4 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import { isQuoteNode, QUOTE_NODE_TYPE } from '@prezly/slate-types'; import React from 'react'; @@ -11,8 +11,8 @@ import { normalizeRedundantAttributes } from './lib'; export const EXTENSION_ID = 'BlockquoteExtension'; -export function BlockquoteExtension(): Extension { - return { +export function BlockquoteExtension() { + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { element: composeElementDeserializer({ @@ -35,5 +35,5 @@ export function BlockquoteExtension(): Extension { return undefined; }, withOverrides: withResetFormattingOnBreak(isQuoteNode), - }; + }); } diff --git a/packages/slate-editor/src/extensions/button-block/ButtonBlockExtension.tsx b/packages/slate-editor/src/extensions/button-block/ButtonBlockExtension.tsx index 5b906c208..d00e84178 100644 --- a/packages/slate-editor/src/extensions/button-block/ButtonBlockExtension.tsx +++ b/packages/slate-editor/src/extensions/button-block/ButtonBlockExtension.tsx @@ -1,4 +1,4 @@ -import { createDeserializeElement, type Extension } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import React from 'react'; import type { RenderElementProps } from 'slate-react'; @@ -24,8 +24,8 @@ export interface ButtonBlockExtensionConfiguration { export function ButtonBlockExtension({ withNewTabOption = true, info = [], -}: ButtonBlockExtensionConfiguration): Extension { - return { +}: ButtonBlockExtensionConfiguration) { + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { element: composeElementDeserializer({ @@ -51,5 +51,5 @@ export function ButtonBlockExtension({ }, isRichBlock: ButtonBlockNode.isButtonBlockNode, isVoid: ButtonBlockNode.isButtonBlockNode, - }; + }); } diff --git a/packages/slate-editor/src/extensions/coverage/CoverageExtension.tsx b/packages/slate-editor/src/extensions/coverage/CoverageExtension.tsx index 67cff15d6..ed9f9b654 100644 --- a/packages/slate-editor/src/extensions/coverage/CoverageExtension.tsx +++ b/packages/slate-editor/src/extensions/coverage/CoverageExtension.tsx @@ -1,5 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import { COVERAGE_NODE_TYPE, isCoverageNode } from '@prezly/slate-types'; import React from 'react'; @@ -13,41 +12,43 @@ export const EXTENSION_ID = 'CoverageExtension'; export interface Parameters extends CoverageExtensionConfiguration {} -export const CoverageExtension = ({ dateFormat, fetchCoverage }: Parameters): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [COVERAGE_NODE_TYPE]: createDeserializeElement(parseSerializedElement), - }), - }, - isElementEqual: (node, another) => { - if (isCoverageNode(node) && isCoverageNode(another)) { - return ( - node.coverage.id === another.coverage.id && - node.layout === another.layout && - node.new_tab === another.new_tab && - node.show_thumbnail === another.show_thumbnail - ); - } - return undefined; - }, - isRichBlock: isCoverageNode, - isVoid: isCoverageNode, - normalizeNode: normalizeRedundantCoverageAttributes, - renderElement: ({ attributes, children, element }) => { - if (isCoverageNode(element)) { - return ( - - {children} - - ); - } +export function CoverageExtension({ dateFormat, fetchCoverage }: Parameters) { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [COVERAGE_NODE_TYPE]: createDeserializeElement(parseSerializedElement), + }), + }, + isElementEqual: (node, another) => { + if (isCoverageNode(node) && isCoverageNode(another)) { + return ( + node.coverage.id === another.coverage.id && + node.layout === another.layout && + node.new_tab === another.new_tab && + node.show_thumbnail === another.show_thumbnail + ); + } + return undefined; + }, + isRichBlock: isCoverageNode, + isVoid: isCoverageNode, + normalizeNode: normalizeRedundantCoverageAttributes, + renderElement: ({ attributes, children, element }) => { + if (isCoverageNode(element)) { + return ( + + {children} + + ); + } - return undefined; - }, -}); + return undefined; + }, + }); +} diff --git a/packages/slate-editor/src/extensions/custom-normalization/CustomNormalizationExtension.ts b/packages/slate-editor/src/extensions/custom-normalization/CustomNormalizationExtension.ts index d682be12e..ed9ad5c94 100644 --- a/packages/slate-editor/src/extensions/custom-normalization/CustomNormalizationExtension.ts +++ b/packages/slate-editor/src/extensions/custom-normalization/CustomNormalizationExtension.ts @@ -1,12 +1,13 @@ import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; export const EXTENSION_ID = 'CustomNormalizationExtension'; export type ExtensionConfiguration = Pick; -export function CustomNormalizationExtension({ normalizeNode }: ExtensionConfiguration): Extension { - return { +export function CustomNormalizationExtension({ normalizeNode }: ExtensionConfiguration) { + return useRegisterExtension({ id: EXTENSION_ID, normalizeNode, - }; + }); } diff --git a/packages/slate-editor/src/extensions/decorate-selection/DecorateSelectionExtension.tsx b/packages/slate-editor/src/extensions/decorate-selection/DecorateSelectionExtension.tsx index e539998dd..a50f00011 100644 --- a/packages/slate-editor/src/extensions/decorate-selection/DecorateSelectionExtension.tsx +++ b/packages/slate-editor/src/extensions/decorate-selection/DecorateSelectionExtension.tsx @@ -1,4 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import React from 'react'; import type { Editor } from 'slate'; @@ -10,8 +10,8 @@ interface Params { decorate?: boolean; } -export function DecorateSelectionExtension({ decorate = true }: Params = {}): Extension { - return { +export function DecorateSelectionExtension({ decorate = true }: Params = {}) { + return useRegisterExtension({ id: 'DecorateSelectionExtension', decorate(editor: Editor) { if (!decorate) { @@ -26,7 +26,7 @@ export function DecorateSelectionExtension({ decorate = true }: Params = {}): Ex return <>{children}; }, - }; + }); } function noopDecoration() { diff --git a/packages/slate-editor/src/extensions/divider/DividerExtension.tsx b/packages/slate-editor/src/extensions/divider/DividerExtension.tsx index 85077fb0a..5c3288718 100644 --- a/packages/slate-editor/src/extensions/divider/DividerExtension.tsx +++ b/packages/slate-editor/src/extensions/divider/DividerExtension.tsx @@ -1,5 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import { DIVIDER_NODE_TYPE, isDividerNode } from '@prezly/slate-types'; import React from 'react'; import type { RenderElementProps } from 'slate-react'; @@ -11,31 +10,33 @@ import { createDivider, normalizeRedundantDividerAttributes, parseSerializedElem export const EXTENSION_ID = 'DividerExtension'; -export const DividerExtension = (): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [DIVIDER_NODE_TYPE]: createDeserializeElement(parseSerializedElement), - HR: (element) => { - if (element.getAttribute('data-is-slate')) { - return undefined; - } +export function DividerExtension() { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [DIVIDER_NODE_TYPE]: createDeserializeElement(parseSerializedElement), + HR: (element) => { + if (element.getAttribute('data-is-slate')) { + return undefined; + } - return createDivider(); - }, - }), - }, - isVoid: isDividerNode, - normalizeNode: normalizeRedundantDividerAttributes, - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isDividerNode(element)) { - return ( - - {children} - - ); - } + return createDivider(); + }, + }), + }, + isVoid: isDividerNode, + normalizeNode: normalizeRedundantDividerAttributes, + renderElement: ({ attributes, children, element }: RenderElementProps) => { + if (isDividerNode(element)) { + return ( + + {children} + + ); + } - return undefined; - }, -}); + return undefined; + }, + }); +} diff --git a/packages/slate-editor/src/extensions/embed/EmbedExtension.tsx b/packages/slate-editor/src/extensions/embed/EmbedExtension.tsx index 5140f1c48..0fdd0d1fa 100644 --- a/packages/slate-editor/src/extensions/embed/EmbedExtension.tsx +++ b/packages/slate-editor/src/extensions/embed/EmbedExtension.tsx @@ -1,6 +1,5 @@ import type { OEmbedInfo } from '@prezly/sdk'; -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import { isEqual } from '@technically/lodash'; import React from 'react'; import type { RenderElementProps } from 'slate-react'; @@ -30,7 +29,7 @@ export interface EmbedExtensionConfiguration { withConversionOptions?: boolean; } -export const EmbedExtension = ({ +export function EmbedExtension({ allowHtmlInjection, allowScreenshots, availableWidth, @@ -38,47 +37,49 @@ export const EmbedExtension = ({ withMenu = false, withLayoutControls = true, withConversionOptions = false, -}: Parameters): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [EmbedNode.TYPE]: createDeserializeElement(parseSerializedElement), - }), - }, - isElementEqual: (node, another) => { - if (EmbedNode.isEmbedNode(node) && EmbedNode.isEmbedNode(another)) { - return ( - node.url === another.url && - node.layout === another.layout && - isEqual(node.oembed, another.oembed) - ); - } - return undefined; - }, - isRichBlock: EmbedNode.isEmbedNode, - isVoid: EmbedNode.isEmbedNode, - normalizeNode: [fixUuidCollisions, normalizeRedundantEmbedAttributes], - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (EmbedNode.isEmbedNode(element)) { - return ( - <> - - {children} - - - ); - } +}: Parameters) { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [EmbedNode.TYPE]: createDeserializeElement(parseSerializedElement), + }), + }, + isElementEqual: (node, another) => { + if (EmbedNode.isEmbedNode(node) && EmbedNode.isEmbedNode(another)) { + return ( + node.url === another.url && + node.layout === another.layout && + isEqual(node.oembed, another.oembed) + ); + } + return undefined; + }, + isRichBlock: EmbedNode.isEmbedNode, + isVoid: EmbedNode.isEmbedNode, + normalizeNode: [fixUuidCollisions, normalizeRedundantEmbedAttributes], + renderElement: ({ attributes, children, element }: RenderElementProps) => { + if (EmbedNode.isEmbedNode(element)) { + return ( + <> + + {children} + + + ); + } - return undefined; - }, -}); + return undefined; + }, + }); +} diff --git a/packages/slate-editor/src/extensions/file-attachment/FileAttachmentExtension.tsx b/packages/slate-editor/src/extensions/file-attachment/FileAttachmentExtension.tsx index 8448848d3..cce95aed8 100644 --- a/packages/slate-editor/src/extensions/file-attachment/FileAttachmentExtension.tsx +++ b/packages/slate-editor/src/extensions/file-attachment/FileAttachmentExtension.tsx @@ -1,5 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import type { AttachmentNode } from '@prezly/slate-types'; import { ATTACHMENT_NODE_TYPE, isAttachmentNode } from '@prezly/slate-types'; import { isEqual, noop } from '@technically/lodash'; @@ -21,50 +20,49 @@ export interface Parameters { export const EXTENSION_ID = 'FileAttachmentExtension'; -export const FileAttachmentExtension = ({ - onEdited = noop, - onRemoved = noop, -}: Parameters): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [ATTACHMENT_NODE_TYPE]: createDeserializeElement(parseSerializedElement), - }), - }, - isElementEqual: (node, another) => { - if (isAttachmentNode(node) && isAttachmentNode(another)) { - // Compare ignoring `uuid` - return node.description === another.description && isEqual(node.file, another.file); - } - return undefined; - }, - isRichBlock: isAttachmentNode, - isVoid: isAttachmentNode, - normalizeNode: normalizeRedundantFileAttachmentAttributes, - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isAttachmentNode(element)) { - return ( - } - renderMenu={({ onClose }) => ( - - )} - rounded - void - /> - ); - } +export function FileAttachmentExtension({ onEdited = noop, onRemoved = noop }: Parameters) { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [ATTACHMENT_NODE_TYPE]: createDeserializeElement(parseSerializedElement), + }), + }, + isElementEqual: (node, another) => { + if (isAttachmentNode(node) && isAttachmentNode(another)) { + // Compare ignoring `uuid` + return node.description === another.description && isEqual(node.file, another.file); + } + return undefined; + }, + isRichBlock: isAttachmentNode, + isVoid: isAttachmentNode, + normalizeNode: normalizeRedundantFileAttachmentAttributes, + renderElement: ({ attributes, children, element }: RenderElementProps) => { + if (isAttachmentNode(element)) { + return ( + } + renderMenu={({ onClose }) => ( + + )} + rounded + void + /> + ); + } - return undefined; - }, -}); + return undefined; + }, + }); +} diff --git a/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx b/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx index 3c43a526d..c046d6719 100644 --- a/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx +++ b/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx @@ -1,7 +1,7 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; -export function FlashNodesExtension(): Extension { - return { +export function FlashNodesExtension() { + return useRegisterExtension({ id: 'FlashNodesExtension', withOverrides: (editor) => { editor.nodesToFlash = []; @@ -16,5 +16,5 @@ export function FlashNodesExtension(): Extension { return editor; }, - }; + }); } diff --git a/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenuExtension.tsx b/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenuExtension.tsx index 498ba1593..c0c43e6db 100644 --- a/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenuExtension.tsx +++ b/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenuExtension.tsx @@ -1,4 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import { isHotkey } from 'is-hotkey'; import { isMenuHotkey, MENU_TRIGGER_CHARACTER, shouldShowMenuButton } from './lib'; @@ -11,8 +11,8 @@ interface Parameters { onOpen: (trigger: 'hotkey' | 'input') => void; } -export function FloatingAddMenuExtension({ onOpen }: Parameters): Extension { - return { +export function FloatingAddMenuExtension({ onOpen }: Parameters) { + return useRegisterExtension({ id: EXTENSION_ID, onKeyDown(event, editor) { if (isMenuHotkey(event) && shouldShowMenuButton(editor)) { @@ -26,5 +26,5 @@ export function FloatingAddMenuExtension({ onOpen }: Parameters): Extension { } return false; }, - }; + }); } diff --git a/packages/slate-editor/src/extensions/galleries/GalleriesExtension.tsx b/packages/slate-editor/src/extensions/galleries/GalleriesExtension.tsx index c17d3220a..2e3b2a54c 100644 --- a/packages/slate-editor/src/extensions/galleries/GalleriesExtension.tsx +++ b/packages/slate-editor/src/extensions/galleries/GalleriesExtension.tsx @@ -1,6 +1,5 @@ import type { NewsroomRef } from '@prezly/sdk'; -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import type { GalleryNode } from '@prezly/slate-types'; import { GALLERY_NODE_TYPE, isGalleryNode } from '@prezly/slate-types'; import { isEqual } from '@technically/lodash'; @@ -34,50 +33,52 @@ interface Parameters { export const EXTENSION_ID = 'GalleriesExtension'; -export const GalleriesExtension = ({ +export function GalleriesExtension({ availableWidth, onEdited, onShuffled, withMediaGalleryTab, withLayoutOptions = false, -}: Parameters): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [GALLERY_NODE_TYPE]: createDeserializeElement(parseSerializedElement), - }), - }, - isElementEqual: (node, another) => { - if (isGalleryNode(node) && isGalleryNode(another)) { - return ( - node.layout === another.layout && - node.padding === another.padding && - node.thumbnail_size === another.thumbnail_size && - isEqual(node.images, another.images) - ); - } - return undefined; - }, - isRichBlock: isGalleryNode, - isVoid: isGalleryNode, - normalizeNode: [normalizeInvalidGallery, normalizeRedundantGalleryAttributes], - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isGalleryNode(element)) { - return ( - - {children} - - ); - } +}: Parameters) { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [GALLERY_NODE_TYPE]: createDeserializeElement(parseSerializedElement), + }), + }, + isElementEqual: (node, another) => { + if (isGalleryNode(node) && isGalleryNode(another)) { + return ( + node.layout === another.layout && + node.padding === another.padding && + node.thumbnail_size === another.thumbnail_size && + isEqual(node.images, another.images) + ); + } + return undefined; + }, + isRichBlock: isGalleryNode, + isVoid: isGalleryNode, + normalizeNode: [normalizeInvalidGallery, normalizeRedundantGalleryAttributes], + renderElement: ({ attributes, children, element }: RenderElementProps) => { + if (isGalleryNode(element)) { + return ( + + {children} + + ); + } - return undefined; - }, -}); + return undefined; + }, + }); +} diff --git a/packages/slate-editor/src/extensions/heading/HeadingExtension.tsx b/packages/slate-editor/src/extensions/heading/HeadingExtension.tsx index 6238cb72f..0b23b12a8 100644 --- a/packages/slate-editor/src/extensions/heading/HeadingExtension.tsx +++ b/packages/slate-editor/src/extensions/heading/HeadingExtension.tsx @@ -1,5 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import type { HeadingNode, HeadingRole } from '@prezly/slate-types'; import { HEADING_1_NODE_TYPE, @@ -20,8 +19,8 @@ import { normalizeRedundantAttributes, onTabSwitchBlock, parseHeadingElement } f export const EXTENSION_ID = 'HeadingExtension'; -export function HeadingExtension(): Extension { - return { +export function HeadingExtension() { + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { element: composeElementDeserializer({ @@ -53,7 +52,7 @@ export function HeadingExtension(): Extension { return undefined; }, withOverrides: withResetFormattingOnBreak(isHeadingNode), - }; + }); } function isTitleSubtitleNode(node: Node): node is HeadingNode & { role: HeadingRole } { diff --git a/packages/slate-editor/src/extensions/hotkeys/HotkeysExtension.ts b/packages/slate-editor/src/extensions/hotkeys/HotkeysExtension.ts index df5340a25..91077098e 100644 --- a/packages/slate-editor/src/extensions/hotkeys/HotkeysExtension.ts +++ b/packages/slate-editor/src/extensions/hotkeys/HotkeysExtension.ts @@ -1,14 +1,14 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import { onEscBlurEditor } from './onKeyDown'; export const EXTENSION_ID = 'HotkeysExtension'; -export function HotkeysExtension(): Extension { - return { +export function HotkeysExtension() { + return useRegisterExtension({ id: EXTENSION_ID, onKeyDown(event, editor) { return onEscBlurEditor(editor, event); }, - }; + }); } diff --git a/packages/slate-editor/src/extensions/html/HtmlExtension.tsx b/packages/slate-editor/src/extensions/html/HtmlExtension.tsx index 7caabbb5c..3f47172ea 100644 --- a/packages/slate-editor/src/extensions/html/HtmlExtension.tsx +++ b/packages/slate-editor/src/extensions/html/HtmlExtension.tsx @@ -1,5 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import { HTML_NODE_TYPE, isHtmlNode } from '@prezly/slate-types'; import React from 'react'; import type { RenderElementProps } from 'slate-react'; @@ -11,24 +10,26 @@ import { normalizeRedundantHtmlBlockAttributes, parseSerializedElement } from '. export const EXTENSION_ID = 'HtmlExtension'; -export const HtmlExtension = (): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [HTML_NODE_TYPE]: createDeserializeElement(parseSerializedElement), - }), - }, - isVoid: isHtmlNode, - normalizeNode: normalizeRedundantHtmlBlockAttributes, - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isHtmlNode(element)) { - return ( - - {children} - - ); - } +export function HtmlExtension() { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [HTML_NODE_TYPE]: createDeserializeElement(parseSerializedElement), + }), + }, + isVoid: isHtmlNode, + normalizeNode: normalizeRedundantHtmlBlockAttributes, + renderElement: ({ attributes, children, element }: RenderElementProps) => { + if (isHtmlNode(element)) { + return ( + + {children} + + ); + } - return undefined; - }, -}); + return undefined; + }, + }); +} diff --git a/packages/slate-editor/src/extensions/image/ImageExtension.tsx b/packages/slate-editor/src/extensions/image/ImageExtension.tsx index 6db2d229f..47bae546b 100644 --- a/packages/slate-editor/src/extensions/image/ImageExtension.tsx +++ b/packages/slate-editor/src/extensions/image/ImageExtension.tsx @@ -1,5 +1,8 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement, EditorCommands } from '@prezly/slate-commons'; +import { + createDeserializeElement, + EditorCommands, + useRegisterExtension, +} from '@prezly/slate-commons'; import type { ImageNode } from '@prezly/slate-types'; import { IMAGE_NODE_TYPE, isImageNode } from '@prezly/slate-types'; import { toProgressPromise, UploadcareImage } from '@prezly/uploadcare'; @@ -42,7 +45,7 @@ interface Parameters extends ImageExtensionConfiguration { export const EXTENSION_ID = 'ImageExtension'; -export const ImageExtension = ({ +export function ImageExtension({ onCrop = noop, onCropped = noop, onRemoved = noop, @@ -54,159 +57,164 @@ export const ImageExtension = ({ withMediaGalleryTab = false, withNewTabOption = true, withSizeOptions = false, -}: Parameters): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [IMAGE_NODE_TYPE]: createDeserializeElement(parseSerializedElement), - IMG: (element: HTMLElement): PlaceholderNode | undefined => { - const imageElement = element as HTMLImageElement; - const anchorElement = getAncestorAnchor(imageElement); - - function upload(src: string, alt?: string, href?: string) { - const filePromise = toFilePromise(src); - - if (!filePromise) { - return Promise.reject( - new Error(`Unable to upload an image from the given URL: ${src}`), - ); +}: Parameters) { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [IMAGE_NODE_TYPE]: createDeserializeElement(parseSerializedElement), + IMG: (element: HTMLElement): PlaceholderNode | undefined => { + const imageElement = element as HTMLImageElement; + const anchorElement = getAncestorAnchor(imageElement); + + function upload(src: string, alt?: string, href?: string) { + const filePromise = toFilePromise(src); + + if (!filePromise) { + return Promise.reject( + new Error(`Unable to upload an image from the given URL: ${src}`), + ); + } + + return toProgressPromise(filePromise).then((fileInfo) => { + const image = + UploadcareImage.createFromUploadcareWidgetPayload(fileInfo); + return { + image: createImage({ + file: image.toPrezlyStoragePayload(), + href, + children: [{ text: alt ?? '' }], + }), + operation: 'add' as const, + trigger: 'paste-html' as const, + }; + }); } - return toProgressPromise(filePromise).then((fileInfo) => { - const image = UploadcareImage.createFromUploadcareWidgetPayload(fileInfo); - return { - image: createImage({ - file: image.toPrezlyStoragePayload(), - href, - children: [{ text: alt ?? '' }], - }), - operation: 'add' as const, - trigger: 'paste-html' as const, - }; + const placeholder = createPlaceholder({ + type: PlaceholderNode.Type.IMAGE, }); - } - const placeholder = createPlaceholder({ - type: PlaceholderNode.Type.IMAGE, - }); - - const uploading = upload( - imageElement.src, - imageElement.alt ?? imageElement.title, - anchorElement && !anchorElement.textContent ? anchorElement.href : undefined, + const uploading = upload( + imageElement.src, + imageElement.alt ?? imageElement.title, + anchorElement && !anchorElement.textContent + ? anchorElement.href + : undefined, + ); + + PlaceholdersManager.register(placeholder.type, placeholder.uuid, uploading); + + return placeholder; + }, + }), + }, + isElementEqual: (node, another) => { + if (isImageNode(node) && isImageNode(another)) { + return ( + node.href === another.href && + node.layout === another.layout && + node.align === another.align && + node.new_tab === another.new_tab && + node.width === another.width && + isEqual(node.file, another.file) ); + } + return undefined; + }, + isRichBlock: isImageNode, + isVoid: (node) => { + if (isImageNode(node)) { + return !withCaptions; + } + return false; + }, + normalizeNode: normalizeRedundantImageAttributes, + onKeyDown: (event: KeyboardEvent, editor: Editor) => { + if (!withCaptions) { + return; + } - PlaceholdersManager.register(placeholder.type, placeholder.uuid, uploading); - - return placeholder; - }, - }), - }, - isElementEqual: (node, another) => { - if (isImageNode(node) && isImageNode(another)) { - return ( - node.href === another.href && - node.layout === another.layout && - node.align === another.align && - node.new_tab === another.new_tab && - node.width === another.width && - isEqual(node.file, another.file) - ); - } - return undefined; - }, - isRichBlock: isImageNode, - isVoid: (node) => { - if (isImageNode(node)) { - return !withCaptions; - } - return false; - }, - normalizeNode: normalizeRedundantImageAttributes, - onKeyDown: (event: KeyboardEvent, editor: Editor) => { - if (!withCaptions) { - return; - } - - if (isHotkey('enter', event.nativeEvent)) { - const nodeEntry = EditorCommands.getCurrentNodeEntry(editor); - if (nodeEntry && isImageNode(nodeEntry[0])) { - event.preventDefault(); + if (isHotkey('enter', event.nativeEvent)) { + const nodeEntry = EditorCommands.getCurrentNodeEntry(editor); + if (nodeEntry && isImageNode(nodeEntry[0])) { + event.preventDefault(); + + const nextPath = Path.next(nodeEntry[1]); + EditorCommands.insertEmptyParagraph(editor, { at: nextPath }); + Transforms.select(editor, nextPath); + return true; + } + } - const nextPath = Path.next(nodeEntry[1]); - EditorCommands.insertEmptyParagraph(editor, { at: nextPath }); - Transforms.select(editor, nextPath); + if (isHotkey('shift+enter', event.nativeEvent) && !event.isDefaultPrevented()) { + event.preventDefault(); + Transforms.insertText(editor, '\n'); return true; } - } - if (isHotkey('shift+enter', event.nativeEvent) && !event.isDefaultPrevented()) { - event.preventDefault(); - Transforms.insertText(editor, '\n'); - return true; - } + if (isDeletingEvent(event)) { + const nodeEntry = EditorCommands.getCurrentNodeEntry(editor); + const now = Date.now(); + const isHoldingDelete = now - lastBackspaceTimestamp <= HOLDING_BACKSPACE_THRESHOLD; + lastBackspaceTimestamp = now; + + if (!nodeEntry || !isImageNode(nodeEntry[0])) { + return; + } - if (isDeletingEvent(event)) { - const nodeEntry = EditorCommands.getCurrentNodeEntry(editor); - const now = Date.now(); - const isHoldingDelete = now - lastBackspaceTimestamp <= HOLDING_BACKSPACE_THRESHOLD; - lastBackspaceTimestamp = now; + if (EditorCommands.isNodeEmpty(editor, nodeEntry[0])) { + if (!isHoldingDelete) { + replaceImageWithParagraph(editor, nodeEntry[1]); + } - if (!nodeEntry || !isImageNode(nodeEntry[0])) { - return; - } + event.preventDefault(); + event.stopPropagation(); + return true; + } - if (EditorCommands.isNodeEmpty(editor, nodeEntry[0])) { - if (!isHoldingDelete) { + if ( + isDeletingEventBackward(event) && + EditorCommands.isSelectionAtBlockStart(editor) && + EditorCommands.isSelectionEmpty(editor) + ) { replaceImageWithParagraph(editor, nodeEntry[1]); + event.preventDefault(); + event.stopPropagation(); + return true; } - - event.preventDefault(); - event.stopPropagation(); - return true; } - if ( - isDeletingEventBackward(event) && - EditorCommands.isSelectionAtBlockStart(editor) && - EditorCommands.isSelectionEmpty(editor) - ) { - replaceImageWithParagraph(editor, nodeEntry[1]); - event.preventDefault(); - event.stopPropagation(); - return true; + return false; + }, + renderElement: ({ attributes, children, element }: RenderElementProps) => { + if (isImageNode(element)) { + return ( + + {children} + + ); } - } - - return false; - }, - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isImageNode(element)) { - return ( - - {children} - - ); - } - - return undefined; - }, - withOverrides: withImages, -}); + + return undefined; + }, + withOverrides: withImages, + }); +} function replaceImageWithParagraph(editor: Editor, at: Path) { EditorCommands.replaceNode(editor, createParagraph(), { at, match: isImageNode }); diff --git a/packages/slate-editor/src/extensions/inline-contacts/InlineContactsExtension.tsx b/packages/slate-editor/src/extensions/inline-contacts/InlineContactsExtension.tsx index 289b63ab2..a65e5e208 100644 --- a/packages/slate-editor/src/extensions/inline-contacts/InlineContactsExtension.tsx +++ b/packages/slate-editor/src/extensions/inline-contacts/InlineContactsExtension.tsx @@ -1,5 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import { CONTACT_NODE_TYPE, isContactNode } from '@prezly/slate-types'; import { isEqual } from '@technically/lodash'; import React from 'react'; @@ -16,8 +15,8 @@ import { InlineContactElement } from './components'; export const EXTENSION_ID = 'InlineContactExtension'; -export function InlineContactsExtension(): Extension { - return { +export function InlineContactsExtension() { + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { element: composeElementDeserializer({ @@ -49,5 +48,5 @@ export function InlineContactsExtension(): Extension { return undefined; }, - }; + }); } diff --git a/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx b/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx index 4c5285c42..327d000bb 100644 --- a/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx +++ b/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx @@ -1,5 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import type { LinkNode } from '@prezly/slate-types'; import { isLinkNode, LINK_NODE_TYPE } from '@prezly/slate-types'; import { flow } from '@technically/lodash'; @@ -20,40 +19,42 @@ import { export const EXTENSION_ID = 'InlineLinksExtension'; -export const InlineLinksExtension = (): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [LINK_NODE_TYPE]: createDeserializeElement(parseSerializedLinkElement), - A: (element: HTMLElement): Omit | undefined => { - if (element instanceof HTMLAnchorElement && element.textContent) { - return { - type: LINK_NODE_TYPE, - href: element.href, - new_tab: Boolean(element.getAttribute('target')), - }; - } +export function InlineLinksExtension() { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [LINK_NODE_TYPE]: createDeserializeElement(parseSerializedLinkElement), + A: (element: HTMLElement): Omit | undefined => { + if (element instanceof HTMLAnchorElement && element.textContent) { + return { + type: LINK_NODE_TYPE, + href: element.href, + new_tab: Boolean(element.getAttribute('target')), + }; + } - return undefined; - }, - }), - }, - isInline: isLinkNode, - normalizeNode: [normalizeEmptyLink, normalizeNestedLink, normalizeRedundantLinkAttributes], - onKeyDown: function (event, editor) { - escapeLinksBoundaries(event, editor); - }, - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isLinkNode(element)) { - return ( - - {children} - - ); - } + return undefined; + }, + }), + }, + isInline: isLinkNode, + normalizeNode: [normalizeEmptyLink, normalizeNestedLink, normalizeRedundantLinkAttributes], + onKeyDown: function (event, editor) { + escapeLinksBoundaries(event, editor); + }, + renderElement: ({ attributes, children, element }: RenderElementProps) => { + if (isLinkNode(element)) { + return ( + + {children} + + ); + } - return undefined; - }, - withOverrides: (editor) => - flow([withPastedContentAutolinking, withSelectionAutolinking])(editor), -}); + return undefined; + }, + withOverrides: (editor) => + flow([withPastedContentAutolinking, withSelectionAutolinking])(editor), + }); +} diff --git a/packages/slate-editor/src/extensions/insert-block-hotkey/InsertBlockHotkeyExtension.ts b/packages/slate-editor/src/extensions/insert-block-hotkey/InsertBlockHotkeyExtension.ts index c91c6e8b9..b879e1a6f 100644 --- a/packages/slate-editor/src/extensions/insert-block-hotkey/InsertBlockHotkeyExtension.ts +++ b/packages/slate-editor/src/extensions/insert-block-hotkey/InsertBlockHotkeyExtension.ts @@ -1,4 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import { noop } from '@technically/lodash'; import { isHotkey } from 'is-hotkey'; import type { Editor, Element } from 'slate'; @@ -18,8 +18,8 @@ interface Parameters { export function InsertBlockHotkeyExtension({ createDefaultElement, onInserted = noop, -}: Parameters): Extension { - return { +}: Parameters) { + return useRegisterExtension({ id: EXTENSION_ID, onKeyDown: (event, editor) => { if (isShiftModEnter(event) && insertBlockAbove(editor, createDefaultElement)) { @@ -33,5 +33,5 @@ export function InsertBlockHotkeyExtension({ } return false; }, - }; + }); } diff --git a/packages/slate-editor/src/extensions/list/ListExtension.tsx b/packages/slate-editor/src/extensions/list/ListExtension.tsx index 8504fa630..5efe39ea5 100644 --- a/packages/slate-editor/src/extensions/list/ListExtension.tsx +++ b/packages/slate-editor/src/extensions/list/ListExtension.tsx @@ -1,11 +1,10 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import { ListsEditor, normalizeNode, onKeyDown, - withListsSchema, withListsReact, + withListsSchema, } from '@prezly/slate-lists'; import { TablesEditor } from '@prezly/slate-tables'; import { @@ -27,8 +26,8 @@ import { schema } from './schema'; export const EXTENSION_ID = 'ListExtension'; -export function ListExtension(): Extension { - return { +export function ListExtension() { + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { element: composeElementDeserializer({ @@ -94,5 +93,5 @@ export function ListExtension(): Extension { withOverrides: (editor) => { return withListsReact(withListsSchema(schema)(editor)); }, - }; + }); } diff --git a/packages/slate-editor/src/extensions/mentions/MentionsExtension.ts b/packages/slate-editor/src/extensions/mentions/MentionsExtension.ts index 4f5242cc0..35818d4a1 100644 --- a/packages/slate-editor/src/extensions/mentions/MentionsExtension.ts +++ b/packages/slate-editor/src/extensions/mentions/MentionsExtension.ts @@ -1,5 +1,5 @@ import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import type { Node } from 'slate'; import { Element } from 'slate'; @@ -15,18 +15,19 @@ interface Options { renderElement: Extension['renderElement']; } +// TODO: Get rid of this abstraction? export function MentionsExtension({ id, normalizeNode, parseSerializedElement, renderElement, type, -}: Options): Extension { +}: Options) { function isMention(node: Node) { return Element.isElementType(node, type); } - return { + return useRegisterExtension({ deserialize: { element: composeElementDeserializer({ [type]: createDeserializeElement(parseSerializedElement), @@ -37,5 +38,5 @@ export function MentionsExtension({ isVoid: isMention, normalizeNode, renderElement, - }; + }); } diff --git a/packages/slate-editor/src/extensions/paragraphs/ParagraphsExtension.tsx b/packages/slate-editor/src/extensions/paragraphs/ParagraphsExtension.tsx index 47ced7f89..d1f9ea99d 100644 --- a/packages/slate-editor/src/extensions/paragraphs/ParagraphsExtension.tsx +++ b/packages/slate-editor/src/extensions/paragraphs/ParagraphsExtension.tsx @@ -1,6 +1,5 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; -import { PARAGRAPH_NODE_TYPE, isParagraphNode } from '@prezly/slate-types'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; +import { isParagraphNode, PARAGRAPH_NODE_TYPE } from '@prezly/slate-types'; import React from 'react'; import type { RenderElementProps } from 'slate-react'; @@ -15,31 +14,33 @@ import { export const EXTENSION_ID = 'ParagraphsExtension'; -export const ParagraphsExtension = (): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [PARAGRAPH_NODE_TYPE]: createDeserializeElement(parseSerializedElement), - }), - elementFallback: composeElementDeserializer({ - P: paragraph, // It has to be in the fallbacks, to allow other extensions to parse specific P tags hierarchies. - DIV: paragraph, // It has to be in the fallbacks, to allow other extensions to parse specific DIV tags hierarchies. - BR: paragraph, - }), - }, - normalizeNode: [normalizeRedundantParagraphAttributes, normalizeUnknownElement], - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isParagraphNode(element)) { - return ( - - {children} - - ); - } +export function ParagraphsExtension() { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [PARAGRAPH_NODE_TYPE]: createDeserializeElement(parseSerializedElement), + }), + elementFallback: composeElementDeserializer({ + P: paragraph, // It has to be in the fallbacks, to allow other extensions to parse specific P tags hierarchies. + DIV: paragraph, // It has to be in the fallbacks, to allow other extensions to parse specific DIV tags hierarchies. + BR: paragraph, + }), + }, + normalizeNode: [normalizeRedundantParagraphAttributes, normalizeUnknownElement], + renderElement: ({ attributes, children, element }: RenderElementProps) => { + if (isParagraphNode(element)) { + return ( + + {children} + + ); + } - return undefined; - }, -}); + return undefined; + }, + }); +} function paragraph() { return { type: PARAGRAPH_NODE_TYPE }; diff --git a/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts b/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts index e41cf227b..dfeb3a28e 100644 --- a/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts +++ b/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts @@ -1,4 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import type { Editor } from 'slate'; import { withFilesPasting } from './lib'; @@ -9,9 +9,9 @@ export interface Parameters { onFilesPasted?: (editor: Editor, files: File[]) => void; } -export function PasteFilesExtension({ onFilesPasted }: Parameters = {}): Extension { - return { +export function PasteFilesExtension({ onFilesPasted }: Parameters = {}) { + return useRegisterExtension({ id: EXTENSION_ID, withOverrides: withFilesPasting({ onFilesPasted }), - }; + }); } diff --git a/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts b/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts index 5ee1c6b2d..08acf48db 100644 --- a/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts +++ b/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts @@ -1,4 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import type { Editor } from 'slate'; import { withImagesPasting } from './lib'; @@ -9,9 +9,9 @@ export interface Parameters { onImagesPasted?: (editor: Editor, images: File[]) => void; } -export function PasteImagesExtension({ onImagesPasted }: Parameters = {}): Extension { - return { +export function PasteImagesExtension({ onImagesPasted }: Parameters = {}) { + return useRegisterExtension({ id: EXTENSION_ID, withOverrides: withImagesPasting({ onImagesPasted }), - }; + }); } diff --git a/packages/slate-editor/src/extensions/paste-slate-content/PasteSlateContentExtension.ts b/packages/slate-editor/src/extensions/paste-slate-content/PasteSlateContentExtension.ts index 57b9baefb..fdf71c15b 100644 --- a/packages/slate-editor/src/extensions/paste-slate-content/PasteSlateContentExtension.ts +++ b/packages/slate-editor/src/extensions/paste-slate-content/PasteSlateContentExtension.ts @@ -1,4 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import { withSlatePasting } from './lib'; import type { IsPreservedBlock } from './lib/withSlatePasting'; @@ -12,11 +12,9 @@ interface Options { isPreservedBlock?: IsPreservedBlock; } -export function PasteSlateContentExtension({ - isPreservedBlock = () => false, -}: Options = {}): Extension { - return { +export function PasteSlateContentExtension({ isPreservedBlock = () => false }: Options = {}) { + return useRegisterExtension({ id: EXTENSION_ID, withOverrides: withSlatePasting(isPreservedBlock), - }; + }); } diff --git a/packages/slate-editor/src/extensions/placeholders/PlaceholdersExtension.tsx b/packages/slate-editor/src/extensions/placeholders/PlaceholdersExtension.tsx index 6671a611a..042d785d4 100644 --- a/packages/slate-editor/src/extensions/placeholders/PlaceholdersExtension.tsx +++ b/packages/slate-editor/src/extensions/placeholders/PlaceholdersExtension.tsx @@ -1,21 +1,19 @@ import type { NewsroomRef } from '@prezly/sdk'; -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import React from 'react'; import { withPastedUrlsUnfurling } from './behaviour'; -import { StoryEmbedPlaceholderElement } from './elements'; -import { - CoveragePlaceholderElement, - InlineContactPlaceholderElement, - StoryBookmarkPlaceholderElement, -} from './elements'; import { AttachmentPlaceholderElement, ContactPlaceholderElement, + CoveragePlaceholderElement, EmbedPlaceholderElement, GalleryPlaceholderElement, ImagePlaceholderElement, + InlineContactPlaceholderElement, SocialPostPlaceholderElement, + StoryBookmarkPlaceholderElement, + StoryEmbedPlaceholderElement, VideoPlaceholderElement, WebBookmarkPlaceholderElement, } from './elements'; @@ -117,8 +115,8 @@ export function PlaceholdersExtension({ withPastedUrlsUnfurling: isUnfurlingPastedUrls = false, withVideoPlaceholders = false, withWebBookmarkPlaceholders = false, -}: Parameters = {}): Extension { - return { +}: Parameters = {}) { + return useRegisterExtension({ id: EXTENSION_ID, isElementEqual(element, another) { if (isPlaceholderNode(element) && isPlaceholderNode(another)) { @@ -370,5 +368,5 @@ export function PlaceholdersExtension({ withOverrides: withPastedUrlsUnfurling( isUnfurlingPastedUrls ? isUnfurlingPastedUrls.fetchOembed : undefined, ), - }; + }); } diff --git a/packages/slate-editor/src/extensions/press-contacts/PressContactsExtension.tsx b/packages/slate-editor/src/extensions/press-contacts/PressContactsExtension.tsx index 837dd91bb..d402df86d 100644 --- a/packages/slate-editor/src/extensions/press-contacts/PressContactsExtension.tsx +++ b/packages/slate-editor/src/extensions/press-contacts/PressContactsExtension.tsx @@ -1,5 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import { CONTACT_NODE_TYPE, isContactNode } from '@prezly/slate-types'; import { isEqual } from '@technically/lodash'; import React from 'react'; @@ -15,40 +14,45 @@ import { export const EXTENSION_ID = 'PressContactExtension'; -export const PressContactsExtension = (): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [CONTACT_NODE_TYPE]: createDeserializeElement(parseSerializedElement), - }), - }, - isElementEqual(element, another) { - if (isContactNode(element) && isContactNode(another)) { - if (element.layout !== another.layout || element.show_avatar !== another.show_avatar) { - return false; - } +export function PressContactsExtension() { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [CONTACT_NODE_TYPE]: createDeserializeElement(parseSerializedElement), + }), + }, + isElementEqual(element, another) { + if (isContactNode(element) && isContactNode(another)) { + if ( + element.layout !== another.layout || + element.show_avatar !== another.show_avatar + ) { + return false; + } - // If these are contact references, then ContactInfo object is irrelevant - if (element.reference || another.reference) { - return element.reference === another.reference; + // If these are contact references, then ContactInfo object is irrelevant + if (element.reference || another.reference) { + return element.reference === another.reference; + } + // Otherwise, compare ContactInfo ignoring node `uuid` and `reference` + return isEqual(element.contact, another.contact); + } + return undefined; + }, + isRichBlock: isContactNode, + isVoid: isContactNode, + normalizeNode: [normalizeContactNodeAttributes, normalizeContactInfoAttributes], + renderElement: ({ attributes, children, element }) => { + if (isContactNode(element)) { + return ( + + {children} + + ); } - // Otherwise, compare ContactInfo ignoring node `uuid` and `reference` - return isEqual(element.contact, another.contact); - } - return undefined; - }, - isRichBlock: isContactNode, - isVoid: isContactNode, - normalizeNode: [normalizeContactNodeAttributes, normalizeContactInfoAttributes], - renderElement: ({ attributes, children, element }) => { - if (isContactNode(element)) { - return ( - - {children} - - ); - } - return undefined; - }, -}); + return undefined; + }, + }); +} diff --git a/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx b/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx index 1fa629735..be87d213e 100644 --- a/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx +++ b/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx @@ -1,9 +1,12 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import type { SnippetsExtensionParameters } from './types'; export const EXTENSION_ID = 'SnippetsExtension'; -export const SnippetsExtension = (_params: SnippetsExtensionParameters): Extension => ({ - id: EXTENSION_ID, -}); +// TODO: Fix unused parameter +export function SnippetsExtension(_params: SnippetsExtensionParameters) { + return useRegisterExtension({ + id: EXTENSION_ID, + }); +} diff --git a/packages/slate-editor/src/extensions/soft-break/SoftBreakExtension.ts b/packages/slate-editor/src/extensions/soft-break/SoftBreakExtension.ts index 1f5815ee2..c99defe8b 100644 --- a/packages/slate-editor/src/extensions/soft-break/SoftBreakExtension.ts +++ b/packages/slate-editor/src/extensions/soft-break/SoftBreakExtension.ts @@ -1,4 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import { isHotkey } from 'is-hotkey'; import { Editor } from 'slate'; @@ -6,8 +6,8 @@ export const EXTENSION_ID = 'SoftBreakExtension'; const isShiftEnter = isHotkey('shift+enter'); -export function SoftBreakExtension(): Extension { - return { +export function SoftBreakExtension() { + return useRegisterExtension({ id: EXTENSION_ID, onKeyDown(event, editor) { if (isShiftEnter(event.nativeEvent) && !event.isDefaultPrevented()) { @@ -16,5 +16,5 @@ export function SoftBreakExtension(): Extension { } return false; }, - }; + }); } diff --git a/packages/slate-editor/src/extensions/story-bookmark/StoryBookmarkExtension.tsx b/packages/slate-editor/src/extensions/story-bookmark/StoryBookmarkExtension.tsx index ceb3be156..8a06c329e 100644 --- a/packages/slate-editor/src/extensions/story-bookmark/StoryBookmarkExtension.tsx +++ b/packages/slate-editor/src/extensions/story-bookmark/StoryBookmarkExtension.tsx @@ -1,6 +1,5 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; -import { STORY_BOOKMARK_NODE_TYPE, isStoryBookmarkNode } from '@prezly/slate-types'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; +import { isStoryBookmarkNode, STORY_BOOKMARK_NODE_TYPE } from '@prezly/slate-types'; import React from 'react'; import type { RenderElementProps } from 'slate-react'; @@ -12,36 +11,38 @@ import type { StoryBookmarkExtensionParameters } from './types'; export const EXTENSION_ID = 'StoryBookmarkExtension'; -export const StoryBookmarkExtension = (params: StoryBookmarkExtensionParameters): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [STORY_BOOKMARK_NODE_TYPE]: createDeserializeElement(parseSerializedElement), - }), - }, - isElementEqual: (node, another) => { - if (isStoryBookmarkNode(node) && isStoryBookmarkNode(another)) { - return ( - node.story.uuid === another.story.uuid && - node.show_thumbnail === another.show_thumbnail && - node.new_tab === another.new_tab && - node.layout === another.layout - ); - } - return undefined; - }, - isRichBlock: isStoryBookmarkNode, - isVoid: isStoryBookmarkNode, - normalizeNode: normalizeRedundantStoryBookmarkAttributes, - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isStoryBookmarkNode(element)) { - return ( - - {children} - - ); - } +export function StoryBookmarkExtension(params: StoryBookmarkExtensionParameters) { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [STORY_BOOKMARK_NODE_TYPE]: createDeserializeElement(parseSerializedElement), + }), + }, + isElementEqual: (node, another) => { + if (isStoryBookmarkNode(node) && isStoryBookmarkNode(another)) { + return ( + node.story.uuid === another.story.uuid && + node.show_thumbnail === another.show_thumbnail && + node.new_tab === another.new_tab && + node.layout === another.layout + ); + } + return undefined; + }, + isRichBlock: isStoryBookmarkNode, + isVoid: isStoryBookmarkNode, + normalizeNode: normalizeRedundantStoryBookmarkAttributes, + renderElement: ({ attributes, children, element }: RenderElementProps) => { + if (isStoryBookmarkNode(element)) { + return ( + + {children} + + ); + } - return undefined; - }, -}); + return undefined; + }, + }); +} diff --git a/packages/slate-editor/src/extensions/story-embed/StoryEmbedExtension.tsx b/packages/slate-editor/src/extensions/story-embed/StoryEmbedExtension.tsx index 29bfcf6bc..a50127188 100644 --- a/packages/slate-editor/src/extensions/story-embed/StoryEmbedExtension.tsx +++ b/packages/slate-editor/src/extensions/story-embed/StoryEmbedExtension.tsx @@ -1,6 +1,5 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; -import { STORY_EMBED_NODE_TYPE, isStoryEmbedNode } from '@prezly/slate-types'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; +import { isStoryEmbedNode, STORY_EMBED_NODE_TYPE } from '@prezly/slate-types'; import React from 'react'; import type { RenderElementProps } from 'slate-react'; @@ -12,37 +11,43 @@ import type { StoryEmbedExtensionParameters } from './types'; export const EXTENSION_ID = 'StoryEmbedExtension'; -export const StoryEmbedExtension = ({ render }: StoryEmbedExtensionParameters): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [STORY_EMBED_NODE_TYPE]: createDeserializeElement(parseSerializedElement), - }), - }, - isElementEqual: (node, another) => { - if (isStoryEmbedNode(node) && isStoryEmbedNode(another)) { - return ( - node.story.uuid === another.story.uuid && - node.appearance === another.appearance && - node.position === another.position - ); - } - return undefined; - }, - isRichBlock: isStoryEmbedNode, - isVoid: isStoryEmbedNode, - normalizeNode: normalizeRedundantStoryEmbedAttributes, - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isStoryEmbedNode(element)) { - return ( - <> - - {children} - - - ); - } +export function StoryEmbedExtension({ render }: StoryEmbedExtensionParameters) { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [STORY_EMBED_NODE_TYPE]: createDeserializeElement(parseSerializedElement), + }), + }, + isElementEqual: (node, another) => { + if (isStoryEmbedNode(node) && isStoryEmbedNode(another)) { + return ( + node.story.uuid === another.story.uuid && + node.appearance === another.appearance && + node.position === another.position + ); + } + return undefined; + }, + isRichBlock: isStoryEmbedNode, + isVoid: isStoryEmbedNode, + normalizeNode: normalizeRedundantStoryEmbedAttributes, + renderElement: ({ attributes, children, element }: RenderElementProps) => { + if (isStoryEmbedNode(element)) { + return ( + <> + + {children} + + + ); + } - return undefined; - }, -}); + return undefined; + }, + }); +} diff --git a/packages/slate-editor/src/extensions/tables/TablesExtension.tsx b/packages/slate-editor/src/extensions/tables/TablesExtension.tsx index d79cf124a..a1c025340 100644 --- a/packages/slate-editor/src/extensions/tables/TablesExtension.tsx +++ b/packages/slate-editor/src/extensions/tables/TablesExtension.tsx @@ -1,18 +1,18 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import { - withTables, - TablesEditor, onKeyDown, - withTablesDeleteBehavior, + TablesEditor, + withTables, withTablesCopyPasteBehavior, + withTablesDeleteBehavior, } from '@prezly/slate-tables'; import { - type TableNode, - type TableRowNode, - type TableCellNode, + isTableCellNode, isTableNode, isTableRowNode, - isTableCellNode, + type TableCellNode, + type TableNode, + type TableRowNode, } from '@prezly/slate-types'; import { flow } from '@technically/lodash'; import React from 'react'; @@ -21,8 +21,8 @@ import type { RenderElementProps } from 'slate-react'; import { composeElementDeserializer } from '#modules/html-deserialization'; -import { TableElement, TableRowElement, TableCellElement } from './components'; -import { createTableNode, createTableRowNode, createTableCellNode } from './lib'; +import { TableCellElement, TableElement, TableRowElement } from './components'; +import { createTableCellNode, createTableNode, createTableRowNode } from './lib'; import { normalizeCellAttributes, normalizeRowAttributes, @@ -36,8 +36,8 @@ interface Parameters { createDefaultElement: (props?: Partial) => Element; } -export function TablesExtension({ createDefaultElement }: Parameters): Extension { - return { +export function TablesExtension({ createDefaultElement }: Parameters) { + return useRegisterExtension({ id: EXTENSION_ID, isRichBlock: isTableNode, normalizeNode: [normalizeTableAttributes, normalizeRowAttributes, normalizeCellAttributes], @@ -124,5 +124,5 @@ export function TablesExtension({ createDefaultElement }: Parameters): Extension return flow([withTablesCopyPasteBehavior, withTablesDeleteBehavior])(tablesEditor); }, - }; + }); } diff --git a/packages/slate-editor/src/extensions/text-styling/TextStylingExtension.tsx b/packages/slate-editor/src/extensions/text-styling/TextStylingExtension.tsx index cd849e31a..06b835570 100644 --- a/packages/slate-editor/src/extensions/text-styling/TextStylingExtension.tsx +++ b/packages/slate-editor/src/extensions/text-styling/TextStylingExtension.tsx @@ -1,4 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; import React from 'react'; import { Text } from './components'; @@ -7,13 +7,13 @@ import { onKeyDown } from './onKeyDown'; export const EXTENSION_ID = 'TextStylingExtension'; -export function TextStylingExtension(): Extension { - return { +export function TextStylingExtension() { + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { marks: detectMarks, }, onKeyDown, renderLeaf: (props) => , - }; + }); } diff --git a/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx b/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx index c4bfab85d..06d55bba5 100644 --- a/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx +++ b/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx @@ -1,4 +1,3 @@ -import type { Extension } from '@prezly/slate-commons'; import { isMentionNode, MENTION_NODE_TYPE } from '@prezly/slate-types'; import React from 'react'; import type { RenderElementProps } from 'slate-react'; @@ -9,22 +8,27 @@ import { normalizeRedundantUserMentionAttributes, parseSerializedElement } from export const EXTENSION_ID = 'UserMentionsExtension'; -export const UserMentionsExtension = (): Extension => - MentionsExtension({ - id: EXTENSION_ID, - type: MENTION_NODE_TYPE, - normalizeNode: normalizeRedundantUserMentionAttributes, - parseSerializedElement, - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isMentionNode(element)) { - return ( - - {element.user.name} - {children} - - ); - } +export function UserMentionsExtension() { + return ( + + ); +} - return undefined; - }, - }); +function renderElement({ attributes, children, element }: RenderElementProps) { + if (isMentionNode(element)) { + return ( + + {element.user.name} + {children} + + ); + } + + return undefined; +} diff --git a/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx b/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx index 641c37910..9dee2db41 100644 --- a/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx +++ b/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx @@ -1,6 +1,5 @@ -import type { Extension } from '@prezly/slate-commons'; import { isVariableNode, VARIABLE_NODE_TYPE } from '@prezly/slate-types'; -import React from 'react'; +import React, { useMemo } from 'react'; import type { RenderElementProps } from 'slate-react'; import { MentionElement, MentionsExtension } from '#extensions/mentions'; @@ -15,28 +14,37 @@ import type { VariablesExtensionParameters } from './types'; export const EXTENSION_ID = 'VariablesExtension'; -export function VariablesExtension({ variables }: VariablesExtensionParameters): Extension { +export function VariablesExtension({ variables }: VariablesExtensionParameters) { const variablesNames = variables.map(({ key }) => key); - return MentionsExtension({ - id: EXTENSION_ID, - type: VARIABLE_NODE_TYPE, - normalizeNode: [ + const normalizeNode = useMemo( + () => [ convertLegacyPlaceholderNodesToVariables, removeUnknownVariables(variablesNames), removeUnknownVariableNodeAttributes, ], - parseSerializedElement, - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isVariableNode(element)) { - return ( - - {`%${element.key}%`} - {children} - - ); - } + [JSON.stringify(variablesNames)], + ); - return undefined; - }, - }); + return ( + + ); +} + +function renderElement({ attributes, children, element }: RenderElementProps) { + if (isVariableNode(element)) { + return ( + + {`%${element.key}%`} + {children} + + ); + } + + return undefined; } diff --git a/packages/slate-editor/src/extensions/video/VideoExtension.tsx b/packages/slate-editor/src/extensions/video/VideoExtension.tsx index 1c4f32e8d..9a0153130 100644 --- a/packages/slate-editor/src/extensions/video/VideoExtension.tsx +++ b/packages/slate-editor/src/extensions/video/VideoExtension.tsx @@ -1,6 +1,5 @@ import type { OEmbedInfo } from '@prezly/sdk'; -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import { VideoNode } from '@prezly/slate-types'; import { isEqual } from '@technically/lodash'; import React from 'react'; @@ -29,8 +28,8 @@ export function VideoExtension({ withMenu = false, withLayoutControls = true, withConversionOptions = false, -}: VideoExtensionParameters): Extension { - return { +}: VideoExtensionParameters) { + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { element: composeElementDeserializer({ @@ -69,5 +68,5 @@ export function VideoExtension({ return undefined; }, - }; + }); } diff --git a/packages/slate-editor/src/extensions/void/VoidExtension.tsx b/packages/slate-editor/src/extensions/void/VoidExtension.tsx index abd2571cd..04100f140 100644 --- a/packages/slate-editor/src/extensions/void/VoidExtension.tsx +++ b/packages/slate-editor/src/extensions/void/VoidExtension.tsx @@ -1,5 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; -import { EditorCommands } from '@prezly/slate-commons'; +import { EditorCommands, useRegisterExtension } from '@prezly/slate-commons'; import { isHotkey } from 'is-hotkey'; import { Transforms } from 'slate'; @@ -9,8 +8,8 @@ import { createParagraph } from '#extensions/paragraphs'; export const EXTENSION_ID = 'VoidExtension'; -export function VoidExtension(): Extension { - return { +export function VoidExtension() { + return useRegisterExtension({ id: EXTENSION_ID, onKeyDown: (event, editor) => { const [currentNode] = EditorCommands.getCurrentNodeEntry(editor) ?? []; @@ -37,5 +36,5 @@ export function VoidExtension(): Extension { return false; }, - }; + }); } diff --git a/packages/slate-editor/src/extensions/web-bookmark/WebBookmarkExtension.tsx b/packages/slate-editor/src/extensions/web-bookmark/WebBookmarkExtension.tsx index a9fa7023a..bdbd0bf51 100644 --- a/packages/slate-editor/src/extensions/web-bookmark/WebBookmarkExtension.tsx +++ b/packages/slate-editor/src/extensions/web-bookmark/WebBookmarkExtension.tsx @@ -1,5 +1,4 @@ -import type { Extension } from '@prezly/slate-commons'; -import { createDeserializeElement } from '@prezly/slate-commons'; +import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import { BookmarkNode } from '@prezly/slate-types'; import { isEqual } from '@technically/lodash'; import React from 'react'; @@ -18,48 +17,50 @@ export interface Parameters { export const EXTENSION_ID = 'WebBookmarkExtension'; -export const WebBookmarkExtension = ({ +export function WebBookmarkExtension({ withNewTabOption = true, withConversionOptions = false, -}: Parameters): Extension => ({ - id: EXTENSION_ID, - deserialize: { - element: composeElementDeserializer({ - [BookmarkNode.TYPE]: createDeserializeElement(parseSerializedElement), - }), - }, - isElementEqual: (node, another) => { - if (BookmarkNode.isBookmarkNode(node) && BookmarkNode.isBookmarkNode(another)) { - // Compare ignoring `uuid` and `children` - return ( - node.url === another.url && - node.show_thumbnail === another.show_thumbnail && - node.layout === another.layout && - node.new_tab === another.new_tab && - isEqual(node.oembed, another.oembed) - ); - } - return undefined; - }, - isRichBlock: BookmarkNode.isBookmarkNode, - isVoid: BookmarkNode.isBookmarkNode, - normalizeNode: [unsetUnknownAttributes, normalizeUrlAttribute, fixUuidCollisions], - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (BookmarkNode.isBookmarkNode(element)) { - return ( - <> - - {children} - - - ); - } +}: Parameters) { + return useRegisterExtension({ + id: EXTENSION_ID, + deserialize: { + element: composeElementDeserializer({ + [BookmarkNode.TYPE]: createDeserializeElement(parseSerializedElement), + }), + }, + isElementEqual: (node, another) => { + if (BookmarkNode.isBookmarkNode(node) && BookmarkNode.isBookmarkNode(another)) { + // Compare ignoring `uuid` and `children` + return ( + node.url === another.url && + node.show_thumbnail === another.show_thumbnail && + node.layout === another.layout && + node.new_tab === another.new_tab && + isEqual(node.oembed, another.oembed) + ); + } + return undefined; + }, + isRichBlock: BookmarkNode.isBookmarkNode, + isVoid: BookmarkNode.isBookmarkNode, + normalizeNode: [unsetUnknownAttributes, normalizeUrlAttribute, fixUuidCollisions], + renderElement: ({ attributes, children, element }: RenderElementProps) => { + if (BookmarkNode.isBookmarkNode(element)) { + return ( + <> + + {children} + + + ); + } - return undefined; - }, -}); + return undefined; + }, + }); +} From 07cb9ad7464dc325c7a672ee16b21affe1c47d47 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 00:43:14 +0300 Subject: [PATCH 05/54] [CARE-1802] Convert `getEnabledExtensions()` to a React component Conflicts: - packages/slate-editor/src/modules/editor/getEnabledExtensions.ts  Conflicts:  packages/slate-editor/src/modules/editor/Editor.tsx  packages/slate-editor/src/modules/editor/getEnabledExtensions.ts --- .../extensions/snippet/SnippetsExtension.tsx | 5 +- .../editable/EditableWithExtensions.tsx | 3 +- .../src/modules/editor/Editor.tsx | 359 +++++++------- .../src/modules/editor/Extensions.tsx | 364 ++++++++++++++ .../modules/editor/getEnabledExtensions.ts | 469 ------------------ 5 files changed, 547 insertions(+), 653 deletions(-) create mode 100644 packages/slate-editor/src/modules/editor/Extensions.tsx delete mode 100644 packages/slate-editor/src/modules/editor/getEnabledExtensions.ts diff --git a/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx b/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx index be87d213e..ddb694878 100644 --- a/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx +++ b/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx @@ -1,11 +1,8 @@ import { useRegisterExtension } from '@prezly/slate-commons'; -import type { SnippetsExtensionParameters } from './types'; - export const EXTENSION_ID = 'SnippetsExtension'; -// TODO: Fix unused parameter -export function SnippetsExtension(_params: SnippetsExtensionParameters) { +export function SnippetsExtension() { return useRegisterExtension({ id: EXTENSION_ID, }); diff --git a/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx b/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx index 2a014b377..257cba834 100644 --- a/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx +++ b/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx @@ -8,6 +8,7 @@ import type { RenderLeaf, } from '@prezly/slate-commons'; import classNames from 'classnames'; +import type { ReactNode } from 'react'; import React, { useCallback, useMemo } from 'react'; import type { Editor } from 'slate'; import type { ReactEditor } from 'slate-react'; @@ -23,6 +24,7 @@ import { } from './lib'; export interface Props { + children?: ReactNode; className?: string; decorate?: Decorate; editor: Editor & ReactEditor; @@ -73,7 +75,6 @@ export function EditableWithExtensions({ className, decorate, editor, - extensions = [], onDOMBeforeInput: onDOMBeforeInputList = [], onDOMBeforeInputDeps = [], onKeyDown: onKeyDownList = [], diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 40d0200ce..7b79e6696 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/display-name */ import { Events } from '@prezly/events'; -import { EditorCommands } from '@prezly/slate-commons'; +import { EditorCommands, ExtensionsManagerProvider } from '@prezly/slate-commons'; import { TablesEditor } from '@prezly/slate-tables'; import { type HeadingNode, @@ -45,7 +45,7 @@ import { PopperOptionsContext } from '#modules/popper-options-context'; import { RichFormattingMenu, toggleBlock } from '#modules/rich-formatting-menu'; import styles from './Editor.module.scss'; -import { getEnabledExtensions } from './getEnabledExtensions'; +import { Extensions } from './Extensions'; import { InitialNormalization } from './InitialNormalization'; import { createOnCut, @@ -137,43 +137,9 @@ export const Editor = forwardRef((props, forwardedRef) = [setFloatingAddMenuOpen], ); - const extensions = Array.from( - getEnabledExtensions({ - availableWidth, - onFloatingAddMenuToggle, - withAllowedBlocks, - withAttachments, - withAutoformat, - withBlockquotes, - withButtonBlocks, - withCoverage, - withCustomNormalization, - withDivider, - withEmbeds, - withFloatingAddMenu, - withGalleries, - withHeadings, - withImages, - withInlineContacts, - withInlineLinks, - withLists, - withPlaceholders, - withPressContacts, - withTextStyling, - withTables, - withUserMentions, - withVariables, - withVideos, - withWebBookmarks, - withStoryEmbeds, - withStoryBookmarks, - withSnippets, - }), - ); - const { editor, onKeyDownList } = useCreateEditor({ events, - extensions, + // extensions, // FIXME onKeyDown, plugins, }); @@ -762,150 +728,185 @@ export const Editor = forwardRef((props, forwardedRef) = }); return ( - -
- {sizer} - { - /** - * @see https://docs.slatejs.org/concepts/11-normalizing#built-in-constraints - * - * The top-level editor node can only contain block nodes. If any of the top-level - * children are inline or text nodes they will be removed. This ensures that there - * are always block nodes in the editor so that behaviors like "splitting a block - * in two" work as expected. - */ - onChange(newValue as Element[]); - variables.onChange(editor); - userMentions.onChange(editor); - }} - initialValue={getInitialValue()} + + + + +
- - {(combinedDecorate) => ( - <> - - - - - - {!hasCustomPlaceholder && ( - - {placeholder} - - )} - - {withFloatingAddMenu && ( - - onFloatingAddMenuToggle(toggle, 'click') - } - options={menuOptions} - showTooltipByDefault={EditorCommands.isEmpty(editor)} - /> - )} - - {withVariables && ( - variables.onAdd(editor, option)} - options={variables.options} - target={variables.target} + {sizer} + { + /** + * @see https://docs.slatejs.org/concepts/11-normalizing#built-in-constraints + * + * The top-level editor node can only contain block nodes. If any of the top-level + * children are inline or text nodes they will be removed. This ensures that there + * are always block nodes in the editor so that behaviors like "splitting a block + * in two" work as expected. + */ + onChange(newValue as Element[]); + variables.onChange(editor); + userMentions.onChange(editor); + }} + initialValue={getInitialValue()} + > + + {(combinedDecorate) => ( + <> + + - )} - - {withUserMentions && ( - - userMentions.onAdd(editor, option) - } - options={userMentions.options} - target={userMentions.target} - /> - )} - - {withRichFormattingMenu && ( - - )} - - {withSnippets && isFloatingSnippetInputOpen && ( - - withSnippets.renderInput({ - onCreate: submitFloatingSnippetInput, - }) - } - /> - )} - - )} - - -
-
+ + + + {!hasCustomPlaceholder && ( + + {placeholder} + + )} + + {withFloatingAddMenu && ( + + onFloatingAddMenuToggle(toggle, 'click') + } + options={menuOptions} + showTooltipByDefault={EditorCommands.isEmpty(editor)} + /> + )} + + {withVariables && ( + + variables.onAdd(editor, option) + } + options={variables.options} + target={variables.target} + /> + )} + + {withUserMentions && ( + + userMentions.onAdd(editor, option) + } + options={userMentions.options} + target={userMentions.target} + /> + )} + + {withRichFormattingMenu && ( + + )} + + {withSnippets && isFloatingSnippetInputOpen && ( + + withSnippets.renderInput({ + onCreate: submitFloatingSnippetInput, + }) + } + /> + )} + + )} + +
+
+
+ ); }); diff --git a/packages/slate-editor/src/modules/editor/Extensions.tsx b/packages/slate-editor/src/modules/editor/Extensions.tsx new file mode 100644 index 000000000..233e2e264 --- /dev/null +++ b/packages/slate-editor/src/modules/editor/Extensions.tsx @@ -0,0 +1,364 @@ +import { EditorCommands } from '@prezly/slate-commons'; +import { isImageNode, isQuoteNode } from '@prezly/slate-types'; +import React from 'react'; +import { Node } from 'slate'; + +import { AllowedBlocksExtension } from '#extensions/allowed-blocks'; +import { AutoformatExtension } from '#extensions/autoformat'; +import { BlockquoteExtension } from '#extensions/blockquote'; +import { ButtonBlockExtension } from '#extensions/button-block'; +import { CoverageExtension } from '#extensions/coverage'; +import { CustomNormalizationExtension } from '#extensions/custom-normalization'; +import { DecorateSelectionExtension } from '#extensions/decorate-selection'; +import { DividerExtension } from '#extensions/divider'; +import { EmbedExtension } from '#extensions/embed'; +import { FileAttachmentExtension } from '#extensions/file-attachment'; +import { FlashNodesExtension } from '#extensions/flash-nodes'; +import { FloatingAddMenuExtension } from '#extensions/floating-add-menu'; +import { GalleriesExtension } from '#extensions/galleries'; +import { HeadingExtension } from '#extensions/heading'; +import { HotkeysExtension } from '#extensions/hotkeys'; +import { HtmlExtension } from '#extensions/html'; +import { ImageExtension } from '#extensions/image'; +import { InlineContactsExtension } from '#extensions/inline-contacts'; +import { InlineLinksExtension } from '#extensions/inline-links'; +import { InsertBlockHotkeyExtension } from '#extensions/insert-block-hotkey'; +import { ListExtension } from '#extensions/list'; +import { createParagraph, ParagraphsExtension } from '#extensions/paragraphs'; +import { PasteFilesExtension } from '#extensions/paste-files'; +import { PasteImagesExtension } from '#extensions/paste-images'; +import { PasteSlateContentExtension } from '#extensions/paste-slate-content'; +import { PlaceholdersExtension } from '#extensions/placeholders'; +import { PressContactsExtension } from '#extensions/press-contacts'; +import { SnippetsExtension } from '#extensions/snippet'; +import { SoftBreakExtension } from '#extensions/soft-break'; +import { StoryBookmarkExtension } from '#extensions/story-bookmark'; +import { StoryEmbedExtension } from '#extensions/story-embed'; +import { TablesExtension } from '#extensions/tables'; +import { TextStylingExtension } from '#extensions/text-styling'; +import { UserMentionsExtension } from '#extensions/user-mentions'; +import { VariablesExtension } from '#extensions/variables'; +import { VideoExtension } from '#extensions/video'; +import { VoidExtension } from '#extensions/void'; +import { WebBookmarkExtension } from '#extensions/web-bookmark'; +import { EventsEditor } from '#modules/events'; + +import { UPLOAD_MULTIPLE_IMAGES_SOME_ERROR_MESSAGE } from '../uploadcare'; + +import { + BLOCKQUOTE_RULES, + COMPOSITE_CHARACTERS_RULES, + DIVIDER_RULES, + HEADING_RULES, + LIST_RULES, + TEXT_STYLE_RULES, +} from './autoformatRules'; +import type { EditorProps } from './types'; + +type Props = { + availableWidth: number; + onFloatingAddMenuToggle: (show: boolean, trigger: 'input' | 'hotkey') => void; +} & Pick< + Required, + | 'withAllowedBlocks' + | 'withAttachments' + | 'withAutoformat' + | 'withBlockquotes' + | 'withButtonBlocks' + | 'withCoverage' + | 'withCustomNormalization' + | 'withDivider' + | 'withEmbeds' + | 'withFloatingAddMenu' + | 'withGalleries' + | 'withHeadings' + | 'withImages' + | 'withInlineContacts' + | 'withInlineLinks' + | 'withLists' + | 'withPlaceholders' + | 'withPressContacts' + | 'withTextStyling' + | 'withTables' + | 'withUserMentions' + | 'withVariables' + | 'withVideos' + | 'withWebBookmarks' + | 'withStoryEmbeds' + | 'withStoryBookmarks' + | 'withSnippets' +>; + +export function Extensions({ + availableWidth, + onFloatingAddMenuToggle, + withAllowedBlocks, + withAttachments, + withAutoformat, + withBlockquotes, + withButtonBlocks, + withCoverage, + withCustomNormalization, + withDivider, + withEmbeds, + withFloatingAddMenu, + withGalleries, + withHeadings, + withImages, + withInlineContacts, + withInlineLinks, + withLists, + withPlaceholders, + withPressContacts, + withSnippets, + withStoryBookmarks, + withStoryEmbeds, + withTables, + withTextStyling, + withUserMentions, + withVariables, + withVideos, + withWebBookmarks, +}: Props) { + if (withPressContacts && withInlineContacts) { + throw new Error( + `Using 'withPressContacts' and 'withInlineContacts' at the same time is not supported.`, + ); + } + + return ( + <> + {withCustomNormalization && ( + + )} + + {/* We only use it for rendering the selection marks decorations. + The automatic selection decoration itself is disabled and is dynamically attached + using the `useDecorationFactory()` call in the RichFormattingMenu component. */} + + + + + + + + + { + EventsEditor.dispatchEvent(editor, 'empty-paragraph-inserted', { + trigger: 'hotkey', + }); + }} + /> + + {withBlockquotes && } + + {withButtonBlocks && ( + + )} + + {withDivider && } + + {withFloatingAddMenu && ( + onFloatingAddMenuToggle(true, trigger)} + /> + )} + + {withHeadings && } + + {withInlineContacts && } + + {withInlineLinks && } + + {/* Since we're overriding the default Tab key behavior + we need to bring back the possibility to blur the editor with keyboard. */} + + + {withLists && } + + {withPressContacts && } + + {withVariables && } + + {withTextStyling && } + + {withUserMentions && } + + {withAttachments && ( + <> + { + EventsEditor.dispatchEvent(editor, 'files-pasted', { + filesCount: files.length, + isEmpty: EditorCommands.isEmpty(editor), + }); + }} + /> + { + // TODO: It seems it would be more useful to only provide the changeset patch in the event payload. + EventsEditor.dispatchEvent(editor, 'attachment-edited', { + description: updated.description, + mimeType: updated.file.mime_type, + size: updated.file.size, + uuid: updated.file.uuid, + }); + }} + onRemoved={(editor, attachment) => { + EventsEditor.dispatchEvent(editor, 'attachment-removed', { + uuid: attachment.file.uuid, + }); + }} + /> + + )} + + {withCoverage && } + + {withGalleries && ( + { + EventsEditor.dispatchEvent(editor, 'gallery-edited', { + imagesCount: gallery.images.length, + }); + + failedUploads.forEach((error) => { + EventsEditor.dispatchEvent(editor, 'error', error); + }); + + if (failedUploads.length > 0) { + EventsEditor.dispatchEvent(editor, 'notification', { + children: UPLOAD_MULTIPLE_IMAGES_SOME_ERROR_MESSAGE, + type: 'error', + }); + } + }} + onShuffled={(editor, updated) => { + EventsEditor.dispatchEvent(editor, 'gallery-images-shuffled', { + imagesCount: updated.images.length, + }); + }} + withMediaGalleryTab={withGalleries.withMediaGalleryTab ?? false} + withLayoutOptions={withGalleries.withWidthOption} + /> + )} + + {withImages && ( + <> + { + EventsEditor.dispatchEvent(editor, 'images-pasted', { + imagesCount: images.length, + isEmpty: EditorCommands.isEmpty(editor), + }); + }} + /> + {/* + ImageExtension has to be after RichFormattingExtension due to the fact + that it also deserializes elements (ImageExtension is more specific). + */} + { + EventsEditor.dispatchEvent(editor, 'image-edit-clicked'); + }} + onRemoved={(editor, image) => { + EventsEditor.dispatchEvent(editor, 'image-removed', { + uuid: image.file.uuid, + }); + }} + onReplace={(editor) => { + EventsEditor.dispatchEvent(editor, 'image-edit-clicked'); + }} + onReplaced={(editor, updated) => { + EventsEditor.dispatchEvent(editor, 'image-edited', { + description: Node.string(updated), + mimeType: updated.file.mime_type, + size: updated.file.size, + uuid: updated.file.uuid, + trigger: 'image-menu', + operation: 'replace', + }); + }} + /> + + )} + + {withEmbeds && } + + {withVideos && } + + {withWebBookmarks && } + + {withAutoformat && ( + + )} + + + + {withSnippets && } + + {withStoryEmbeds && } + + {withStoryBookmarks && } + + {withTables && } + + {withAllowedBlocks && } + + + + + + isImageNode(node) || isQuoteNode(node)} + /> + + ); +} diff --git a/packages/slate-editor/src/modules/editor/getEnabledExtensions.ts b/packages/slate-editor/src/modules/editor/getEnabledExtensions.ts deleted file mode 100644 index cc6d130fb..000000000 --- a/packages/slate-editor/src/modules/editor/getEnabledExtensions.ts +++ /dev/null @@ -1,469 +0,0 @@ -import { EditorCommands, type Extension } from '@prezly/slate-commons'; -import { isImageNode, isQuoteNode } from '@prezly/slate-types'; -import { Node } from 'slate'; - -import { AllowedBlocksExtension } from '#extensions/allowed-blocks'; -import { AutoformatExtension } from '#extensions/autoformat'; -import { BlockquoteExtension } from '#extensions/blockquote'; -import { ButtonBlockExtension } from '#extensions/button-block'; -import { CoverageExtension } from '#extensions/coverage'; -import { CustomNormalizationExtension } from '#extensions/custom-normalization'; -import { DecorateSelectionExtension } from '#extensions/decorate-selection'; -import { DividerExtension } from '#extensions/divider'; -import { EmbedExtension } from '#extensions/embed'; -import { FileAttachmentExtension } from '#extensions/file-attachment'; -import { FlashNodesExtension } from '#extensions/flash-nodes'; -import { FloatingAddMenuExtension } from '#extensions/floating-add-menu'; -import { GalleriesExtension } from '#extensions/galleries'; -import { HeadingExtension } from '#extensions/heading'; -import { HotkeysExtension } from '#extensions/hotkeys'; -import { HtmlExtension } from '#extensions/html'; -import { ImageExtension } from '#extensions/image'; -import { InlineContactsExtension } from '#extensions/inline-contacts'; -import { InlineLinksExtension } from '#extensions/inline-links'; -import { InsertBlockHotkeyExtension } from '#extensions/insert-block-hotkey'; -import { ListExtension } from '#extensions/list'; -import { createParagraph, ParagraphsExtension } from '#extensions/paragraphs'; -import { PasteFilesExtension } from '#extensions/paste-files'; -import { PasteImagesExtension } from '#extensions/paste-images'; -import { PasteSlateContentExtension } from '#extensions/paste-slate-content'; -import type { PlaceholdersExtensionParameters } from '#extensions/placeholders'; -import { PlaceholdersExtension } from '#extensions/placeholders'; -import { PressContactsExtension } from '#extensions/press-contacts'; -import { SnippetsExtension } from '#extensions/snippet'; -import { SoftBreakExtension } from '#extensions/soft-break'; -import { StoryBookmarkExtension } from '#extensions/story-bookmark'; -import { StoryEmbedExtension } from '#extensions/story-embed'; -import { TablesExtension } from '#extensions/tables'; -import { TextStylingExtension } from '#extensions/text-styling'; -import { UserMentionsExtension } from '#extensions/user-mentions'; -import { VariablesExtension } from '#extensions/variables'; -import { VideoExtension } from '#extensions/video'; -import { VoidExtension } from '#extensions/void'; -import { WebBookmarkExtension } from '#extensions/web-bookmark'; -import { EventsEditor } from '#modules/events'; -import { UPLOAD_MULTIPLE_IMAGES_SOME_ERROR_MESSAGE } from '#modules/uploadcare'; - -import { - BLOCKQUOTE_RULES, - COMPOSITE_CHARACTERS_RULES, - DIVIDER_RULES, - HEADING_RULES, - LIST_RULES, - TEXT_STYLE_RULES, -} from './autoformatRules'; -import type { EditorProps } from './types'; - -type Parameters = { - availableWidth: number; - onFloatingAddMenuToggle: (show: boolean, trigger: 'input' | 'hotkey') => void; -} & Pick< - Required, - | 'withAllowedBlocks' - | 'withAttachments' - | 'withAutoformat' - | 'withBlockquotes' - | 'withButtonBlocks' - | 'withCoverage' - | 'withCustomNormalization' - | 'withDivider' - | 'withEmbeds' - | 'withFloatingAddMenu' - | 'withGalleries' - | 'withHeadings' - | 'withImages' - | 'withInlineContacts' - | 'withInlineLinks' - | 'withLists' - | 'withPlaceholders' - | 'withPressContacts' - | 'withTextStyling' - | 'withTables' - | 'withUserMentions' - | 'withVariables' - | 'withVideos' - | 'withWebBookmarks' - | 'withStoryEmbeds' - | 'withStoryBookmarks' - | 'withSnippets' ->; - -export function* getEnabledExtensions(parameters: Parameters): Generator { - const { - availableWidth, - onFloatingAddMenuToggle, - withAllowedBlocks, - withAttachments, - withAutoformat, - withBlockquotes, - withButtonBlocks, - withCoverage, - withCustomNormalization, - withDivider, - withEmbeds, - withFloatingAddMenu, - withGalleries, - withHeadings, - withImages, - withInlineContacts, - withInlineLinks, - withLists, - withPressContacts, - withSnippets, - withStoryBookmarks, - withStoryEmbeds, - withTextStyling, - withTables, - withUserMentions, - withVariables, - withVideos, - withWebBookmarks, - } = parameters; - if (withPressContacts && withInlineContacts) { - throw new Error( - `Using 'withPressContacts' and 'withInlineContacts' at the same time is not supported.`, - ); - } - - if (withCustomNormalization) { - yield CustomNormalizationExtension({ normalizeNode: withCustomNormalization }); - } - - // We only use it for rendering the selection marks decorations. - // The automatic selection decoration itself is disabled and is dynamically attached - // using the `useDecorationFactory()` call in the RichFormattingMenu component. - yield DecorateSelectionExtension({ decorate: false }); - - yield FlashNodesExtension(); - yield ParagraphsExtension(); - yield SoftBreakExtension(); - yield InsertBlockHotkeyExtension({ - createDefaultElement: createParagraph, - onInserted: (editor) => - EventsEditor.dispatchEvent(editor, 'empty-paragraph-inserted', { trigger: 'hotkey' }), - }); - - if (withBlockquotes) { - yield BlockquoteExtension(); - } - - if (withButtonBlocks) { - const config = withButtonBlocks === true ? {} : withButtonBlocks; - yield ButtonBlockExtension(config); - } - - if (withDivider) { - yield DividerExtension(); - } - - if (withFloatingAddMenu) { - yield FloatingAddMenuExtension({ - onOpen: (trigger) => onFloatingAddMenuToggle(true, trigger), - }); - } - - if (withHeadings) { - yield HeadingExtension(); - } - - if (withInlineContacts) { - yield InlineContactsExtension(); - } - - if (withInlineLinks) { - yield InlineLinksExtension(); - } - - // Since we're overriding the default Tab key behavior - // we need to bring back the possibility to blur the editor - // with keyboard. - yield HotkeysExtension(); - - if (withLists) { - yield ListExtension(); - } - - if (withPressContacts) { - yield PressContactsExtension(); - } - - if (withVariables) { - yield VariablesExtension(withVariables); - } - - if (withTextStyling) { - yield TextStylingExtension(); - } - - if (withUserMentions) { - yield UserMentionsExtension(); - } - - if (withAttachments) { - yield PasteFilesExtension({ - onFilesPasted: (editor, files) => { - EventsEditor.dispatchEvent(editor, 'files-pasted', { - filesCount: files.length, - isEmpty: EditorCommands.isEmpty(editor), - }); - }, - }); - - yield FileAttachmentExtension({ - onEdited(editor, updated) { - // TODO: It seems it would be more useful to only provide the changeset patch in the event payload. - EventsEditor.dispatchEvent(editor, 'attachment-edited', { - description: updated.description, - mimeType: updated.file.mime_type, - size: updated.file.size, - uuid: updated.file.uuid, - }); - }, - onRemoved(editor, attachment) { - EventsEditor.dispatchEvent(editor, 'attachment-removed', { - uuid: attachment.file.uuid, - }); - }, - }); - } - - if (withCoverage) { - yield CoverageExtension(withCoverage); - } - - if (withGalleries) { - yield GalleriesExtension({ - availableWidth, - onEdited(editor, gallery, { failedUploads }) { - EventsEditor.dispatchEvent(editor, 'gallery-edited', { - imagesCount: gallery.images.length, - }); - - failedUploads.forEach((error) => { - EventsEditor.dispatchEvent(editor, 'error', error); - }); - - if (failedUploads.length > 0) { - EventsEditor.dispatchEvent(editor, 'notification', { - children: UPLOAD_MULTIPLE_IMAGES_SOME_ERROR_MESSAGE, - type: 'error', - }); - } - }, - onShuffled(editor, updated) { - EventsEditor.dispatchEvent(editor, 'gallery-images-shuffled', { - imagesCount: updated.images.length, - }); - }, - withMediaGalleryTab: withGalleries.withMediaGalleryTab ?? false, - withLayoutOptions: withGalleries.withWidthOption, - }); - } - - if (withImages) { - yield PasteImagesExtension({ - onImagesPasted: (editor, images) => { - EventsEditor.dispatchEvent(editor, 'images-pasted', { - imagesCount: images.length, - isEmpty: EditorCommands.isEmpty(editor), - }); - }, - }); - - // ImageExtension has to be after RichFormattingExtension due to the fact - // that it also deserializes elements (ImageExtension is more specific). - yield ImageExtension({ - ...withImages, - onCrop(editor) { - EventsEditor.dispatchEvent(editor, 'image-edit-clicked'); - }, - onCropped(editor, image) { - EventsEditor.dispatchEvent(editor, 'image-edited', { - description: Node.string(image), - mimeType: image.file.mime_type, - size: image.file.size, - uuid: image.file.uuid, - trigger: 'image-menu', - operation: 'crop', - }); - }, - onRemoved(editor, image) { - EventsEditor.dispatchEvent(editor, 'image-removed', { uuid: image.file.uuid }); - }, - onReplace(editor) { - EventsEditor.dispatchEvent(editor, 'image-edit-clicked'); - }, - onReplaced(editor, image) { - EventsEditor.dispatchEvent(editor, 'image-edited', { - description: Node.string(image), - mimeType: image.file.mime_type, - size: image.file.size, - uuid: image.file.uuid, - trigger: 'image-menu', - operation: 'replace', - }); - }, - }); - } - - if (withEmbeds) { - yield EmbedExtension({ - ...withEmbeds, - availableWidth, - }); - } - - if (withVideos) { - yield VideoExtension(withVideos); - } - - if (withWebBookmarks) { - yield WebBookmarkExtension(withWebBookmarks); - } - - if (withAutoformat) { - const defaultRules = [ - ...(withBlockquotes ? BLOCKQUOTE_RULES : []), - ...(withDivider ? DIVIDER_RULES : []), - ...(withHeadings ? HEADING_RULES : []), - ...(withLists ? LIST_RULES : []), - ...(withTextStyling ? TEXT_STYLE_RULES : []), - ...COMPOSITE_CHARACTERS_RULES, - ]; - const rules = withAutoformat === true ? defaultRules : withAutoformat.rules; - yield AutoformatExtension({ rules }); - } - - const placeholders = buildPlaceholdersExtensionConfiguration(parameters); - if (placeholders) { - yield PlaceholdersExtension(placeholders); - } - - if (withSnippets) { - yield SnippetsExtension(withSnippets); - } - - if (withStoryEmbeds) { - yield StoryEmbedExtension(withStoryEmbeds); - } - - if (withStoryBookmarks) { - yield StoryBookmarkExtension(withStoryBookmarks); - } - - if (withTables) { - yield TablesExtension({ createDefaultElement: createParagraph }); - } - - if (withAllowedBlocks) { - yield AllowedBlocksExtension(withAllowedBlocks); - } - - yield VoidExtension(); - - yield HtmlExtension(); - - yield PasteSlateContentExtension({ - isPreservedBlock: (_, node) => isImageNode(node) || isQuoteNode(node), - }); -} - -function buildPlaceholdersExtensionConfiguration({ - withAttachments, - withCoverage, - withEmbeds, - withGalleries, - withImages, - withInlineContacts, - withPlaceholders, - withPressContacts, - withStoryBookmarks, - withStoryEmbeds, - withVideos, - withWebBookmarks, -}: Parameters): PlaceholdersExtensionParameters | false { - function* generate(): Generator> { - if (withAttachments) { - yield { - withAttachmentPlaceholders: true, - }; - } - if (withCoverage) { - yield { - withCoveragePlaceholders: withCoverage, - }; - } - if (withEmbeds) { - yield { - withEmbedPlaceholders: withEmbeds, - withSocialPostPlaceholders: withEmbeds, - withPastedUrlsUnfurling: - withEmbeds && withPlaceholders - ? withPlaceholders.withPastedUrlsUnfurling - : undefined, - }; - } - if (withGalleries) { - const newsroom = withGalleries.withMediaGalleryTab - ? withGalleries.withMediaGalleryTab.newsroom - : undefined; - yield { - withGalleryPlaceholders: { newsroom }, - }; - } - if (withInlineContacts) { - yield { - withInlineContactPlaceholders: withInlineContacts, - }; - } - if (withImages) { - yield { - withImagePlaceholders: { - withCaptions: Boolean(withImages.withCaptions), - newsroom: withImages.mediaGalleryTab?.newsroom, - }, - }; - } - if (withPlaceholders && withPlaceholders.withMediaPlaceholders) { - yield { - withMediaPlaceholders: withPlaceholders.withMediaPlaceholders, - }; - } - if (withPressContacts) { - yield { - withContactPlaceholders: withPressContacts, - }; - } - if (withStoryBookmarks) { - yield { - withStoryBookmarkPlaceholders: withStoryBookmarks, - }; - } - if (withStoryEmbeds) { - yield { - withStoryEmbedPlaceholders: withStoryEmbeds, - }; - } - if (withWebBookmarks) { - yield { - withWebBookmarkPlaceholders: withWebBookmarks, - }; - } - if (withVideos) { - yield { - withVideoPlaceholders: withVideos, - }; - } - } - - const extensions = Array.from(generate()); - - if (extensions.length === 0) { - return false; - } - - return extensions.reduce( - (config, part): PlaceholdersExtensionParameters => ({ ...config, ...part }), - withPlaceholders, - ); -} From 1f636e3042241acd0e7076921173ea21c2b98791 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Thu, 7 Sep 2023 17:40:36 +0300 Subject: [PATCH 06/54] [CARE-1802] Move variables UI markup and state to `VariablesExtension` --- .../src/extensions/mentions/useMentions.ts | 50 +++++++++---------- .../variables/VariablesExtension.tsx | 36 ++++++++++--- .../src/extensions/variables/useVariables.ts | 9 ++-- .../src/modules/editor/Editor.tsx | 22 +------- .../src/modules/editor/Extensions.tsx | 2 +- 5 files changed, 59 insertions(+), 60 deletions(-) diff --git a/packages/slate-editor/src/extensions/mentions/useMentions.ts b/packages/slate-editor/src/extensions/mentions/useMentions.ts index 0f79b4375..fb1f1b4bb 100644 --- a/packages/slate-editor/src/extensions/mentions/useMentions.ts +++ b/packages/slate-editor/src/extensions/mentions/useMentions.ts @@ -2,8 +2,8 @@ import { stubTrue } from '@technically/lodash'; import { isHotkey } from 'is-hotkey'; import type { KeyboardEvent } from 'react'; import { useCallback, useMemo, useState } from 'react'; -import type { Editor } from 'slate'; import { Range, Transforms } from 'slate'; +import { useSlateStatic } from 'slate-react'; import { getWordAfterTrigger, insertMention, isPointAtWordEnd } from './lib'; import type { MentionElementType, Option } from './types'; @@ -17,9 +17,9 @@ interface Parameters { export interface Mentions { index: number; - onAdd: (editor: Editor, option: Option) => void; - onChange: (editor: Editor) => void; - onKeyDown: (event: KeyboardEvent, editor: Editor) => void; + onAdd: (option: Option) => void; + onChange: () => void; + onKeyDown: (event: KeyboardEvent) => void; options: Option[]; query: string; target: Range | null; @@ -31,16 +31,19 @@ export function useMentions({ options, trigger, }: Parameters): Mentions { + const editor = useSlateStatic(); + const [index, setIndex] = useState(0); const [query, setQuery] = useState(''); const [target, setTarget] = useState(null); + const filteredOptions = useMemo(() => { if (!isEnabled(target)) return []; - return options.filter(({ label }) => label.search(new RegExp(query, 'i')) !== -1); + return options.filter(({ label }) => label.search(new RegExp(query, 'i')) !== -1); // FIXME: RegExp(query, i) }, [isEnabled, query, options, target]); const onAdd = useCallback( - (editor: Editor, option: Option) => { + (option: Option) => { if (target) { Transforms.select(editor, target); const mentionElement = createMentionElement(option); @@ -48,32 +51,29 @@ export function useMentions({ setTarget(null); } }, - [createMentionElement, target], + [editor, createMentionElement, target], ); - const onChange = useCallback( - (editor: Editor) => { - const { selection } = editor; + const onChange = useCallback(() => { + const { selection } = editor; - if (selection && Range.isCollapsed(selection)) { - const at = Range.start(selection); - const word = getWordAfterTrigger(editor, { at, trigger }); + if (selection && Range.isCollapsed(selection)) { + const at = Range.start(selection); + const word = getWordAfterTrigger(editor, { at, trigger }); - if (word && isPointAtWordEnd(editor, { at })) { - setTarget(word.range); - setQuery(word.text); - setIndex(0); - return; - } + if (word && isPointAtWordEnd(editor, { at })) { + setTarget(word.range); + setQuery(word.text); + setIndex(0); + return; } + } - setTarget(null); - }, - [setIndex, setQuery, trigger], - ); + setTarget(null); + }, [editor, setIndex, setQuery, trigger]); const onKeyDown = useCallback( - (event: KeyboardEvent, editor: Editor) => { + (event: KeyboardEvent) => { if (!target || !isEnabled(target)) { return; } @@ -98,7 +98,7 @@ export function useMentions({ filteredOptions[index] ) { event.preventDefault(); - onAdd(editor, filteredOptions[index]); + onAdd(filteredOptions[index]); } }, [index, isEnabled, filteredOptions, onAdd, target], diff --git a/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx b/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx index 9dee2db41..980ea1847 100644 --- a/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx +++ b/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx @@ -1,9 +1,11 @@ +import { useRegisterExtension } from '@prezly/slate-commons'; import { isVariableNode, VARIABLE_NODE_TYPE } from '@prezly/slate-types'; import React, { useMemo } from 'react'; import type { RenderElementProps } from 'slate-react'; import { MentionElement, MentionsExtension } from '#extensions/mentions'; +import { VariablesDropdown } from './components'; import { parseSerializedElement } from './lib'; import { convertLegacyPlaceholderNodesToVariables, @@ -11,6 +13,7 @@ import { removeUnknownVariables, } from './normalization'; import type { VariablesExtensionParameters } from './types'; +import { useVariables } from './useVariables'; export const EXTENSION_ID = 'VariablesExtension'; @@ -25,14 +28,33 @@ export function VariablesExtension({ variables }: VariablesExtensionParameters) [JSON.stringify(variablesNames)], ); + const { + index, + target, + options, + onAdd, + onKeyDown, + onChange, // FIXME: onChange should be passed to + } = useVariables(variables); + + useRegisterExtension({ id: `${EXTENSION_ID}:onKeyDown`, onKeyDown }); + return ( - + <> + + + ); } diff --git a/packages/slate-editor/src/extensions/variables/useVariables.ts b/packages/slate-editor/src/extensions/variables/useVariables.ts index 925e3289d..9f682d489 100644 --- a/packages/slate-editor/src/extensions/variables/useVariables.ts +++ b/packages/slate-editor/src/extensions/variables/useVariables.ts @@ -2,6 +2,7 @@ import { isSubtitleHeadingNode, isTitleHeadingNode } from '@prezly/slate-types'; import { useCallback, useMemo } from 'react'; import type { BaseRange } from 'slate'; import { Editor } from 'slate'; +import { useSlateStatic } from 'slate-react'; import type { Option } from '#extensions/mentions'; import { useMentions } from '#extensions/mentions'; @@ -17,12 +18,8 @@ function placeholderToOption(placeholder: Variable): Option { }; } -const DEFAULT_PARAMETERS: VariablesExtensionParameters = { variables: [] }; - -export function useVariables( - editor: Editor, - { variables }: VariablesExtensionParameters = DEFAULT_PARAMETERS, -) { +export function useVariables(variables: VariablesExtensionParameters['variables']) { + const editor = useSlateStatic(); const options = useMemo(() => variables.map(placeholderToOption), [variables]); const isEnabled = useCallback( (range: BaseRange | null) => { diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 7b79e6696..64daba60b 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -35,7 +35,6 @@ import { FloatingAddMenu, type Option } from '#extensions/floating-add-menu'; import { insertPlaceholder, PlaceholderNode } from '#extensions/placeholders'; import { useFloatingSnippetInput } from '#extensions/snippet'; import { UserMentionsDropdown, useUserMentions } from '#extensions/user-mentions'; -import { useVariables, VariablesDropdown } from '#extensions/variables'; import { FloatingSnippetInput, Placeholder } from '#modules/components'; import { DecorationsProvider } from '#modules/decorations'; import { EditableWithExtensions } from '#modules/editable'; @@ -248,7 +247,6 @@ export const Editor = forwardRef((props, forwardedRef) = }), ); - const variables = useVariables(editor, withVariables || undefined); const userMentions = useUserMentions(withUserMentions || undefined); const [ @@ -261,10 +259,6 @@ export const Editor = forwardRef((props, forwardedRef) = }, ] = useFloatingSnippetInput(editor); - if (withVariables) { - onKeyDownList.push(variables.onKeyDown); - } - if (withUserMentions) { onKeyDownList.push(userMentions.onKeyDown); } @@ -781,7 +775,7 @@ export const Editor = forwardRef((props, forwardedRef) = * in two" work as expected. */ onChange(newValue as Element[]); - variables.onChange(editor); + // variables.onChange(editor); // FIXME: Find a way to wire `onChange` with VariablesExtension userMentions.onChange(editor); }} initialValue={getInitialValue()} @@ -803,9 +797,6 @@ export const Editor = forwardRef((props, forwardedRef) = userMentions.query, userMentions.target, withUserMentions, - variables.index, - variables.query, - variables.target, withVariables, ]} readOnly={readOnly} @@ -841,17 +832,6 @@ export const Editor = forwardRef((props, forwardedRef) = /> )} - {withVariables && ( - - variables.onAdd(editor, option) - } - options={variables.options} - target={variables.target} - /> - )} - {withUserMentions && ( } - {withVariables && } + {withVariables && } {withTextStyling && } From 4164b8963c01ca135170bdeaf4c65c08a6f635f5 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Thu, 7 Sep 2023 17:46:13 +0300 Subject: [PATCH 07/54] [CARE-1802] Move user mentions UI markup and state to `UserMentionsExtension` --- .../user-mentions/UserMentionsExtension.tsx | 35 ++++++++++++++----- .../user-mentions/useUserMentions.ts | 14 ++++---- .../variables/VariablesExtension.tsx | 2 ++ .../src/modules/editor/Editor.tsx | 27 +------------- .../src/modules/editor/Extensions.tsx | 2 +- 5 files changed, 39 insertions(+), 41 deletions(-) diff --git a/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx b/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx index 06d55bba5..b8355b50c 100644 --- a/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx +++ b/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx @@ -1,22 +1,41 @@ +import { useRegisterExtension } from '@prezly/slate-commons'; import { isMentionNode, MENTION_NODE_TYPE } from '@prezly/slate-types'; import React from 'react'; import type { RenderElementProps } from 'slate-react'; import { MentionElement, MentionsExtension } from '#extensions/mentions'; +import { UserMentionsDropdown } from './components'; import { normalizeRedundantUserMentionAttributes, parseSerializedElement } from './lib'; +import type { UserMentionsExtensionParameters } from './types'; +import { useUserMentions } from './useUserMentions'; export const EXTENSION_ID = 'UserMentionsExtension'; -export function UserMentionsExtension() { +export function UserMentionsExtension({ users }: UserMentionsExtensionParameters) { + // FIXME: onChange should be passed to + const { index, target, options, onAdd, onChange, onKeyDown } = useUserMentions(users); + + // TODO: Find a better way maybe? + useRegisterExtension({ id: `${EXTENSION_ID}:onKeyDown`, onKeyDown }); + return ( - + <> + + + + ); } diff --git a/packages/slate-editor/src/extensions/user-mentions/useUserMentions.ts b/packages/slate-editor/src/extensions/user-mentions/useUserMentions.ts index 1b2236bcb..da35558ea 100644 --- a/packages/slate-editor/src/extensions/user-mentions/useUserMentions.ts +++ b/packages/slate-editor/src/extensions/user-mentions/useUserMentions.ts @@ -4,7 +4,7 @@ import type { Option } from '#extensions/mentions'; import { useMentions } from '#extensions/mentions'; import { createUserMention } from './lib'; -import type { User, UserMentionsExtensionParameters } from './types'; +import type { User } from './types'; function userToOption(user: User): Option { return { @@ -14,14 +14,16 @@ function userToOption(user: User): Option { }; } -const DEFAULT_PARAMETERS: UserMentionsExtensionParameters = { users: [] }; - -export function useUserMentions({ users }: UserMentionsExtensionParameters = DEFAULT_PARAMETERS) { - const options = useMemo(() => users.map(userToOption), [users]); +export function useUserMentions(users: User[]) { + const options = useMemo(() => users.map(userToOption), [JSON.stringify(users)]); return useMentions({ - createMentionElement: (option) => createUserMention(option.value), + createMentionElement, options, trigger: '@', }); } + +function createMentionElement(option: Option) { + return createUserMention(option.value); +} diff --git a/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx b/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx index 980ea1847..94e88192f 100644 --- a/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx +++ b/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx @@ -37,6 +37,7 @@ export function VariablesExtension({ variables }: VariablesExtensionParameters) onChange, // FIXME: onChange should be passed to } = useVariables(variables); + // TODO: Find a better way maybe? useRegisterExtension({ id: `${EXTENSION_ID}:onKeyDown`, onKeyDown }); return ( @@ -48,6 +49,7 @@ export function VariablesExtension({ variables }: VariablesExtensionParameters) parseSerializedElement={parseSerializedElement} renderElement={renderElement} /> + ((props, forwardedRef) = }), ); - const userMentions = useUserMentions(withUserMentions || undefined); - const [ { isOpen: isFloatingSnippetInputOpen }, { @@ -259,10 +256,6 @@ export const Editor = forwardRef((props, forwardedRef) = }, ] = useFloatingSnippetInput(editor); - if (withUserMentions) { - onKeyDownList.push(userMentions.onKeyDown); - } - const withSpecificProviderOptions = typeof withFloatingAddMenu === 'object' ? withFloatingAddMenu.withSpecificProviderOptions @@ -776,7 +769,7 @@ export const Editor = forwardRef((props, forwardedRef) = */ onChange(newValue as Element[]); // variables.onChange(editor); // FIXME: Find a way to wire `onChange` with VariablesExtension - userMentions.onChange(editor); + // userMentions.onChange(editor); // FIXME: Find a way to wire `onChange` with UserMentionsExtension }} initialValue={getInitialValue()} > @@ -792,13 +785,6 @@ export const Editor = forwardRef((props, forwardedRef) = editor={editor} onCut={createOnCut(editor)} onKeyDown={onKeyDownList} - onKeyDownDeps={[ - userMentions.index, - userMentions.query, - userMentions.target, - withUserMentions, - withVariables, - ]} readOnly={readOnly} renderElementDeps={[availableWidth]} style={contentStyle} @@ -832,17 +818,6 @@ export const Editor = forwardRef((props, forwardedRef) = /> )} - {withUserMentions && ( - - userMentions.onAdd(editor, option) - } - options={userMentions.options} - target={userMentions.target} - /> - )} - {withRichFormattingMenu && ( } - {withUserMentions && } + {withUserMentions && } {withAttachments && ( <> From 7d908968298915e745b61c4df850704d69606cd7 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 00:45:07 +0300 Subject: [PATCH 08/54] [CARE-1802] Render Extensions inside `` context  Conflicts:  packages/slate-editor/src/modules/editor/Editor.tsx --- .../src/modules/editor/Editor.tsx | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 24772a776..614b672b0 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -716,38 +716,6 @@ export const Editor = forwardRef((props, forwardedRef) = return ( - -
((props, forwardedRef) = style={contentStyle} /> + + {!hasCustomPlaceholder && ( From 1c6ef3a694e7f4a8d14230978283e32960c53368 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Thu, 7 Sep 2023 17:49:59 +0300 Subject: [PATCH 09/54] [CARE-1802] Simplify `onKeyDown` prop handling No need to do the `onKeyDownList` & `onKeyDownDeps` yada-yada --- .../editable/EditableWithExtensions.tsx | 14 ++++------- .../src/modules/editor/Editor.tsx | 5 ++-- .../src/modules/editor/useCreateEditor.ts | 23 +++---------------- 3 files changed, 9 insertions(+), 33 deletions(-) diff --git a/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx b/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx index 257cba834..a18e00703 100644 --- a/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx +++ b/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx @@ -43,12 +43,7 @@ export interface Props { onDOMBeforeInput?: OnDOMBeforeInput[]; // Dependencies of `onDOMBeforeInput` onDOMBeforeInputDeps?: any[]; - /** - * Handlers when we press a key - */ - onKeyDown?: OnKeyDown[]; - // Dependencies of `onKeyDown.ts` - onKeyDownDeps?: any[]; + onKeyDown?: OnKeyDown; placeholder?: string; readOnly?: boolean; /** @@ -77,8 +72,7 @@ export function EditableWithExtensions({ editor, onDOMBeforeInput: onDOMBeforeInputList = [], onDOMBeforeInputDeps = [], - onKeyDown: onKeyDownList = [], - onKeyDownDeps = [], + onKeyDown, renderElement: renderElementList = [], renderElementDeps = [], renderLeaf: renderLeafList = [], @@ -97,8 +91,8 @@ export function EditableWithExtensions({ onDOMBeforeInputDeps, ); const combinedOnKeyDown = useCallback( - combineOnKeyDown(editor, extensions, onKeyDownList), - onKeyDownDeps, + combineOnKeyDown(editor, extensions, onKeyDown ? [onKeyDown] : []), + [onKeyDown], ); const combinedRenderElement = useMemo( () => combineRenderElement(editor, extensions, renderElementList), diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 614b672b0..1df78a65f 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -135,10 +135,9 @@ export const Editor = forwardRef((props, forwardedRef) = [setFloatingAddMenuOpen], ); - const { editor, onKeyDownList } = useCreateEditor({ + const editor = useCreateEditor({ events, // extensions, // FIXME - onKeyDown, plugins, }); @@ -752,7 +751,7 @@ export const Editor = forwardRef((props, forwardedRef) = decorate={combinedDecorate} editor={editor} onCut={createOnCut(editor)} - onKeyDown={onKeyDownList} + onKeyDown={onKeyDown} readOnly={readOnly} renderElementDeps={[availableWidth]} style={contentStyle} diff --git a/packages/slate-editor/src/modules/editor/useCreateEditor.ts b/packages/slate-editor/src/modules/editor/useCreateEditor.ts index c1f7dec07..946249d63 100644 --- a/packages/slate-editor/src/modules/editor/useCreateEditor.ts +++ b/packages/slate-editor/src/modules/editor/useCreateEditor.ts @@ -1,6 +1,5 @@ import type { Events } from '@prezly/events'; -import type { Extension, OnKeyDown } from '@prezly/slate-commons'; -import type { KeyboardEvent } from 'react'; +import type { Extension } from '@prezly/slate-commons'; import { useCallback, useEffect, useMemo, useState } from 'react'; import type { Editor } from 'slate'; import { createEditor as createSlateEditor } from 'slate'; @@ -15,15 +14,9 @@ import { createEditor } from './createEditor'; interface Parameters { events: Events; extensions: Extension[]; - onKeyDown?: (event: KeyboardEvent) => void; plugins?: ((editor: T) => T)[] | undefined; } -interface State { - editor: Editor; - onKeyDownList: OnKeyDown[]; -} - type NonUndefined = T extends undefined ? never : T; const DEFAULT_PLUGINS: NonUndefined = []; @@ -31,15 +24,8 @@ const DEFAULT_PLUGINS: NonUndefined = []; export function useCreateEditor({ events, extensions, - onKeyDown, plugins = DEFAULT_PLUGINS, -}: Parameters): State { - const onKeyDownList: OnKeyDown[] = []; - - if (onKeyDown) { - onKeyDownList.push(onKeyDown); - } - +}: Parameters): Editor { // We have to make sure that editor is created only once. // We do it by ensuring dependencies of `useMemo` returning the editor never change. const extensionsRef = useLatest(extensions); @@ -58,8 +44,5 @@ export function useCreateEditor({ } }, [plugins, userPlugins]); - return { - editor, - onKeyDownList, - }; + return editor; } From 8d2a5e31bd0ce3eba819973f22f09f6ae8c9d16d Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 00:48:28 +0300 Subject: [PATCH 10/54] [CARE-1802] Consolidate Snippets UI and state in the SnippetsExtension folder  Conflicts:  packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx --- .../extensions/snippet/SnippetsExtension.tsx | 31 +++++++++++++++++-- .../FloatingSnippetInput.module.scss | 0 .../components}/FloatingSnippetInput.tsx | 0 .../extensions/snippet/components/index.ts | 1 + .../src/extensions/snippet/types.ts | 2 +- .../components/FloatingSnippetInput/index.ts | 1 - .../src/modules/components/index.ts | 1 - .../src/modules/editor/Editor.tsx | 30 ++---------------- .../src/modules/editor/Extensions.tsx | 11 ++++++- .../slate-editor/src/modules/editor/types.ts | 4 +-- 10 files changed, 46 insertions(+), 35 deletions(-) rename packages/slate-editor/src/{modules/components/FloatingSnippetInput => extensions/snippet/components}/FloatingSnippetInput.module.scss (100%) rename packages/slate-editor/src/{modules/components/FloatingSnippetInput => extensions/snippet/components}/FloatingSnippetInput.tsx (100%) create mode 100644 packages/slate-editor/src/extensions/snippet/components/index.ts delete mode 100644 packages/slate-editor/src/modules/components/FloatingSnippetInput/index.ts diff --git a/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx b/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx index ddb694878..b27b8a07b 100644 --- a/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx +++ b/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx @@ -1,9 +1,36 @@ import { useRegisterExtension } from '@prezly/slate-commons'; +import type { RefObject } from 'react'; +import React from 'react'; + +import { FloatingSnippetInput } from './components'; +import { useFloatingSnippetInput } from './lib'; +import type { SnippetsExtensionConfiguration } from './types'; export const EXTENSION_ID = 'SnippetsExtension'; -export function SnippetsExtension() { - return useRegisterExtension({ +export interface Parameters extends SnippetsExtensionConfiguration { + availableWidth: number; + containerRef: RefObject; +} + +export function SnippetsExtension({ availableWidth, containerRef, renderInput }: Parameters) { + const [{ isOpen }, { close, open, rootClose, submit }] = useFloatingSnippetInput(); + + useRegisterExtension({ id: EXTENSION_ID, }); + + return ( + <> + {isOpen && ( + renderInput({ onCreate: submit })} + /> + )} + + ); } diff --git a/packages/slate-editor/src/modules/components/FloatingSnippetInput/FloatingSnippetInput.module.scss b/packages/slate-editor/src/extensions/snippet/components/FloatingSnippetInput.module.scss similarity index 100% rename from packages/slate-editor/src/modules/components/FloatingSnippetInput/FloatingSnippetInput.module.scss rename to packages/slate-editor/src/extensions/snippet/components/FloatingSnippetInput.module.scss diff --git a/packages/slate-editor/src/modules/components/FloatingSnippetInput/FloatingSnippetInput.tsx b/packages/slate-editor/src/extensions/snippet/components/FloatingSnippetInput.tsx similarity index 100% rename from packages/slate-editor/src/modules/components/FloatingSnippetInput/FloatingSnippetInput.tsx rename to packages/slate-editor/src/extensions/snippet/components/FloatingSnippetInput.tsx diff --git a/packages/slate-editor/src/extensions/snippet/components/index.ts b/packages/slate-editor/src/extensions/snippet/components/index.ts new file mode 100644 index 000000000..9d784cc52 --- /dev/null +++ b/packages/slate-editor/src/extensions/snippet/components/index.ts @@ -0,0 +1 @@ +export { FloatingSnippetInput } from './FloatingSnippetInput'; diff --git a/packages/slate-editor/src/extensions/snippet/types.ts b/packages/slate-editor/src/extensions/snippet/types.ts index 58897a5a6..12251a7d7 100644 --- a/packages/slate-editor/src/extensions/snippet/types.ts +++ b/packages/slate-editor/src/extensions/snippet/types.ts @@ -1,6 +1,6 @@ import type { DocumentNode } from '@prezly/slate-types'; import type { ReactNode } from 'react'; -export interface SnippetsExtensionParameters { +export interface SnippetsExtensionConfiguration { renderInput: (args: { onCreate: (documentNode: DocumentNode) => void }) => ReactNode; } diff --git a/packages/slate-editor/src/modules/components/FloatingSnippetInput/index.ts b/packages/slate-editor/src/modules/components/FloatingSnippetInput/index.ts deleted file mode 100644 index 5431c69dc..000000000 --- a/packages/slate-editor/src/modules/components/FloatingSnippetInput/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './FloatingSnippetInput'; diff --git a/packages/slate-editor/src/modules/components/index.ts b/packages/slate-editor/src/modules/components/index.ts index 2d4e41a0f..75387214c 100644 --- a/packages/slate-editor/src/modules/components/index.ts +++ b/packages/slate-editor/src/modules/components/index.ts @@ -1,6 +1,5 @@ export { BookmarkCard } from './BookmarkCard'; export * as FloatingContainer from './FloatingContainer'; -export * from './FloatingSnippetInput'; export { InlineContactForm } from './InlineContactForm'; export { LinkWithTooltip } from './LinkWithTooltip'; export { Placeholder } from './Placeholder'; diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 1df78a65f..4e706581c 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -33,8 +33,7 @@ import { insertButtonBlock } from '#extensions/button-block'; import { FlashNodes } from '#extensions/flash-nodes'; import { FloatingAddMenu, type Option } from '#extensions/floating-add-menu'; import { insertPlaceholder, PlaceholderNode } from '#extensions/placeholders'; -import { useFloatingSnippetInput } from '#extensions/snippet'; -import { FloatingSnippetInput, Placeholder } from '#modules/components'; +import { Placeholder } from '#modules/components'; import { DecorationsProvider } from '#modules/decorations'; import { EditableWithExtensions } from '#modules/editable'; import type { EditorEventMap } from '#modules/events'; @@ -245,16 +244,6 @@ export const Editor = forwardRef((props, forwardedRef) = }), ); - const [ - { isOpen: isFloatingSnippetInputOpen }, - { - close: closeFloatingSnippetInput, - open: openFloatingSnippetInput, - rootClose: rootCloseFloatingSnippetInput, - submit: submitFloatingSnippetInput, - }, - ] = useFloatingSnippetInput(editor); - const withSpecificProviderOptions = typeof withFloatingAddMenu === 'object' ? withFloatingAddMenu.withSpecificProviderOptions @@ -620,7 +609,7 @@ export const Editor = forwardRef((props, forwardedRef) = return; } if (action === MenuAction.ADD_SNIPPET) { - return openFloatingSnippetInput(); + return openFloatingSnippetInput(); // FIXME: Find a way to trigger snippet input } if (action === MenuAction.ADD_GALLERY && withGalleries) { const placeholder = insertPlaceholder( @@ -759,6 +748,7 @@ export const Editor = forwardRef((props, forwardedRef) = ((props, forwardedRef) = withParagraphs /> )} - - {withSnippets && isFloatingSnippetInputOpen && ( - - withSnippets.renderInput({ - onCreate: submitFloatingSnippetInput, - }) - } - /> - )} )} diff --git a/packages/slate-editor/src/modules/editor/Extensions.tsx b/packages/slate-editor/src/modules/editor/Extensions.tsx index f3b803fe8..7d0b6af29 100644 --- a/packages/slate-editor/src/modules/editor/Extensions.tsx +++ b/packages/slate-editor/src/modules/editor/Extensions.tsx @@ -1,5 +1,6 @@ import { EditorCommands } from '@prezly/slate-commons'; import { isImageNode, isQuoteNode } from '@prezly/slate-types'; +import type { RefObject } from 'react'; import React from 'react'; import { Node } from 'slate'; @@ -57,6 +58,7 @@ import type { EditorProps } from './types'; type Props = { availableWidth: number; + containerRef: RefObject; onFloatingAddMenuToggle: (show: boolean, trigger: 'input' | 'hotkey') => void; } & Pick< Required, @@ -91,6 +93,7 @@ type Props = { export function Extensions({ availableWidth, + containerRef, onFloatingAddMenuToggle, withAllowedBlocks, withAttachments, @@ -342,7 +345,13 @@ export function Extensions({ withWebBookmarkPlaceholders={withWebBookmarks} /> - {withSnippets && } + {withSnippets && ( + + )} {withStoryEmbeds && } diff --git a/packages/slate-editor/src/modules/editor/types.ts b/packages/slate-editor/src/modules/editor/types.ts index 775a1c752..d9436a497 100644 --- a/packages/slate-editor/src/modules/editor/types.ts +++ b/packages/slate-editor/src/modules/editor/types.ts @@ -19,7 +19,7 @@ import type { PlaceholderNode, PlaceholdersExtensionParameters, } from '#extensions/placeholders'; -import type { SnippetsExtensionParameters } from '#extensions/snippet'; +import type { SnippetsExtensionConfiguration } from '#extensions/snippet'; import type { StoryBookmarkExtensionParameters } from '#extensions/story-bookmark'; import type { StoryEmbedExtensionParameters } from '#extensions/story-embed'; import type { UserMentionsExtensionParameters } from '#extensions/user-mentions'; @@ -140,7 +140,7 @@ export interface EditorProps { | false | (StoryEmbedExtensionParameters & PlaceholdersExtensionParameters['withStoryEmbedPlaceholders']); - withSnippets?: false | SnippetsExtensionParameters; + withSnippets?: false | SnippetsExtensionConfiguration; withTables?: boolean; withTextStyling?: boolean; withUserMentions?: false | UserMentionsExtensionParameters; From f52b0a9fe23cd19c3033bc35f24effff21f7c33a Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 8 Sep 2023 15:59:33 +0300 Subject: [PATCH 11/54] [CARE-1802] Solve `onChange` callback for UserMentions & Variables extensions --- .../src/extensions/mentions/useMentions.ts | 13 +++++-------- .../user-mentions/UserMentionsExtension.tsx | 3 +-- .../src/extensions/variables/VariablesExtension.tsx | 9 +-------- packages/slate-editor/src/modules/editor/Editor.tsx | 2 -- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/packages/slate-editor/src/extensions/mentions/useMentions.ts b/packages/slate-editor/src/extensions/mentions/useMentions.ts index fb1f1b4bb..da4025a4d 100644 --- a/packages/slate-editor/src/extensions/mentions/useMentions.ts +++ b/packages/slate-editor/src/extensions/mentions/useMentions.ts @@ -1,9 +1,8 @@ import { stubTrue } from '@technically/lodash'; import { isHotkey } from 'is-hotkey'; -import type { KeyboardEvent } from 'react'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState, type KeyboardEvent } from 'react'; import { Range, Transforms } from 'slate'; -import { useSlateStatic } from 'slate-react'; +import { useSlate } from 'slate-react'; import { getWordAfterTrigger, insertMention, isPointAtWordEnd } from './lib'; import type { MentionElementType, Option } from './types'; @@ -18,7 +17,6 @@ interface Parameters { export interface Mentions { index: number; onAdd: (option: Option) => void; - onChange: () => void; onKeyDown: (event: KeyboardEvent) => void; options: Option[]; query: string; @@ -31,7 +29,7 @@ export function useMentions({ options, trigger, }: Parameters): Mentions { - const editor = useSlateStatic(); + const editor = useSlate(); // `useSlate()` is to react to the editor changes const [index, setIndex] = useState(0); const [query, setQuery] = useState(''); @@ -54,7 +52,7 @@ export function useMentions({ [editor, createMentionElement, target], ); - const onChange = useCallback(() => { + useEffect(() => { const { selection } = editor; if (selection && Range.isCollapsed(selection)) { @@ -70,7 +68,7 @@ export function useMentions({ } setTarget(null); - }, [editor, setIndex, setQuery, trigger]); + }, [editor.children, editor.selection]); const onKeyDown = useCallback( (event: KeyboardEvent) => { @@ -107,7 +105,6 @@ export function useMentions({ return { index, onAdd, - onChange, onKeyDown, options: filteredOptions, query, diff --git a/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx b/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx index b8355b50c..af2e2bab4 100644 --- a/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx +++ b/packages/slate-editor/src/extensions/user-mentions/UserMentionsExtension.tsx @@ -13,8 +13,7 @@ import { useUserMentions } from './useUserMentions'; export const EXTENSION_ID = 'UserMentionsExtension'; export function UserMentionsExtension({ users }: UserMentionsExtensionParameters) { - // FIXME: onChange should be passed to - const { index, target, options, onAdd, onChange, onKeyDown } = useUserMentions(users); + const { index, target, options, onAdd, onKeyDown } = useUserMentions(users); // TODO: Find a better way maybe? useRegisterExtension({ id: `${EXTENSION_ID}:onKeyDown`, onKeyDown }); diff --git a/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx b/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx index 94e88192f..4513b5493 100644 --- a/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx +++ b/packages/slate-editor/src/extensions/variables/VariablesExtension.tsx @@ -28,14 +28,7 @@ export function VariablesExtension({ variables }: VariablesExtensionParameters) [JSON.stringify(variablesNames)], ); - const { - index, - target, - options, - onAdd, - onKeyDown, - onChange, // FIXME: onChange should be passed to - } = useVariables(variables); + const { index, target, options, onAdd, onKeyDown } = useVariables(variables); // TODO: Find a better way maybe? useRegisterExtension({ id: `${EXTENSION_ID}:onKeyDown`, onKeyDown }); diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 4e706581c..3de41d613 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -724,8 +724,6 @@ export const Editor = forwardRef((props, forwardedRef) = * in two" work as expected. */ onChange(newValue as Element[]); - // variables.onChange(editor); // FIXME: Find a way to wire `onChange` with VariablesExtension - // userMentions.onChange(editor); // FIXME: Find a way to wire `onChange` with UserMentionsExtension }} initialValue={getInitialValue()} > From f0f44671e71d1debb0b2174671f5fddb0402fe4c Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 8 Sep 2023 16:01:57 +0300 Subject: [PATCH 12/54] [CARE-1802] Comment out broken code --- packages/slate-editor/src/modules/editor/Editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 3de41d613..392cbe521 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -609,7 +609,7 @@ export const Editor = forwardRef((props, forwardedRef) = return; } if (action === MenuAction.ADD_SNIPPET) { - return openFloatingSnippetInput(); // FIXME: Find a way to trigger snippet input + // return openFloatingSnippetInput(); // FIXME: Find a way to trigger snippet input } if (action === MenuAction.ADD_GALLERY && withGalleries) { const placeholder = insertPlaceholder( From 5a93734f9e297658c85035157c655977f66996a2 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Thu, 14 Sep 2023 16:05:43 +0300 Subject: [PATCH 13/54] [CARE-1802] Improve error message --- packages/slate-commons/src/extensions/context.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/slate-commons/src/extensions/context.tsx b/packages/slate-commons/src/extensions/context.tsx index 9c7dfc091..756860f4c 100644 --- a/packages/slate-commons/src/extensions/context.tsx +++ b/packages/slate-commons/src/extensions/context.tsx @@ -7,7 +7,7 @@ import type { ExtensionsManager } from './types'; const NULL_MANAGER: ExtensionsManager = { register() { throw new Error( - 'It is required to wrap code using ExtensionsManager into ExtensionsManagerProvider.', + 'It is required to wrap any code using ExtensionsManager into ExtensionsManagerProvider.', ); }, }; From 94e5661513f5152d4a162e6560d132de348659b1 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Thu, 14 Sep 2023 16:21:32 +0300 Subject: [PATCH 14/54] [CARE-1802] Move FlashNodes rendering to the FlashNodesExtension --- .../flash-nodes/FlashNodesExtension.tsx | 13 ++++++++-- .../flash-nodes/components/FlashNodes.tsx | 26 +++++++++---------- .../src/modules/editor/Editor.tsx | 4 +-- .../src/modules/editor/Extensions.tsx | 2 -- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx b/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx index c046d6719..3af078e1c 100644 --- a/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx +++ b/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx @@ -1,7 +1,14 @@ import { useRegisterExtension } from '@prezly/slate-commons'; +import React from 'react'; -export function FlashNodesExtension() { - return useRegisterExtension({ +import { FlashNodes } from './components/FlashNodes'; + +export interface Parameters { + containerElement: HTMLElement | null | undefined; +} + +export function FlashNodesExtension({ containerElement }: Parameters) { + useRegisterExtension({ id: 'FlashNodesExtension', withOverrides: (editor) => { editor.nodesToFlash = []; @@ -17,4 +24,6 @@ export function FlashNodesExtension() { return editor; }, }); + + return ; } diff --git a/packages/slate-editor/src/extensions/flash-nodes/components/FlashNodes.tsx b/packages/slate-editor/src/extensions/flash-nodes/components/FlashNodes.tsx index cea44ec4d..af14ea80f 100644 --- a/packages/slate-editor/src/extensions/flash-nodes/components/FlashNodes.tsx +++ b/packages/slate-editor/src/extensions/flash-nodes/components/FlashNodes.tsx @@ -1,11 +1,14 @@ -import type { RefObject } from 'react'; import React, { useEffect, useState } from 'react'; import type { Editor, Node } from 'slate'; import { ReactEditor, useSlateStatic } from 'slate-react'; import styles from './FlashNodes.module.scss'; -export function FlashNodes({ containerRef }: { containerRef: RefObject }) { +interface Props { + containerElement: HTMLElement | null | undefined; +} + +export function FlashNodes({ containerElement }: Props) { const editor = useSlateStatic(); return ( @@ -15,7 +18,7 @@ export function FlashNodes({ containerRef }: { containerRef: RefObject (editor.nodesToFlash = editor.nodesToFlash.filter( @@ -28,28 +31,23 @@ export function FlashNodes({ containerRef }: { containerRef: RefObject; + containerElement: HTMLElement | null | undefined; onComplete: () => void; }) { + const { editor, top, bottom, containerElement, onComplete } = props; const [rect, setRect] = useState | undefined>(undefined); useEffect(() => { - if (!containerRef.current) { + if (!containerElement) { return; } try { - const containerRect = containerRef.current.getBoundingClientRect(); + const containerRect = containerElement.getBoundingClientRect(); const rectA = ReactEditor.toDOMNode(editor, top).getBoundingClientRect(); const rectB = ReactEditor.toDOMNode(editor, bottom).getBoundingClientRect(); @@ -63,7 +61,7 @@ function Flasher({ console.error(error); onComplete(); } - }, []); + }, [containerElement]); return
onComplete()} style={rect} />; } diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 392cbe521..bbebd9517 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -30,7 +30,7 @@ import { ReactEditor, Slate } from 'slate-react'; import { useFunction, useGetSet, useSize } from '#lib'; import { insertButtonBlock } from '#extensions/button-block'; -import { FlashNodes } from '#extensions/flash-nodes'; +import { FlashNodesExtension } from '#extensions/flash-nodes'; import { FloatingAddMenu, type Option } from '#extensions/floating-add-menu'; import { insertPlaceholder, PlaceholderNode } from '#extensions/placeholders'; import { Placeholder } from '#modules/components'; @@ -777,7 +777,7 @@ export const Editor = forwardRef((props, forwardedRef) = withWebBookmarks={withWebBookmarks} /> - + {!hasCustomPlaceholder && ( diff --git a/packages/slate-editor/src/modules/editor/Extensions.tsx b/packages/slate-editor/src/modules/editor/Extensions.tsx index 7d0b6af29..946016c6f 100644 --- a/packages/slate-editor/src/modules/editor/Extensions.tsx +++ b/packages/slate-editor/src/modules/editor/Extensions.tsx @@ -140,8 +140,6 @@ export function Extensions({ using the `useDecorationFactory()` call in the RichFormattingMenu component. */} - - From f5a05fb6df205b935a89f837a6da662d5fb80ba6 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Thu, 14 Sep 2023 16:43:17 +0300 Subject: [PATCH 15/54] [CARE-1802] Optimize TextStylingExtension by ensuring referential equality --- .../text-styling/TextStylingExtension.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/slate-editor/src/extensions/text-styling/TextStylingExtension.tsx b/packages/slate-editor/src/extensions/text-styling/TextStylingExtension.tsx index 06b835570..a744e60f6 100644 --- a/packages/slate-editor/src/extensions/text-styling/TextStylingExtension.tsx +++ b/packages/slate-editor/src/extensions/text-styling/TextStylingExtension.tsx @@ -1,5 +1,7 @@ +import type { DeserializeHtml } from '@prezly/slate-commons'; import { useRegisterExtension } from '@prezly/slate-commons'; import React from 'react'; +import type { RenderLeafProps } from 'slate-react'; import { Text } from './components'; import { detectMarks } from './lib'; @@ -7,13 +9,19 @@ import { onKeyDown } from './onKeyDown'; export const EXTENSION_ID = 'TextStylingExtension'; +const DESERIALIZE: DeserializeHtml = { + marks: detectMarks, +}; + export function TextStylingExtension() { return useRegisterExtension({ id: EXTENSION_ID, - deserialize: { - marks: detectMarks, - }, + deserialize: DESERIALIZE, onKeyDown, - renderLeaf: (props) => , + renderLeaf, }); } + +function renderLeaf(props: RenderLeafProps) { + return ; +} From e4f434e3b290727d8fb6a1532c1c238a3a4fdc6d Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 15 Sep 2023 16:31:32 +0300 Subject: [PATCH 16/54] [CARE-1802] Reogranize Extensions manager module --- .../src/extensions/ExtensionManager.tsx | 72 +++++++++++++++++++ .../extensions/ExtensionManagerProvider.tsx | 37 ---------- .../slate-commons/src/extensions/context.tsx | 16 ----- .../slate-commons/src/extensions/index.ts | 6 +- .../slate-commons/src/extensions/types.ts | 7 -- .../src/extensions/useExtensions.ts | 9 --- .../src/extensions/useExtensionsManager.ts | 8 --- .../src/extensions/useRegisterExtension.ts | 2 +- .../src/modules/editor/Editor.tsx | 6 +- 9 files changed, 77 insertions(+), 86 deletions(-) create mode 100644 packages/slate-commons/src/extensions/ExtensionManager.tsx delete mode 100644 packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx delete mode 100644 packages/slate-commons/src/extensions/context.tsx delete mode 100644 packages/slate-commons/src/extensions/types.ts delete mode 100644 packages/slate-commons/src/extensions/useExtensions.ts delete mode 100644 packages/slate-commons/src/extensions/useExtensionsManager.ts diff --git a/packages/slate-commons/src/extensions/ExtensionManager.tsx b/packages/slate-commons/src/extensions/ExtensionManager.tsx new file mode 100644 index 000000000..15a1b0011 --- /dev/null +++ b/packages/slate-commons/src/extensions/ExtensionManager.tsx @@ -0,0 +1,72 @@ +import React, { createContext, type ReactNode, useContext, useMemo, useState } from 'react'; + +import type { Extension } from '../types'; + +export interface ExtensionsManager { + register(extension: Extension | Extension[]): UnregisterFn; +} + +type UnregisterFn = () => void; + +/** + * -- CONTEXT -- + * ============= + */ + +export const ManagerContext = createContext({ + register() { + throw new Error( + 'It is required to wrap any code using ExtensionsManager into ExtensionsManagerProvider.', + ); + }, +}); +export const ExtensionsContext = createContext([]); + +/** + * -- HOOKS -- + * =========== + */ + +export function useExtensionsManager(): ExtensionsManager { + return useContext(ManagerContext); +} + +export function useExtensions(): Extension[] { + return useContext(ExtensionsContext); +} + +/** + * -- COMPONENTS -- + * ================ + */ + +interface Props { + children: ReactNode; +} + +export function ExtensionsManager({ children }: Props) { + type Entry = { extension: Extension }; + + const [entries, setEntries] = useState([]); + const [manager] = useState(() => ({ + register(extension) { + const entries: Entry[] = (Array.isArray(extension) ? extension : [extension]).map( + (extension) => ({ extension }), + ); + + setEntries((es) => [...es, ...entries]); + + return () => { + setEntries((es) => es.filter((e) => !entries.includes(e))); + }; + }, + })); + + const extensions = useMemo(() => entries.map(({ extension }) => extension), [entries]); + + return ( + + {children} + + ); +} diff --git a/packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx b/packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx deleted file mode 100644 index 4c31adf50..000000000 --- a/packages/slate-commons/src/extensions/ExtensionManagerProvider.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { useMemo, useState, type ReactNode } from 'react'; - -import type { Extension } from '../types'; - -import { ExtensionsContext, ExtensionsManagerContext } from './context'; -import type { ExtensionsManager } from './types'; - -interface Props { - children: ReactNode; -} - -export function ExtensionsManagerProvider({ children }: Props) { - type Entry = { extension: Extension }; - - const [entries, setEntries] = useState([]); - const [manager] = useState(() => ({ - register(extension) { - const entries: Entry[] = (Array.isArray(extension) ? extension : [extension]).map( - (extension) => ({ extension }), - ); - - setEntries((es) => [...es, ...entries]); - - return () => { - setEntries((es) => es.filter((e) => !entries.includes(e))); - }; - }, - })); - - const extensions = useMemo(() => entries.map(({ extension }) => extension), [entries]); - - return ( - - {children} - - ); -} diff --git a/packages/slate-commons/src/extensions/context.tsx b/packages/slate-commons/src/extensions/context.tsx deleted file mode 100644 index 756860f4c..000000000 --- a/packages/slate-commons/src/extensions/context.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { createContext } from 'react'; - -import type { Extension } from '../types'; - -import type { ExtensionsManager } from './types'; - -const NULL_MANAGER: ExtensionsManager = { - register() { - throw new Error( - 'It is required to wrap any code using ExtensionsManager into ExtensionsManagerProvider.', - ); - }, -}; - -export const ExtensionsManagerContext = createContext(NULL_MANAGER); -export const ExtensionsContext = createContext([]); diff --git a/packages/slate-commons/src/extensions/index.ts b/packages/slate-commons/src/extensions/index.ts index 47d749f05..31a4995af 100644 --- a/packages/slate-commons/src/extensions/index.ts +++ b/packages/slate-commons/src/extensions/index.ts @@ -1,7 +1,3 @@ -export type { ExtensionsManager } from './types'; +export { ExtensionsManager, useExtensionsManager, useExtensions } from './ExtensionManager'; -export { useExtensions } from './useExtensions'; -export { useExtensionsManager } from './useExtensionsManager'; export { useRegisterExtension } from './useRegisterExtension'; - -export { ExtensionsManagerProvider } from './ExtensionManagerProvider'; diff --git a/packages/slate-commons/src/extensions/types.ts b/packages/slate-commons/src/extensions/types.ts deleted file mode 100644 index fd00c23ff..000000000 --- a/packages/slate-commons/src/extensions/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Extension } from '../types'; - -export interface ExtensionsManager { - register(extension: Extension | Extension[]): UnregisterFn; -} - -export type UnregisterFn = () => void; diff --git a/packages/slate-commons/src/extensions/useExtensions.ts b/packages/slate-commons/src/extensions/useExtensions.ts deleted file mode 100644 index b3c7dac51..000000000 --- a/packages/slate-commons/src/extensions/useExtensions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useContext } from 'react'; - -import type { Extension } from '../types'; - -import { ExtensionsContext } from './context'; - -export function useExtensions(): Extension[] { - return useContext(ExtensionsContext); -} diff --git a/packages/slate-commons/src/extensions/useExtensionsManager.ts b/packages/slate-commons/src/extensions/useExtensionsManager.ts deleted file mode 100644 index 0c9cf234c..000000000 --- a/packages/slate-commons/src/extensions/useExtensionsManager.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useContext } from 'react'; - -import { ExtensionsManagerContext } from './context'; -import type { ExtensionsManager } from './types'; - -export function useExtensionsManager(): ExtensionsManager { - return useContext(ExtensionsManagerContext); -} diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts index 0b8708612..5fe70130e 100644 --- a/packages/slate-commons/src/extensions/useRegisterExtension.ts +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import type { Extension } from '../types'; -import { useExtensionsManager } from './useExtensionsManager'; +import { useExtensionsManager } from './ExtensionManager'; export function useRegisterExtension(extension: Extension): null { const manager = useExtensionsManager(); diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index bbebd9517..9c1a088e4 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/display-name */ import { Events } from '@prezly/events'; -import { EditorCommands, ExtensionsManagerProvider } from '@prezly/slate-commons'; +import { EditorCommands, ExtensionsManager } from '@prezly/slate-commons'; import { TablesEditor } from '@prezly/slate-tables'; import { type HeadingNode, @@ -703,7 +703,7 @@ export const Editor = forwardRef((props, forwardedRef) = }); return ( - +
((props, forwardedRef) =
-
+
); }); From 8ec34b512d27bcf14d93837be773c0c0e3352f4a Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 15 Sep 2023 17:11:24 +0300 Subject: [PATCH 17/54] [CARE-1802] Rewrite ExtensionsManager to keep `editor.extensions` up to date --- .../src/extensions/ExtensionManager.tsx | 76 +++++++++++-------- .../src/extensions/ExtensionsEditor.ts | 13 ++++ .../slate-commons/src/extensions/index.ts | 4 +- packages/slate-editor/src/index.ts | 3 +- .../src/modules/editor/Editor.tsx | 2 +- .../src/modules/editor/useCreateEditor.ts | 5 +- 6 files changed, 65 insertions(+), 38 deletions(-) create mode 100644 packages/slate-commons/src/extensions/ExtensionsEditor.ts diff --git a/packages/slate-commons/src/extensions/ExtensionManager.tsx b/packages/slate-commons/src/extensions/ExtensionManager.tsx index 15a1b0011..692f3672b 100644 --- a/packages/slate-commons/src/extensions/ExtensionManager.tsx +++ b/packages/slate-commons/src/extensions/ExtensionManager.tsx @@ -1,9 +1,12 @@ -import React, { createContext, type ReactNode, useContext, useMemo, useState } from 'react'; +import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react'; +import type { BaseEditor } from 'slate'; import type { Extension } from '../types'; +import type { ExtensionsEditor } from './ExtensionsEditor'; + export interface ExtensionsManager { - register(extension: Extension | Extension[]): UnregisterFn; + register(extension: Extension): UnregisterFn; } type UnregisterFn = () => void; @@ -20,7 +23,6 @@ export const ManagerContext = createContext({ ); }, }); -export const ExtensionsContext = createContext([]); /** * -- HOOKS -- @@ -31,42 +33,50 @@ export function useExtensionsManager(): ExtensionsManager { return useContext(ManagerContext); } -export function useExtensions(): Extension[] { - return useContext(ExtensionsContext); -} - /** * -- COMPONENTS -- * ================ */ -interface Props { +interface Props { children: ReactNode; + editor: T; } -export function ExtensionsManager({ children }: Props) { - type Entry = { extension: Extension }; - - const [entries, setEntries] = useState([]); - const [manager] = useState(() => ({ - register(extension) { - const entries: Entry[] = (Array.isArray(extension) ? extension : [extension]).map( - (extension) => ({ extension }), - ); - - setEntries((es) => [...es, ...entries]); - - return () => { - setEntries((es) => es.filter((e) => !entries.includes(e))); - }; - }, - })); - - const extensions = useMemo(() => entries.map(({ extension }) => extension), [entries]); - - return ( - - {children} - - ); +type Entry = { extension: Extension }; + +const EDITOR_EXTENSIONS = new WeakMap(); + +export function ExtensionsManager({ children, editor }: Props) { + const [counter, setCounter] = useState(0); + const [manager] = useState(() => { + function updateEntries(editor: T, updater: (entries: Entry[]) => Entry[]) { + const entries = EDITOR_EXTENSIONS.get(editor) ?? []; + const updatedEntries = updater(entries); + + EDITOR_EXTENSIONS.set(editor, updatedEntries); + editor.extensions = updatedEntries.map(({ extension }) => extension); + setCounter((c) => c + 1); + } + + return { + register(extension) { + const entry = { extension }; + updateEntries(editor, (entries) => [...entries, entry]); + + return () => { + updateEntries(editor, (entries) => entries.filter((e) => e !== entry)); + }; + }, + }; + }); + + /** + * Force editor re-rendering every time the extensions list is changed. + */ + useEffect(() => { + editor.onChange(); + }, [counter]); + + return {children}; } diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts new file mode 100644 index 000000000..037dd6034 --- /dev/null +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -0,0 +1,13 @@ +import type { BaseEditor } from 'slate'; + +import type { Extension } from '../types'; + +export interface ExtensionsEditor extends BaseEditor { + extensions: Extension[]; +} + +export function withExtensions(editor: T): T & ExtensionsEditor { + return Object.assign(editor, { + extensions: [], + }); +} diff --git a/packages/slate-commons/src/extensions/index.ts b/packages/slate-commons/src/extensions/index.ts index 31a4995af..aa3f83d98 100644 --- a/packages/slate-commons/src/extensions/index.ts +++ b/packages/slate-commons/src/extensions/index.ts @@ -1,3 +1,5 @@ -export { ExtensionsManager, useExtensionsManager, useExtensions } from './ExtensionManager'; +export { ExtensionsManager, useExtensionsManager } from './ExtensionManager'; export { useRegisterExtension } from './useRegisterExtension'; + +export { type ExtensionsEditor, withExtensions } from './ExtensionsEditor'; diff --git a/packages/slate-editor/src/index.ts b/packages/slate-editor/src/index.ts index ef232afe0..b1e11f91d 100644 --- a/packages/slate-editor/src/index.ts +++ b/packages/slate-editor/src/index.ts @@ -18,7 +18,7 @@ export type { User } from './extensions/user-mentions'; export { type ResultPromise, type UploadcareOptions, withUploadcare } from './modules/uploadcare'; // Editor type - +import type { ExtensionsEditor } from '@prezly/slate-commons'; import type { ElementNode, ParagraphNode, TextNode } from '@prezly/slate-types'; import type { BaseEditor } from 'slate'; import type { HistoryEditor } from 'slate-history'; @@ -35,6 +35,7 @@ import type { type Editor = BaseEditor & ReactEditor & HistoryEditor & + ExtensionsEditor & DefaultTextBlockEditor & ElementsEqualityCheckEditor & RichBlocksAwareEditor & diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 9c1a088e4..8a394d54a 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -703,7 +703,7 @@ export const Editor = forwardRef((props, forwardedRef) = }); return ( - +
[withEvents(events), ...userPlugins], [userPlugins, events]); const editor = useMemo(() => { - return createEditor(createSlateEditor(), getExtensions, finalPlugins); - }, [getExtensions, finalPlugins]); + return withExtensions(createEditor(createSlateEditor(), getExtensions, finalPlugins)); + }, [getExtensions, userPlugins]); useEffect(() => { if (plugins !== userPlugins) { From 825e8557cdbb3bf0fe831866c7a603d2213fb557 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 00:52:42 +0300 Subject: [PATCH 18/54] [CARE-1802] Move RichFormattingMenu UI into the new `RichFormattingMenuExtension`  Conflicts:  packages/slate-editor/src/modules/editor/Editor.tsx  packages/slate-editor/src/modules/editor/Extensions.tsx --- .../floating-add-menu/FloatingAddMenu.tsx | 11 +++- .../FloatingAddMenuExtension.tsx | 58 ++++++++++++++++--- .../src/extensions/floating-add-menu/types.ts | 7 +-- .../rich-formatting-menu/LinkMenu.tsx | 0 .../RichFormattingMenu.module.scss | 0 .../RichFormattingMenu.tsx | 4 +- .../RichFormattingMenuExtension.tsx | 41 +++++++++++++ .../components/FormattingDropdown.module.scss | 0 .../components/FormattingDropdown.tsx | 0 .../components/Toolbar.tsx | 0 .../rich-formatting-menu/components/index.ts | 0 .../rich-formatting-menu/index.ts | 1 + .../rich-formatting-menu/lib/convertLink.ts | 0 .../lib/findRichFormattingTextParent.ts | 0 .../lib/getCurrentFormatting.test.tsx | 0 .../lib/getCurrentFormatting.ts | 0 .../rich-formatting-menu/lib/index.ts | 0 .../lib/isSelectionSupported.ts | 0 .../lib/keepToolbarInTextColumn.ts | 0 .../lib/restoreSelection.ts | 0 .../lib/toggleBlock.test.tsx | 0 .../rich-formatting-menu/lib/toggleBlock.ts | 0 .../lib/useLinkCandidateElement.ts | 0 .../rich-formatting-menu/lib/useRangeRef.ts | 0 .../rich-formatting-menu/types.ts | 0 .../src/modules/editor/Editor.tsx | 38 ++---------- .../src/modules/editor/Extensions.tsx | 12 ---- .../src/modules/editor/autoformatRules.ts | 2 +- 28 files changed, 111 insertions(+), 63 deletions(-) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/LinkMenu.tsx (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/RichFormattingMenu.module.scss (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/RichFormattingMenu.tsx (99%) create mode 100644 packages/slate-editor/src/extensions/rich-formatting-menu/RichFormattingMenuExtension.tsx rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/components/FormattingDropdown.module.scss (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/components/FormattingDropdown.tsx (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/components/Toolbar.tsx (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/components/index.ts (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/index.ts (51%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/convertLink.ts (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/findRichFormattingTextParent.ts (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/getCurrentFormatting.test.tsx (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/getCurrentFormatting.ts (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/index.ts (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/isSelectionSupported.ts (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/keepToolbarInTextColumn.ts (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/restoreSelection.ts (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/toggleBlock.test.tsx (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/toggleBlock.ts (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/useLinkCandidateElement.ts (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/lib/useRangeRef.ts (100%) rename packages/slate-editor/src/{modules => extensions}/rich-formatting-menu/types.ts (100%) diff --git a/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenu.tsx b/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenu.tsx index 231241d8b..2b74b6c4d 100644 --- a/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenu.tsx +++ b/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenu.tsx @@ -7,7 +7,7 @@ import { } from '@prezly/slate-types'; import classNames from 'classnames'; import { isHotkey } from 'is-hotkey'; -import type { KeyboardEvent, RefObject } from 'react'; +import type { KeyboardEvent, RefObject, ReactNode } from 'react'; import React, { useEffect, useState } from 'react'; import type { Modifier } from 'react-popper'; import { Node, Transforms } from 'slate'; @@ -29,9 +29,9 @@ import { useKeyboardFiltering, useMenuToggle, } from './lib'; -import type { Option, ExtensionConfiguration } from './types'; +import type { Option } from './types'; -interface Props extends ExtensionConfiguration { +export interface Props { availableWidth: number; containerRef: RefObject; open: boolean; @@ -40,6 +40,11 @@ interface Props extends ExtensionConfiguration { onFilter?: (query: string, resultsCount: number) => void; onToggle: (isShown: boolean) => void; showTooltipByDefault: boolean; + tooltip?: { + placement: 'top' | 'right' | 'bottom' | 'left'; + content: ReactNode; + }; + withSpecificProviderOptions?: boolean; } const TOOLTIP_FLIP_MODIFIER: Modifier<'flip'> = { diff --git a/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenuExtension.tsx b/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenuExtension.tsx index c0c43e6db..3a0ee4c1b 100644 --- a/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenuExtension.tsx +++ b/packages/slate-editor/src/extensions/floating-add-menu/FloatingAddMenuExtension.tsx @@ -1,30 +1,70 @@ -import { useRegisterExtension } from '@prezly/slate-commons'; +import { EditorCommands, useRegisterExtension } from '@prezly/slate-commons'; import { isHotkey } from 'is-hotkey'; +import React, { useCallback, useState } from 'react'; +import { useSlateStatic } from 'slate-react'; +import { EventsEditor } from '#modules/events'; + +import type { Props as FoatingAddMenuProps } from './FloatingAddMenu'; +import { FloatingAddMenu } from './FloatingAddMenu'; import { isMenuHotkey, MENU_TRIGGER_CHARACTER, shouldShowMenuButton } from './lib'; const isTriggerHotkey = isHotkey(`shift?+${MENU_TRIGGER_CHARACTER}`, { byKey: true }); export const EXTENSION_ID = 'FloatingAddMenuExtension'; -interface Parameters { - onOpen: (trigger: 'hotkey' | 'input') => void; -} +export type Parameters = Omit, 'open' | 'onToggle'>; -export function FloatingAddMenuExtension({ onOpen }: Parameters) { - return useRegisterExtension({ +export function FloatingAddMenuExtension({ + tooltip, + availableWidth, + containerRef, + options, + onActivate, + onFilter, +}: Parameters) { + const editor = useSlateStatic(); + const [isFloatingAddMenuOpen, setFloatingAddMenuOpen] = useState(false); + + const onFloatingAddMenuToggle = useCallback( + function (shouldOpen: boolean, trigger: 'click' | 'hotkey' | 'input') { + setFloatingAddMenuOpen(shouldOpen); + if (shouldOpen) { + EventsEditor.dispatchEvent(editor, 'add-button-menu-opened', { trigger }); + } else { + EventsEditor.dispatchEvent(editor, 'add-button-menu-closed'); + } + }, + [setFloatingAddMenuOpen], + ); + + useRegisterExtension({ id: EXTENSION_ID, onKeyDown(event, editor) { if (isMenuHotkey(event) && shouldShowMenuButton(editor)) { - onOpen('hotkey'); + onFloatingAddMenuToggle(true, 'hotkey'); return true; } if (isTriggerHotkey(event) && shouldShowMenuButton(editor)) { - onOpen('input'); - // not returning true intentionally + onFloatingAddMenuToggle(true, 'input'); + return false; // returning false intentionally } return false; }, }); + + return ( + onFloatingAddMenuToggle(toggle, 'click')} + options={options} + showTooltipByDefault={EditorCommands.isEmpty(editor)} + /> + ); } diff --git a/packages/slate-editor/src/extensions/floating-add-menu/types.ts b/packages/slate-editor/src/extensions/floating-add-menu/types.ts index 951c3ead0..a65af1359 100644 --- a/packages/slate-editor/src/extensions/floating-add-menu/types.ts +++ b/packages/slate-editor/src/extensions/floating-add-menu/types.ts @@ -1,5 +1,7 @@ import type { ComponentType, ReactNode } from 'react'; +import type { Parameters } from './FloatingAddMenuExtension'; + type Order = number; export interface Option { @@ -16,9 +18,6 @@ export interface Option { } export interface ExtensionConfiguration { - tooltip?: { - placement: 'top' | 'right' | 'bottom' | 'left'; - content: ReactNode; - }; + tooltip?: Parameters['tooltip']; withSpecificProviderOptions?: boolean; } diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/LinkMenu.tsx b/packages/slate-editor/src/extensions/rich-formatting-menu/LinkMenu.tsx similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/LinkMenu.tsx rename to packages/slate-editor/src/extensions/rich-formatting-menu/LinkMenu.tsx diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/RichFormattingMenu.module.scss b/packages/slate-editor/src/extensions/rich-formatting-menu/RichFormattingMenu.module.scss similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/RichFormattingMenu.module.scss rename to packages/slate-editor/src/extensions/rich-formatting-menu/RichFormattingMenu.module.scss diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/RichFormattingMenu.tsx b/packages/slate-editor/src/extensions/rich-formatting-menu/RichFormattingMenu.tsx similarity index 99% rename from packages/slate-editor/src/modules/rich-formatting-menu/RichFormattingMenu.tsx rename to packages/slate-editor/src/extensions/rich-formatting-menu/RichFormattingMenu.tsx index b422efd21..d2420a81a 100644 --- a/packages/slate-editor/src/modules/rich-formatting-menu/RichFormattingMenu.tsx +++ b/packages/slate-editor/src/extensions/rich-formatting-menu/RichFormattingMenu.tsx @@ -14,7 +14,6 @@ import { decorateSelectionFactory } from '#extensions/decorate-selection'; import { unwrapLink, wrapInLink } from '#extensions/inline-links'; import { MarkType } from '#extensions/text-styling'; import { useDecorationFactory } from '#modules/decorations'; -import { toggleBlock } from '#modules/rich-formatting-menu'; import { Toolbar } from './components'; import { @@ -22,13 +21,14 @@ import { getCurrentFormatting, isSelectionSupported, keepToolbarInTextColumn, + toggleBlock, useRangeRef, } from './lib'; import { LinkMenu } from './LinkMenu'; import styles from './RichFormattingMenu.module.scss'; import type { FetchOEmbedFn, Formatting, Presentation } from './types'; -interface Props { +export interface Props { availableWidth: number; containerElement: HTMLElement | null; defaultAlignment: Alignment; diff --git a/packages/slate-editor/src/extensions/rich-formatting-menu/RichFormattingMenuExtension.tsx b/packages/slate-editor/src/extensions/rich-formatting-menu/RichFormattingMenuExtension.tsx new file mode 100644 index 000000000..093ec787b --- /dev/null +++ b/packages/slate-editor/src/extensions/rich-formatting-menu/RichFormattingMenuExtension.tsx @@ -0,0 +1,41 @@ +import { useRegisterExtension } from '@prezly/slate-commons'; +import React from 'react'; + +import type { Props as RichFormattingMenuProps } from './RichFormattingMenu'; +import { RichFormattingMenu } from './RichFormattingMenu'; + +export const EXTENSION_ID = 'RichFormattingMenuExtension'; + +export type Parameters = RichFormattingMenuProps; + +export function RichFormattingMenuExtension({ + availableWidth, + containerElement, + defaultAlignment, + withAlignment, + withBlockquotes, + withHeadings, + withInlineLinks, + withLists, + withNewTabOption, + withParagraphs, +}: Parameters) { + useRegisterExtension({ + id: EXTENSION_ID, + }); + + return ( + + ); +} diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/components/FormattingDropdown.module.scss b/packages/slate-editor/src/extensions/rich-formatting-menu/components/FormattingDropdown.module.scss similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/components/FormattingDropdown.module.scss rename to packages/slate-editor/src/extensions/rich-formatting-menu/components/FormattingDropdown.module.scss diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/components/FormattingDropdown.tsx b/packages/slate-editor/src/extensions/rich-formatting-menu/components/FormattingDropdown.tsx similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/components/FormattingDropdown.tsx rename to packages/slate-editor/src/extensions/rich-formatting-menu/components/FormattingDropdown.tsx diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/components/Toolbar.tsx b/packages/slate-editor/src/extensions/rich-formatting-menu/components/Toolbar.tsx similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/components/Toolbar.tsx rename to packages/slate-editor/src/extensions/rich-formatting-menu/components/Toolbar.tsx diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/components/index.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/components/index.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/components/index.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/components/index.ts diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/index.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/index.ts similarity index 51% rename from packages/slate-editor/src/modules/rich-formatting-menu/index.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/index.ts index 2c8f20050..d60db6306 100644 --- a/packages/slate-editor/src/modules/rich-formatting-menu/index.ts +++ b/packages/slate-editor/src/extensions/rich-formatting-menu/index.ts @@ -1,2 +1,3 @@ export { RichFormattingMenu } from './RichFormattingMenu'; +export { RichFormattingMenuExtension, EXTENSION_ID } from './RichFormattingMenuExtension'; export { toggleBlock } from './lib'; diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/convertLink.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/convertLink.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/convertLink.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/convertLink.ts diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/findRichFormattingTextParent.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/findRichFormattingTextParent.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/findRichFormattingTextParent.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/findRichFormattingTextParent.ts diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/getCurrentFormatting.test.tsx b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/getCurrentFormatting.test.tsx similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/getCurrentFormatting.test.tsx rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/getCurrentFormatting.test.tsx diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/getCurrentFormatting.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/getCurrentFormatting.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/getCurrentFormatting.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/getCurrentFormatting.ts diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/index.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/index.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/index.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/index.ts diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/isSelectionSupported.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/isSelectionSupported.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/isSelectionSupported.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/isSelectionSupported.ts diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/keepToolbarInTextColumn.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/keepToolbarInTextColumn.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/keepToolbarInTextColumn.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/keepToolbarInTextColumn.ts diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/restoreSelection.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/restoreSelection.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/restoreSelection.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/restoreSelection.ts diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/toggleBlock.test.tsx b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/toggleBlock.test.tsx similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/toggleBlock.test.tsx rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/toggleBlock.test.tsx diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/toggleBlock.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/toggleBlock.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/toggleBlock.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/toggleBlock.ts diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/useLinkCandidateElement.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/useLinkCandidateElement.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/useLinkCandidateElement.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/useLinkCandidateElement.ts diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/lib/useRangeRef.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/lib/useRangeRef.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/lib/useRangeRef.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/lib/useRangeRef.ts diff --git a/packages/slate-editor/src/modules/rich-formatting-menu/types.ts b/packages/slate-editor/src/extensions/rich-formatting-menu/types.ts similarity index 100% rename from packages/slate-editor/src/modules/rich-formatting-menu/types.ts rename to packages/slate-editor/src/extensions/rich-formatting-menu/types.ts diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 8a394d54a..59241e5d4 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -14,15 +14,7 @@ import { } from '@prezly/slate-types'; import { noop } from '@technically/lodash'; import classNames from 'classnames'; -import React, { - forwardRef, - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react'; +import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'; import type { Element } from 'slate'; import { Transforms } from 'slate'; import { ReactEditor, Slate } from 'slate-react'; @@ -31,15 +23,15 @@ import { useFunction, useGetSet, useSize } from '#lib'; import { insertButtonBlock } from '#extensions/button-block'; import { FlashNodesExtension } from '#extensions/flash-nodes'; -import { FloatingAddMenu, type Option } from '#extensions/floating-add-menu'; +import { FloatingAddMenuExtension, type Option } from '#extensions/floating-add-menu'; import { insertPlaceholder, PlaceholderNode } from '#extensions/placeholders'; +import { RichFormattingMenuExtension, toggleBlock } from '#extensions/rich-formatting-menu'; import { Placeholder } from '#modules/components'; import { DecorationsProvider } from '#modules/decorations'; import { EditableWithExtensions } from '#modules/editable'; import type { EditorEventMap } from '#modules/events'; import { EventsEditor } from '#modules/events'; import { PopperOptionsContext } from '#modules/popper-options-context'; -import { RichFormattingMenu, toggleBlock } from '#modules/rich-formatting-menu'; import styles from './Editor.module.scss'; import { Extensions } from './Extensions'; @@ -121,18 +113,6 @@ export const Editor = forwardRef((props, forwardedRef) = // const { onOperationEnd, onOperationStart } = usePendingOperation(onIsOperationPendingChange); // [+] menu - const [isFloatingAddMenuOpen, setFloatingAddMenuOpen] = useState(false); - const onFloatingAddMenuToggle = useCallback( - function (shouldOpen: boolean, trigger: 'click' | 'hotkey' | 'input') { - setFloatingAddMenuOpen(shouldOpen); - if (shouldOpen) { - EventsEditor.dispatchEvent(editor, 'add-button-menu-opened', { trigger }); - } else { - EventsEditor.dispatchEvent(editor, 'add-button-menu-closed'); - } - }, - [setFloatingAddMenuOpen], - ); const editor = useCreateEditor({ events, @@ -696,7 +676,7 @@ export const Editor = forwardRef((props, forwardedRef) = }); const hasCustomPlaceholder = - withFloatingAddMenu && (ReactEditor.isFocused(editor) || isFloatingAddMenuOpen); + withFloatingAddMenu && ReactEditor.isFocused(editor); /*|| isFloatingAddMenuOpen*/ // FIXME: Commented isFloatingAddMenuOpen const onChange = useOnChange((value) => { props.onChange(editor.serialize(value) as Value); @@ -747,7 +727,6 @@ export const Editor = forwardRef((props, forwardedRef) = ((props, forwardedRef) = withCustomNormalization={withCustomNormalization} withDivider={withDivider} withEmbeds={withEmbeds} - withFloatingAddMenu={withFloatingAddMenu} withGalleries={withGalleries} withHeadings={withHeadings} withImages={withImages} @@ -786,27 +764,23 @@ export const Editor = forwardRef((props, forwardedRef) = )} {withFloatingAddMenu && ( - tooltip={ typeof withFloatingAddMenu === 'object' ? withFloatingAddMenu.tooltip : undefined } - open={isFloatingAddMenuOpen} availableWidth={availableWidth} containerRef={containerRef} onActivate={handleMenuAction} onFilter={handleMenuFilter} - onToggle={(toggle) => - onFloatingAddMenuToggle(toggle, 'click') - } options={menuOptions} showTooltipByDefault={EditorCommands.isEmpty(editor)} /> )} {withRichFormattingMenu && ( - ; - onFloatingAddMenuToggle: (show: boolean, trigger: 'input' | 'hotkey') => void; } & Pick< Required, | 'withAllowedBlocks' @@ -71,7 +68,6 @@ type Props = { | 'withCustomNormalization' | 'withDivider' | 'withEmbeds' - | 'withFloatingAddMenu' | 'withGalleries' | 'withHeadings' | 'withImages' @@ -94,7 +90,6 @@ type Props = { export function Extensions({ availableWidth, containerRef, - onFloatingAddMenuToggle, withAllowedBlocks, withAttachments, withAutoformat, @@ -104,7 +99,6 @@ export function Extensions({ withCustomNormalization, withDivider, withEmbeds, - withFloatingAddMenu, withGalleries, withHeadings, withImages, @@ -161,12 +155,6 @@ export function Extensions({ {withDivider && } - {withFloatingAddMenu && ( - onFloatingAddMenuToggle(true, trigger)} - /> - )} - {withHeadings && } {withInlineContacts && } diff --git a/packages/slate-editor/src/modules/editor/autoformatRules.ts b/packages/slate-editor/src/modules/editor/autoformatRules.ts index cb854a8d2..e75b291b3 100644 --- a/packages/slate-editor/src/modules/editor/autoformatRules.ts +++ b/packages/slate-editor/src/modules/editor/autoformatRules.ts @@ -9,8 +9,8 @@ import { } from '@prezly/slate-types'; import type { AutoformatRule } from '#extensions/autoformat'; +import { toggleBlock } from '#extensions/rich-formatting-menu'; import { MarkType } from '#extensions/text-styling'; -import { toggleBlock } from '#modules/rich-formatting-menu'; export const COMPOSITE_CHARACTERS_RULES: AutoformatRule[] = [ { From 5b48268d6a5f67fa2575317c4b41ee69962a4b6d Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 00:55:34 +0300 Subject: [PATCH 19/54] [CARE-1802] Finish transitioning of SnippetsExtension to be self-sufficient UI component Related to 8d2a5e31bd0ce3eba819973f22f09f6ae8c9d16d  Conflicts:  packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx  packages/slate-editor/src/modules/editor/Extensions.tsx --- .../src/lib/useSavedSelection.ts | 43 +++++++------- .../extensions/snippet/SnippetsExtension.tsx | 58 ++++++++++++------- .../snippet/lib/useFloatingSnippetInput.ts | 40 +++++-------- .../src/modules/editor/Editor.tsx | 15 ++++- .../src/modules/editor/Extensions.tsx | 11 ---- 5 files changed, 86 insertions(+), 81 deletions(-) diff --git a/packages/slate-commons/src/lib/useSavedSelection.ts b/packages/slate-commons/src/lib/useSavedSelection.ts index a85ce27ee..3d0e7c0f9 100644 --- a/packages/slate-commons/src/lib/useSavedSelection.ts +++ b/packages/slate-commons/src/lib/useSavedSelection.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import type { BaseEditor, Editor } from 'slate'; import { ReactEditor } from 'slate-react'; @@ -6,28 +6,25 @@ import { saveSelection } from '../commands'; type SavedSelection = ReturnType; -interface Actions { - restore: (editor: ReactEditor & Editor, options?: { focus?: boolean }) => void; - save: (editor: Editor) => void; -} - -export function useSavedSelection(): Actions { +export function useSavedSelection() { const [savedSelection, setSavedSelection] = useState(null); - function restore(editor: ReactEditor & Editor, { focus = false } = {}) { - if (focus) { - ReactEditor.focus(editor); - } - - if (savedSelection) { - savedSelection.restore(editor); - setSavedSelection(null); - } - } - - function save(editor: BaseEditor) { - return setSavedSelection(saveSelection(editor)); - } - - return { restore, save }; + return useMemo( + () => ({ + restore(editor: ReactEditor & Editor, { focus = false } = {}) { + if (focus) { + ReactEditor.focus(editor); + } + + if (savedSelection) { + savedSelection.restore(editor); + setSavedSelection(null); + } + }, + save(editor: BaseEditor) { + return setSavedSelection(saveSelection(editor)); + }, + }), + [savedSelection], + ); } diff --git a/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx b/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx index b27b8a07b..85e9fb892 100644 --- a/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx +++ b/packages/slate-editor/src/extensions/snippet/SnippetsExtension.tsx @@ -1,6 +1,6 @@ import { useRegisterExtension } from '@prezly/slate-commons'; import type { RefObject } from 'react'; -import React from 'react'; +import React, { forwardRef, useImperativeHandle } from 'react'; import { FloatingSnippetInput } from './components'; import { useFloatingSnippetInput } from './lib'; @@ -13,24 +13,42 @@ export interface Parameters extends SnippetsExtensionConfiguration { containerRef: RefObject; } -export function SnippetsExtension({ availableWidth, containerRef, renderInput }: Parameters) { - const [{ isOpen }, { close, open, rootClose, submit }] = useFloatingSnippetInput(); - - useRegisterExtension({ - id: EXTENSION_ID, - }); - - return ( - <> - {isOpen && ( - { + const { isOpen, close, open, rootClose, submit } = useFloatingSnippetInput(); + + useImperativeHandle( + forwardedRef, + (): SnippetsExtension.Ref => ({ + open, + }), + [open], + ); + + useRegisterExtension({ + id: EXTENSION_ID, + }); + + return ( + <> + {isOpen && ( + renderInput({ onCreate: submit })} - /> - )} - - ); + /> + )} + + ); + }, +); + +SnippetsExtension.displayName = 'SnippetsExtension'; + +export namespace SnippetsExtension { + export interface Ref { + open(): void; + } } diff --git a/packages/slate-editor/src/extensions/snippet/lib/useFloatingSnippetInput.ts b/packages/slate-editor/src/extensions/snippet/lib/useFloatingSnippetInput.ts index 74d183755..80ac60fa0 100644 --- a/packages/slate-editor/src/extensions/snippet/lib/useFloatingSnippetInput.ts +++ b/packages/slate-editor/src/extensions/snippet/lib/useFloatingSnippetInput.ts @@ -1,39 +1,29 @@ import { EditorCommands, useSavedSelection } from '@prezly/slate-commons'; import type { DocumentNode } from '@prezly/slate-types'; -import { useState } from 'react'; -import type { Editor } from 'slate'; +import { useCallback, useState } from 'react'; +import { useSlateStatic } from 'slate-react'; import { EventsEditor } from '#modules/events'; -interface State { - isOpen: boolean; -} - -interface Actions { - close: () => void; - open: () => void; - rootClose: () => void; - submit: (node: DocumentNode) => Promise; -} - -export function useFloatingSnippetInput(editor: Editor): [State, Actions] { +export function useFloatingSnippetInput() { + const editor = useSlateStatic(); const [isOpen, setIsOpen] = useState(false); const savedSelection = useSavedSelection(); - function close() { + const open = useCallback(() => { + EventsEditor.dispatchEvent(editor, 'snippet-dialog-opened'); + setIsOpen(true); + savedSelection.save(editor); + }, [editor, savedSelection]); + + const close = useCallback(() => { savedSelection.restore(editor, { focus: true }); setIsOpen(false); - } + }, [editor, savedSelection]); - function rootClose() { + const rootClose = useCallback(() => { setIsOpen(false); - } - - function open() { - EventsEditor.dispatchEvent(editor, 'snippet-dialog-opened'); - setIsOpen(true); - savedSelection.save(editor); - } + }, []); async function submit(node: DocumentNode) { EventsEditor.dispatchEvent(editor, 'snippet-dialog-submitted'); @@ -58,5 +48,5 @@ export function useFloatingSnippetInput(editor: Editor): [State, Actions] { } } - return [{ isOpen }, { close, open, rootClose, submit }]; + return { isOpen, close, open, rootClose, submit }; } diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 59241e5d4..33dc9c5d3 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -26,6 +26,7 @@ import { FlashNodesExtension } from '#extensions/flash-nodes'; import { FloatingAddMenuExtension, type Option } from '#extensions/floating-add-menu'; import { insertPlaceholder, PlaceholderNode } from '#extensions/placeholders'; import { RichFormattingMenuExtension, toggleBlock } from '#extensions/rich-formatting-menu'; +import { SnippetsExtension } from '#extensions/snippet'; import { Placeholder } from '#modules/components'; import { DecorationsProvider } from '#modules/decorations'; import { EditableWithExtensions } from '#modules/editable'; @@ -224,6 +225,8 @@ export const Editor = forwardRef((props, forwardedRef) = }), ); + const snippetsExtension = useRef(); + const withSpecificProviderOptions = typeof withFloatingAddMenu === 'object' ? withFloatingAddMenu.withSpecificProviderOptions @@ -589,7 +592,7 @@ export const Editor = forwardRef((props, forwardedRef) = return; } if (action === MenuAction.ADD_SNIPPET) { - // return openFloatingSnippetInput(); // FIXME: Find a way to trigger snippet input + snippetsExtension.current?.open(); } if (action === MenuAction.ADD_GALLERY && withGalleries) { const placeholder = insertPlaceholder( @@ -744,7 +747,6 @@ export const Editor = forwardRef((props, forwardedRef) = withLists={withLists} withPlaceholders={withPlaceholders} withPressContacts={withPressContacts} - withSnippets={withSnippets} withStoryBookmarks={withStoryBookmarks} withStoryEmbeds={withStoryEmbeds} withTables={withTables} @@ -763,6 +765,15 @@ export const Editor = forwardRef((props, forwardedRef) = )} + {withSnippets && ( + + )} + {withFloatingAddMenu && ( tooltip={ diff --git a/packages/slate-editor/src/modules/editor/Extensions.tsx b/packages/slate-editor/src/modules/editor/Extensions.tsx index d856a3fbc..544caf10c 100644 --- a/packages/slate-editor/src/modules/editor/Extensions.tsx +++ b/packages/slate-editor/src/modules/editor/Extensions.tsx @@ -29,7 +29,6 @@ import { PasteImagesExtension } from '#extensions/paste-images'; import { PasteSlateContentExtension } from '#extensions/paste-slate-content'; import { PlaceholdersExtension } from '#extensions/placeholders'; import { PressContactsExtension } from '#extensions/press-contacts'; -import { SnippetsExtension } from '#extensions/snippet'; import { SoftBreakExtension } from '#extensions/soft-break'; import { StoryBookmarkExtension } from '#extensions/story-bookmark'; import { StoryEmbedExtension } from '#extensions/story-embed'; @@ -84,7 +83,6 @@ type Props = { | 'withWebBookmarks' | 'withStoryEmbeds' | 'withStoryBookmarks' - | 'withSnippets' >; export function Extensions({ @@ -107,7 +105,6 @@ export function Extensions({ withLists, withPlaceholders, withPressContacts, - withSnippets, withStoryBookmarks, withStoryEmbeds, withTables, @@ -331,14 +328,6 @@ export function Extensions({ withWebBookmarkPlaceholders={withWebBookmarks} /> - {withSnippets && ( - - )} - {withStoryEmbeds && } {withStoryBookmarks && } From 9e02f82d96b2800fbd4595af9a883ce0d01e174a Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 00:56:08 +0300 Subject: [PATCH 20/54] [CARE-1802] Leave comments for later  Conflicts:  packages/slate-editor/src/modules/editor/Editor.tsx --- packages/slate-commons/src/extensions/ExtensionManager.tsx | 4 +++- packages/slate-editor/src/modules/editor/Editor.tsx | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/slate-commons/src/extensions/ExtensionManager.tsx b/packages/slate-commons/src/extensions/ExtensionManager.tsx index 692f3672b..a4c6f4aa0 100644 --- a/packages/slate-commons/src/extensions/ExtensionManager.tsx +++ b/packages/slate-commons/src/extensions/ExtensionManager.tsx @@ -24,6 +24,8 @@ export const ManagerContext = createContext({ }, }); +// FIXME: Introduce ManagerSyncContext to only render the Editor itself after all sub-tree extensions are already mounted. + /** * -- HOOKS -- * =========== @@ -75,7 +77,7 @@ export function ExtensionsManager({ children, editor * Force editor re-rendering every time the extensions list is changed. */ useEffect(() => { - editor.onChange(); + editor.onChange(); // FIXME: Verify this works without causing an infinite update loop. }, [counter]); return {children}; diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 33dc9c5d3..82979d00d 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -113,8 +113,6 @@ export const Editor = forwardRef((props, forwardedRef) = // TODO: Wire `onOperationStart` and `onOperationEnd` to the Placeholder extension // const { onOperationEnd, onOperationStart } = usePendingOperation(onIsOperationPendingChange); - // [+] menu - const editor = useCreateEditor({ events, // extensions, // FIXME @@ -714,6 +712,7 @@ export const Editor = forwardRef((props, forwardedRef) = {(combinedDecorate) => ( <> + {/** FIXME: Sync this with Extensions mounting. See ExtensionsManager: ExtensionsManagerSync */} Date: Mon, 18 Sep 2023 14:45:56 +0300 Subject: [PATCH 21/54] [CARE-1802] Wire EditableWithExtensions to the editor extensions property --- packages/slate-commons/src/types/Extension.ts | 2 +- .../editable/EditableWithExtensions.tsx | 101 ++++++++---------- .../modules/editable/lib/combineDecorate.ts | 2 +- .../editable/lib/combineOnDOMBeforeInput.ts | 13 +-- .../modules/editable/lib/combineOnKeyDown.ts | 32 ++---- .../editable/lib/combineRenderElement.tsx | 22 ++-- .../editable/lib/combineRenderLeaf.tsx | 14 +-- .../lib/createExtensionsDecorators.ts | 10 -- .../src/modules/editable/lib/index.ts | 1 - .../src/modules/editor/Editor.tsx | 2 +- 10 files changed, 69 insertions(+), 130 deletions(-) delete mode 100644 packages/slate-editor/src/modules/editable/lib/createExtensionsDecorators.ts diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index 153fe3e24..3d40d8fab 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -25,7 +25,7 @@ export interface Extension { isVoid?: (node: Node) => boolean; normalizeNode?: Normalize | Normalize[]; onDOMBeforeInput?: OnDOMBeforeInput; - onKeyDown?: OnKeyDown | null; + onKeyDown?: OnKeyDown; renderElement?: RenderElement; renderLeaf?: RenderLeaf; serialize?: Serialize; diff --git a/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx b/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx index a18e00703..9650da758 100644 --- a/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx +++ b/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx @@ -1,18 +1,18 @@ /* eslint-disable react-hooks/exhaustive-deps */ import type { Decorate, - Extension, OnDOMBeforeInput, OnKeyDown, RenderElement, RenderLeaf, } from '@prezly/slate-commons'; +import { isNotUndefined } from '@technically/is-not-undefined'; import classNames from 'classnames'; import type { ReactNode } from 'react'; -import React, { useCallback, useMemo } from 'react'; +import React, { useMemo } from 'react'; import type { Editor } from 'slate'; import type { ReactEditor } from 'slate-react'; -import { Editable } from 'slate-react'; +import { Editable, useSlateSelector } from 'slate-react'; import { combineDecorate, @@ -20,7 +20,6 @@ import { combineOnKeyDown, combineRenderElement, combineRenderLeaf, - createExtensionsDecorators, } from './lib'; export interface Props { @@ -28,40 +27,13 @@ export interface Props { className?: string; decorate?: Decorate; editor: Editor & ReactEditor; - /** - * Each extension fields will be combined by role. - * - * To render `Editable`: - * - decorate - * - renderElement - * - renderLeaf - * - onDOMBeforeInput - * - onKeyDown.ts - */ - extensions?: Extension[]; onCut?: (event: React.ClipboardEvent) => void; - onDOMBeforeInput?: OnDOMBeforeInput[]; - // Dependencies of `onDOMBeforeInput` - onDOMBeforeInputDeps?: any[]; + onDOMBeforeInput?: OnDOMBeforeInput; onKeyDown?: OnKeyDown; placeholder?: string; readOnly?: boolean; - /** - * To customize the rendering of each element components. - * Element properties are for contiguous, semantic elements in the document. - */ - renderElement?: RenderElement[]; - // Dependencies of `renderElement` - renderElementDeps?: any[]; - /** - * To customize the rendering of each leaf. - * When text-level formatting is rendered, the characters are grouped into - * "leaves" of text that each contain the same formatting applied to them. - * Text properties are for non-contiguous, character-level formatting. - */ - renderLeaf?: RenderLeaf[]; - // Dependencies of `renderLeaf` - renderLeafDeps?: any[]; + renderElement?: RenderElement; + renderLeaf?: RenderLeaf; role?: string; style?: React.CSSProperties; } @@ -70,39 +42,50 @@ export function EditableWithExtensions({ className, decorate, editor, - onDOMBeforeInput: onDOMBeforeInputList = [], - onDOMBeforeInputDeps = [], + onDOMBeforeInput, onKeyDown, - renderElement: renderElementList = [], - renderElementDeps = [], - renderLeaf: renderLeafList = [], - renderLeafDeps = [], + renderElement, + renderLeaf, ...props }: Props) { + const extensions = useSlateSelector((editor) => editor.extensions); + const combinedDecorate: Decorate = useMemo( function () { - const decorateFns = createExtensionsDecorators(editor, extensions); - return combineDecorate(decorate ? [decorate, ...decorateFns] : decorateFns); + const decorateExtensions = extensions.map((extension) => extension.decorate?.(editor)); + return combineDecorate([decorate, ...decorateExtensions].filter(isNotUndefined)); }, - [decorate, editor, extensions], - ); - const combinedOnDOMBeforeInput = useCallback( - combineOnDOMBeforeInput(editor, extensions, onDOMBeforeInputList), - onDOMBeforeInputDeps, - ); - const combinedOnKeyDown = useCallback( - combineOnKeyDown(editor, extensions, onKeyDown ? [onKeyDown] : []), - [onKeyDown], - ); - const combinedRenderElement = useMemo( - () => combineRenderElement(editor, extensions, renderElementList), - renderElementDeps, - ); - const combinedRenderLeaf = useCallback( - combineRenderLeaf(extensions, renderLeafList), - renderLeafDeps, + [decorate, extensions], ); + const combinedOnDOMBeforeInput = useMemo(() => { + const onDOMBeforeInputExtensions = extensions.map( + (extension) => extension.onDOMBeforeInput, + ); + return combineOnDOMBeforeInput( + editor, + [onDOMBeforeInput, ...onDOMBeforeInputExtensions].filter(isNotUndefined), + ); + }, [onDOMBeforeInput, extensions]); + + const combinedOnKeyDown = useMemo(() => { + const onKeyDownExtensions = extensions.map((extension) => extension.onKeyDown); + return combineOnKeyDown(editor, [onKeyDown, ...onKeyDownExtensions].filter(isNotUndefined)); + }, [onKeyDown, extensions]); + + const combinedRenderElement = useMemo(() => { + const renderElementExtensions = extensions.map((extension) => extension.renderElement); + return combineRenderElement( + editor, + [renderElement, ...renderElementExtensions].filter(isNotUndefined), + ); + }, [renderElement, extensions]); + + const combinedRenderLeaf = useMemo(() => { + const renderLeafExtensions = extensions.map((extension) => extension.renderLeaf); + return combineRenderLeaf([renderLeaf, ...renderLeafExtensions].filter(isNotUndefined)); + }, [renderLeaf, extensions]); + return ( { return decorateFns.flatMap((decorate) => decorate(entry)); }; } diff --git a/packages/slate-editor/src/modules/editable/lib/combineOnDOMBeforeInput.ts b/packages/slate-editor/src/modules/editable/lib/combineOnDOMBeforeInput.ts index c933fd09c..9a114e6f0 100644 --- a/packages/slate-editor/src/modules/editable/lib/combineOnDOMBeforeInput.ts +++ b/packages/slate-editor/src/modules/editable/lib/combineOnDOMBeforeInput.ts @@ -1,18 +1,13 @@ -import type { Extension, OnDOMBeforeInput } from '@prezly/slate-commons'; +import type { OnDOMBeforeInput } from '@prezly/slate-commons'; import type { ReactEditor } from 'slate-react'; export function combineOnDOMBeforeInput( editor: ReactEditor, - extensions: Extension[], - onDOMBeforeInputList: OnDOMBeforeInput[], + onDOMBeforeInputFns: OnDOMBeforeInput[], ) { - return function (event: Event) { - onDOMBeforeInputList.forEach((onDOMBeforeInput) => { + return (event: Event) => { + onDOMBeforeInputFns.forEach((onDOMBeforeInput) => { onDOMBeforeInput(event, editor); }); - - extensions.forEach(({ onDOMBeforeInput }) => { - onDOMBeforeInput?.(event, editor); - }); }; } diff --git a/packages/slate-editor/src/modules/editable/lib/combineOnKeyDown.ts b/packages/slate-editor/src/modules/editable/lib/combineOnKeyDown.ts index 14a58dd2d..fdf9495c0 100644 --- a/packages/slate-editor/src/modules/editable/lib/combineOnKeyDown.ts +++ b/packages/slate-editor/src/modules/editable/lib/combineOnKeyDown.ts @@ -1,31 +1,15 @@ -import type { Extension, OnKeyDown } from '@prezly/slate-commons'; +import type { OnKeyDown } from '@prezly/slate-commons'; import type { KeyboardEvent } from 'react'; import type { Editor } from 'slate'; -export function combineOnKeyDown( - editor: Editor, - extensions: Extension[], - onKeyDownList: OnKeyDown[], -) { - return function (event: KeyboardEvent) { - let handled = false; - onKeyDownList.forEach((onKeyDown) => { - if (!handled) { - const ret = onKeyDown(event, editor); - handled = Boolean(ret); +export function combineOnKeyDown(editor: Editor, onKeyDownFns: OnKeyDown[]) { + return (event: KeyboardEvent) => { + for (const onKeyDown of onKeyDownFns) { + const handled = onKeyDown(event, editor); + if (handled) { + event.preventDefault(); + event.stopPropagation(); } - }); - - extensions.forEach(({ onKeyDown }) => { - if (!handled) { - const ret = onKeyDown?.(event, editor); - handled = Boolean(ret); - } - }); - - if (handled) { - event.preventDefault(); - event.stopPropagation(); } }; } diff --git a/packages/slate-editor/src/modules/editable/lib/combineRenderElement.tsx b/packages/slate-editor/src/modules/editable/lib/combineRenderElement.tsx index 50c0ad069..1981b875b 100644 --- a/packages/slate-editor/src/modules/editable/lib/combineRenderElement.tsx +++ b/packages/slate-editor/src/modules/editable/lib/combineRenderElement.tsx @@ -1,15 +1,11 @@ -import type { Extension, RenderElement } from '@prezly/slate-commons'; +import type { RenderElement } from '@prezly/slate-commons'; import React from 'react'; import type { Editor, Node } from 'slate'; import { Element } from 'slate'; import type { RenderElementProps } from 'slate-react'; -export function combineRenderElement( - editor: Editor, - extensions: Extension[], - renderElementList: RenderElement[], -) { - return function combinedRenderElement({ attributes, children, element }: RenderElementProps) { +export function combineRenderElement(editor: Editor, renderElementFns: RenderElement[]) { + return function combined({ attributes, children, element }: RenderElementProps) { const props = { attributes: { 'data-slate-block': detectBlockType(editor, element), @@ -20,14 +16,12 @@ export function combineRenderElement( children, element, }; - for (const renderElement of renderElementList) { - const ret = renderElement(props); - if (ret) return ret; - } - for (const { renderElement } of extensions) { - const ret = renderElement?.(props); - if (ret) return ret; + for (const renderElement of renderElementFns) { + const ret = renderElement(props); + if (typeof ret !== 'undefined') { + return ret; + } } return
{props.children}
; diff --git a/packages/slate-editor/src/modules/editable/lib/combineRenderLeaf.tsx b/packages/slate-editor/src/modules/editable/lib/combineRenderLeaf.tsx index c6a35b70a..79dd2cd8d 100644 --- a/packages/slate-editor/src/modules/editable/lib/combineRenderLeaf.tsx +++ b/packages/slate-editor/src/modules/editable/lib/combineRenderLeaf.tsx @@ -1,19 +1,13 @@ -import type { Extension, RenderLeaf } from '@prezly/slate-commons'; +import type { RenderLeaf } from '@prezly/slate-commons'; import React from 'react'; import type { RenderLeafProps } from 'slate-react'; -export function combineRenderLeaf(extensions: Extension[], renderLeafList: RenderLeaf[]) { - return function RenderLeaf({ attributes, children, leaf, text }: RenderLeafProps) { - for (const renderLeaf of renderLeafList) { +export function combineRenderLeaf(renderLeafFns: RenderLeaf[]) { + return function combined({ attributes, children, leaf, text }: RenderLeafProps) { + for (const renderLeaf of renderLeafFns) { children = renderLeaf({ attributes, children, leaf, text }); } - for (const { renderLeaf } of extensions) { - if (renderLeaf) { - children = renderLeaf({ attributes, children, leaf, text }); - } - } - return {children}; }; } diff --git a/packages/slate-editor/src/modules/editable/lib/createExtensionsDecorators.ts b/packages/slate-editor/src/modules/editable/lib/createExtensionsDecorators.ts deleted file mode 100644 index 5a01e4271..000000000 --- a/packages/slate-editor/src/modules/editable/lib/createExtensionsDecorators.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Decorate, Extension } from '@prezly/slate-commons'; -import { isNotUndefined } from '@technically/is-not-undefined'; -import type { Editor } from 'slate'; - -export function createExtensionsDecorators( - editor: E, - extensions: Extension[], -): Decorate[] { - return extensions.map((extension) => extension.decorate?.(editor)).filter(isNotUndefined); -} diff --git a/packages/slate-editor/src/modules/editable/lib/index.ts b/packages/slate-editor/src/modules/editable/lib/index.ts index f9e512b90..29fafa70f 100644 --- a/packages/slate-editor/src/modules/editable/lib/index.ts +++ b/packages/slate-editor/src/modules/editable/lib/index.ts @@ -3,4 +3,3 @@ export { combineOnDOMBeforeInput } from './combineOnDOMBeforeInput'; export { combineOnKeyDown } from './combineOnKeyDown'; export { combineRenderElement } from './combineRenderElement'; export { combineRenderLeaf } from './combineRenderLeaf'; -export { createExtensionsDecorators } from './createExtensionsDecorators'; diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 82979d00d..03cc6894e 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -712,6 +712,7 @@ export const Editor = forwardRef((props, forwardedRef) = {(combinedDecorate) => ( <> + {/** FIXME: Sync this with Extensions mounting. See ExtensionsManager: ExtensionsManagerSync */} ((props, forwardedRef) = onCut={createOnCut(editor)} onKeyDown={onKeyDown} readOnly={readOnly} - renderElementDeps={[availableWidth]} style={contentStyle} /> From 660332fecb531fb060f2c88edaa237d5829385c7 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 14:51:00 +0300 Subject: [PATCH 22/54] [CARE-1802] Move `editor.serialization()` functionality to the ExtensionsEditor definition --- .../src/extensions/ExtensionsEditor.ts | 19 +++++++++++++++++-- packages/slate-editor/src/index.ts | 2 -- .../src/modules/editor/createEditor.ts | 2 -- .../slate-editor/src/modules/editor/index.ts | 1 - .../src/modules/editor/plugins/index.ts | 1 - .../editor/plugins/withSerialization.ts | 19 ------------------- 6 files changed, 17 insertions(+), 27 deletions(-) delete mode 100644 packages/slate-editor/src/modules/editor/plugins/withSerialization.ts diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index 037dd6034..076556641 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -1,13 +1,28 @@ -import type { BaseEditor } from 'slate'; +import type { BaseEditor, Descendant } from 'slate'; import type { Extension } from '../types'; export interface ExtensionsEditor extends BaseEditor { extensions: Extension[]; + + /** + * Convert internal editor document value for external consumers. + * This is a convenient location for removing runtime-only editor elements + * to prevent the outer systems from persisting temporary data. + */ + serialize(nodes: Descendant[]): Descendant[]; } export function withExtensions(editor: T): T & ExtensionsEditor { - return Object.assign(editor, { + const extensionsEditor: T & ExtensionsEditor = Object.assign(editor, { extensions: [], + serialize(nodes: Descendant[]) { + return extensionsEditor.extensions.reduce( + (result, extension) => extension.serialize?.(result) ?? result, + nodes, + ); + }, }); + + return extensionsEditor; } diff --git a/packages/slate-editor/src/index.ts b/packages/slate-editor/src/index.ts index b1e11f91d..1fd36c3f7 100644 --- a/packages/slate-editor/src/index.ts +++ b/packages/slate-editor/src/index.ts @@ -29,7 +29,6 @@ import type { DefaultTextBlockEditor, ElementsEqualityCheckEditor, RichBlocksAwareEditor, - SerializingEditor, } from '#modules/editor'; type Editor = BaseEditor & @@ -39,7 +38,6 @@ type Editor = BaseEditor & DefaultTextBlockEditor & ElementsEqualityCheckEditor & RichBlocksAwareEditor & - SerializingEditor & FlashEditor; declare module 'slate' { diff --git a/packages/slate-editor/src/modules/editor/createEditor.ts b/packages/slate-editor/src/modules/editor/createEditor.ts index 0ee55dfcf..5ce1f4185 100644 --- a/packages/slate-editor/src/modules/editor/createEditor.ts +++ b/packages/slate-editor/src/modules/editor/createEditor.ts @@ -21,7 +21,6 @@ import { withDeserializeHtml, withElementsEqualityCheck, withRichBlocks, - withSerialization, } from './plugins'; export function createEditor( @@ -46,7 +45,6 @@ export function createEditor( withDeserializeHtml(getExtensions), withRichBlocks(getExtensions), withElementsEqualityCheck(getExtensions), - withSerialization(getExtensions), ...overrides, ...plugins, ])(baseEditor); diff --git a/packages/slate-editor/src/modules/editor/index.ts b/packages/slate-editor/src/modules/editor/index.ts index e06c5f310..6cd2fb34d 100644 --- a/packages/slate-editor/src/modules/editor/index.ts +++ b/packages/slate-editor/src/modules/editor/index.ts @@ -5,7 +5,6 @@ export type { DefaultTextBlockEditor, ElementsEqualityCheckEditor, RichBlocksAwareEditor, - SerializingEditor, } from './plugins'; export type { EditorRef, EditorProps, Value } from './types'; export { useEditorEvents } from './useEditorEvents'; diff --git a/packages/slate-editor/src/modules/editor/plugins/index.ts b/packages/slate-editor/src/modules/editor/plugins/index.ts index 0f8197aec..4acb31d67 100644 --- a/packages/slate-editor/src/modules/editor/plugins/index.ts +++ b/packages/slate-editor/src/modules/editor/plugins/index.ts @@ -5,4 +5,3 @@ export { withElementsEqualityCheck, } from './withElementsEqualityCheck'; export { type RichBlocksAwareEditor, withRichBlocks } from './withRichBlocks'; -export { type SerializingEditor, withSerialization } from './withSerialization'; diff --git a/packages/slate-editor/src/modules/editor/plugins/withSerialization.ts b/packages/slate-editor/src/modules/editor/plugins/withSerialization.ts deleted file mode 100644 index efb41125b..000000000 --- a/packages/slate-editor/src/modules/editor/plugins/withSerialization.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Extension } from '@prezly/slate-commons'; -import type { BaseEditor, Descendant } from 'slate'; - -export interface SerializingEditor extends BaseEditor { - serialize(nodes: Descendant[]): Descendant[]; -} - -export function withSerialization(getExtensions: () => Extension[]) { - return function (editor: T): T & SerializingEditor { - return Object.assign(editor, { - serialize(nodes: Descendant[]) { - return getExtensions().reduce( - (result, extension) => extension.serialize?.(result) ?? result, - nodes, - ); - }, - }); - }; -} From 6b36e1b42679db959dac2f9875d5ce008a22f178 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 14:55:31 +0300 Subject: [PATCH 23/54] [CARE-1802] Move `editor.isElementEqual()` functionality to the ExtensionsEditor definition --- .../src/extensions/ExtensionsEditor.ts | 23 ++++++++++++- packages/slate-commons/src/types/Extension.ts | 12 +++---- packages/slate-editor/src/index.ts | 7 +--- .../src/modules/editor/createEditor.ts | 2 -- .../slate-editor/src/modules/editor/index.ts | 6 +--- .../src/modules/editor/plugins/index.ts | 4 --- .../plugins/withElementsEqualityCheck.ts | 34 ------------------- 7 files changed, 30 insertions(+), 58 deletions(-) delete mode 100644 packages/slate-editor/src/modules/editor/plugins/withElementsEqualityCheck.ts diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index 076556641..9f9e8eb2b 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -1,10 +1,22 @@ -import type { BaseEditor, Descendant } from 'slate'; +import type { BaseEditor, Descendant, Element } from 'slate'; +import { node } from 'slate'; import type { Extension } from '../types'; export interface ExtensionsEditor extends BaseEditor { extensions: Extension[]; + /** + * Compare two elements. + * + * This is useful to implement smarter comparison rules to, + * for example, ignore data-independent properties like `uuid`. + * + * `children` arrays can be omitted from the comparison, + * as the outer code will compare them anyway. + */ + isElementEqual(node: Element, another: Element): boolean | undefined; + /** * Convert internal editor document value for external consumers. * This is a convenient location for removing runtime-only editor elements @@ -16,6 +28,15 @@ export interface ExtensionsEditor extends BaseEditor { export function withExtensions(editor: T): T & ExtensionsEditor { const extensionsEditor: T & ExtensionsEditor = Object.assign(editor, { extensions: [], + isElementEqual(node: Element, another: Element): boolean | undefined { + for (const extension of extensionsEditor.extensions) { + const ret = extension.isElementEqual?.(node, another); + if (typeof ret !== 'undefined') { + return ret; + } + } + return undefined; + }, serialize(nodes: Descendant[]) { return extensionsEditor.extensions.reduce( (result, extension) => extension.serialize?.(result) ?? result, diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index 3d40d8fab..892d253fb 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -12,7 +12,7 @@ import type { WithOverrides } from './WithOverrides'; export interface Extension { id: string; - decorate?: DecorateFactory; + decorate?: DecorateFactory; // OK deserialize?: DeserializeHtml; /** * Compare two elements. @@ -24,10 +24,10 @@ export interface Extension { isRichBlock?: (node: Node) => boolean; isVoid?: (node: Node) => boolean; normalizeNode?: Normalize | Normalize[]; - onDOMBeforeInput?: OnDOMBeforeInput; - onKeyDown?: OnKeyDown; - renderElement?: RenderElement; - renderLeaf?: RenderLeaf; - serialize?: Serialize; + onDOMBeforeInput?: OnDOMBeforeInput; // OK + onKeyDown?: OnKeyDown; // OK + renderElement?: RenderElement; // OK + renderLeaf?: RenderLeaf; // OK + serialize?: Serialize; // OK withOverrides?: WithOverrides; } diff --git a/packages/slate-editor/src/index.ts b/packages/slate-editor/src/index.ts index 1fd36c3f7..ae049aca6 100644 --- a/packages/slate-editor/src/index.ts +++ b/packages/slate-editor/src/index.ts @@ -25,18 +25,13 @@ import type { HistoryEditor } from 'slate-history'; import type { ReactEditor } from 'slate-react'; import type { FlashEditor } from '#extensions/flash-nodes'; -import type { - DefaultTextBlockEditor, - ElementsEqualityCheckEditor, - RichBlocksAwareEditor, -} from '#modules/editor'; +import type { DefaultTextBlockEditor, RichBlocksAwareEditor } from '#modules/editor'; type Editor = BaseEditor & ReactEditor & HistoryEditor & ExtensionsEditor & DefaultTextBlockEditor & - ElementsEqualityCheckEditor & RichBlocksAwareEditor & FlashEditor; diff --git a/packages/slate-editor/src/modules/editor/createEditor.ts b/packages/slate-editor/src/modules/editor/createEditor.ts index 5ce1f4185..1b1595fcf 100644 --- a/packages/slate-editor/src/modules/editor/createEditor.ts +++ b/packages/slate-editor/src/modules/editor/createEditor.ts @@ -19,7 +19,6 @@ import { withNodesHierarchy, hierarchySchema } from '#modules/nodes-hierarchy'; import { withDefaultTextBlock, withDeserializeHtml, - withElementsEqualityCheck, withRichBlocks, } from './plugins'; @@ -44,7 +43,6 @@ export function createEditor( withUserFriendlyDeleteBehavior, withDeserializeHtml(getExtensions), withRichBlocks(getExtensions), - withElementsEqualityCheck(getExtensions), ...overrides, ...plugins, ])(baseEditor); diff --git a/packages/slate-editor/src/modules/editor/index.ts b/packages/slate-editor/src/modules/editor/index.ts index 6cd2fb34d..0767fbc88 100644 --- a/packages/slate-editor/src/modules/editor/index.ts +++ b/packages/slate-editor/src/modules/editor/index.ts @@ -1,10 +1,6 @@ export { Editor } from './Editor'; export { createEditor } from './createEditor'; export { createEmptyValue } from './lib'; -export type { - DefaultTextBlockEditor, - ElementsEqualityCheckEditor, - RichBlocksAwareEditor, -} from './plugins'; +export type { DefaultTextBlockEditor, RichBlocksAwareEditor } from './plugins'; export type { EditorRef, EditorProps, Value } from './types'; export { useEditorEvents } from './useEditorEvents'; diff --git a/packages/slate-editor/src/modules/editor/plugins/index.ts b/packages/slate-editor/src/modules/editor/plugins/index.ts index 4acb31d67..4a014abbf 100644 --- a/packages/slate-editor/src/modules/editor/plugins/index.ts +++ b/packages/slate-editor/src/modules/editor/plugins/index.ts @@ -1,7 +1,3 @@ export { type DefaultTextBlockEditor, withDefaultTextBlock } from './withDefaultTextBlock'; export { withDeserializeHtml } from './withDeserializeHtml'; -export { - type ElementsEqualityCheckEditor, - withElementsEqualityCheck, -} from './withElementsEqualityCheck'; export { type RichBlocksAwareEditor, withRichBlocks } from './withRichBlocks'; diff --git a/packages/slate-editor/src/modules/editor/plugins/withElementsEqualityCheck.ts b/packages/slate-editor/src/modules/editor/plugins/withElementsEqualityCheck.ts deleted file mode 100644 index 8ce83a379..000000000 --- a/packages/slate-editor/src/modules/editor/plugins/withElementsEqualityCheck.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Extension } from '@prezly/slate-commons'; -import type { Element } from 'slate'; -import type { BaseEditor } from 'slate'; - -export interface ElementsEqualityCheckEditor extends BaseEditor { - /** - * Compare two elements. - * - * This is useful to implement smarter comparison rules to, - * for example, ignore data-independent properties like `uuid`. - * - * `children` arrays can be omitted from the comparison, - * as the outer code will compare them anyway. - */ - isElementEqual(node: Element, another: Element): boolean | undefined; -} - -export function withElementsEqualityCheck(getExtensions: () => Extension[]) { - return function (editor: T): T & ElementsEqualityCheckEditor { - function isElementEqual(node: Element, another: Element): boolean | undefined { - for (const extension of getExtensions()) { - const ret = extension.isElementEqual?.(node, another); - if (typeof ret !== 'undefined') { - return ret; - } - } - return undefined; - } - - return Object.assign(editor, { - isElementEqual, - }); - }; -} From 9754158c35968cc024850d4566a3e52569a7ed56 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 15:30:22 +0300 Subject: [PATCH 24/54] [CARE-1802] Move `editor.isInline()` & `editor.isVoid()` extensions overrides to the ExtensionsEditor definition --- .../src/extensions/ExtensionsEditor.ts | 31 +++++++++-- packages/slate-commons/src/plugins/index.ts | 1 - .../src/plugins/withInlineVoid.ts | 32 ------------ packages/slate-commons/src/types/Extension.ts | 4 +- .../src/extensions/mentions/test-utils.tsx | 52 +++++++++---------- .../src/modules/editor/createEditor.ts | 2 - 6 files changed, 55 insertions(+), 67 deletions(-) delete mode 100644 packages/slate-commons/src/plugins/withInlineVoid.ts diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index 9f9e8eb2b..3237b5d86 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -1,5 +1,5 @@ +import type { ElementNode } from '@prezly/slate-types'; import type { BaseEditor, Descendant, Element } from 'slate'; -import { node } from 'slate'; import type { Extension } from '../types'; @@ -25,9 +25,16 @@ export interface ExtensionsEditor extends BaseEditor { serialize(nodes: Descendant[]): Descendant[]; } -export function withExtensions(editor: T): T & ExtensionsEditor { +export function withExtensions( + editor: T, + extensions: Extension[] = [], +): T & ExtensionsEditor { + const parent = { + isInline: editor.isInline, + isVoid: editor.isVoid, + }; const extensionsEditor: T & ExtensionsEditor = Object.assign(editor, { - extensions: [], + extensions, isElementEqual(node: Element, another: Element): boolean | undefined { for (const extension of extensionsEditor.extensions) { const ret = extension.isElementEqual?.(node, another); @@ -37,6 +44,24 @@ export function withExtensions(editor: T): T & ExtensionsE } return undefined; }, + isInline(element: ElementNode) { + for (const extension of extensionsEditor.extensions) { + if (extension.isInline?.(element)) { + return true; + } + } + + return parent.isInline(element); + }, + isVoid(element: ElementNode) { + for (const extension of extensionsEditor.extensions) { + if (extension.isVoid?.(element)) { + return true; + } + } + + return parent.isVoid(element); + }, serialize(nodes: Descendant[]) { return extensionsEditor.extensions.reduce( (result, extension) => extension.serialize?.(result) ?? result, diff --git a/packages/slate-commons/src/plugins/index.ts b/packages/slate-commons/src/plugins/index.ts index 19ac2ba93..661a70a25 100644 --- a/packages/slate-commons/src/plugins/index.ts +++ b/packages/slate-commons/src/plugins/index.ts @@ -1,5 +1,4 @@ export { withBreaksOnExpandedSelection } from './withBreaksOnExpandedSelection'; export { withBreaksOnVoidNodes } from './withBreaksOnVoidNodes'; -export { withInlineVoid } from './withInlineVoid'; export { withNormalization } from './withNormalization'; export { withUserFriendlyDeleteBehavior } from './withUserFriendlyDeleteBehavior'; diff --git a/packages/slate-commons/src/plugins/withInlineVoid.ts b/packages/slate-commons/src/plugins/withInlineVoid.ts deleted file mode 100644 index a8f5810ac..000000000 --- a/packages/slate-commons/src/plugins/withInlineVoid.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-disable no-param-reassign */ -import type { Editor } from 'slate'; - -import type { Extension } from '../types'; - -export function withInlineVoid(getExtensions: () => Extension[]) { - return function (editor: T) { - const { isInline, isVoid } = editor; - - editor.isInline = (element) => { - for (const extension of getExtensions()) { - if (extension.isInline?.(element)) { - return true; - } - } - - return isInline(element); - }; - - editor.isVoid = (element) => { - for (const extension of getExtensions()) { - if (extension.isVoid?.(element)) { - return true; - } - } - - return isVoid(element); - }; - - return editor; - }; -} diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index 892d253fb..09490e401 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -20,9 +20,9 @@ export interface Extension { * as the outer code will compare them anyway. */ isElementEqual?: (node: Element, another: Element) => boolean | undefined; - isInline?: (node: Node) => boolean; + isInline?: (node: Node) => boolean; // OK isRichBlock?: (node: Node) => boolean; - isVoid?: (node: Node) => boolean; + isVoid?: (node: Node) => boolean; // OK normalizeNode?: Normalize | Normalize[]; onDOMBeforeInput?: OnDOMBeforeInput; // OK onKeyDown?: OnKeyDown; // OK diff --git a/packages/slate-editor/src/extensions/mentions/test-utils.tsx b/packages/slate-editor/src/extensions/mentions/test-utils.tsx index 2bc967da5..737ab8c39 100644 --- a/packages/slate-editor/src/extensions/mentions/test-utils.tsx +++ b/packages/slate-editor/src/extensions/mentions/test-utils.tsx @@ -1,33 +1,30 @@ -import { withInlineVoid } from '@prezly/slate-commons'; +import { withExtensions } from '@prezly/slate-commons'; import type { VariableNode } from '@prezly/slate-types'; -import { isVariableNode, VARIABLE_NODE_TYPE } from '@prezly/slate-types'; -import React from 'react'; +import { VARIABLE_NODE_TYPE } from '@prezly/slate-types'; import type { Editor } from 'slate'; -import type { RenderElementProps } from 'slate-react'; -import { MentionElement } from './components'; -import { MentionsExtension } from './MentionsExtension'; +// import { MentionsExtension } from './MentionsExtension'; -const PlaceholderMentionsExtension = () => - MentionsExtension({ - id: 'MentionsExtension', - parseSerializedElement: JSON.parse, - renderElement: ({ attributes, children, element }: RenderElementProps) => { - if (isVariableNode(element)) { - return ( - - {`%${element.key}%`} - {children} - - ); - } - - return undefined; - }, - type: VARIABLE_NODE_TYPE, - }); - -const getExtensions = () => [PlaceholderMentionsExtension()]; +const PlaceholderMentionsExtension = { + id: 'MentionsExtension', +}; +// MentionsExtension({ +// id: 'MentionsExtension', +// parseSerializedElement: JSON.parse, +// renderElement: ({ attributes, children, element }: RenderElementProps) => { +// if (isVariableNode(element)) { +// return ( +// +// {`%${element.key}%`} +// {children} +// +// ); +// } +// +// return undefined; +// }, +// type: VARIABLE_NODE_TYPE, +// }); export function createPlaceholderMentionElement(key: VariableNode['key']): VariableNode { return { @@ -38,5 +35,6 @@ export function createPlaceholderMentionElement(key: VariableNode['key']): Varia } export function createMentionsEditor(editor: Editor) { - return withInlineVoid(getExtensions)(editor); + // FIXME: Enable PlaceholderMentionsExtension extension for the test + return withExtensions(editor, [PlaceholderMentionsExtension]); } diff --git a/packages/slate-editor/src/modules/editor/createEditor.ts b/packages/slate-editor/src/modules/editor/createEditor.ts index 1b1595fcf..dadfc4251 100644 --- a/packages/slate-editor/src/modules/editor/createEditor.ts +++ b/packages/slate-editor/src/modules/editor/createEditor.ts @@ -1,7 +1,6 @@ import { withBreaksOnExpandedSelection, withBreaksOnVoidNodes, - withInlineVoid, withNormalization, withUserFriendlyDeleteBehavior, } from '@prezly/slate-commons'; @@ -38,7 +37,6 @@ export function createEditor( withBreaksOnExpandedSelection, withBreaksOnVoidNodes, withDefaultTextBlock(createParagraph), - withInlineVoid(getExtensions), withNormalization(getExtensions), withUserFriendlyDeleteBehavior, withDeserializeHtml(getExtensions), From bc19448616b0b1557543e0d599501325ccace2e9 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 15:37:55 +0300 Subject: [PATCH 25/54] [CARE-1802] Move `editor.normalizeNode()` extensions overrides to the ExtensionsEditor definition --- .../src/extensions/ExtensionsEditor.ts | 27 ++++++++++++++----- packages/slate-commons/src/plugins/index.ts | 1 - .../src/plugins/withNormalization.ts | 26 ------------------ packages/slate-commons/src/types/Extension.ts | 2 +- .../src/extensions/image/withImages.test.tsx | 9 ++++--- .../src/extensions/paragraphs/test-utils.ts | 6 +++-- .../src/modules/editor/createEditor.ts | 2 -- 7 files changed, 32 insertions(+), 41 deletions(-) delete mode 100644 packages/slate-commons/src/plugins/withNormalization.ts diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index 3237b5d86..9eb145cce 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -1,4 +1,3 @@ -import type { ElementNode } from '@prezly/slate-types'; import type { BaseEditor, Descendant, Element } from 'slate'; import type { Extension } from '../types'; @@ -32,10 +31,11 @@ export function withExtensions( const parent = { isInline: editor.isInline, isVoid: editor.isVoid, + normalizeNode: editor.normalizeNode, }; const extensionsEditor: T & ExtensionsEditor = Object.assign(editor, { extensions, - isElementEqual(node: Element, another: Element): boolean | undefined { + isElementEqual(node, another): boolean | undefined { for (const extension of extensionsEditor.extensions) { const ret = extension.isElementEqual?.(node, another); if (typeof ret !== 'undefined') { @@ -44,7 +44,7 @@ export function withExtensions( } return undefined; }, - isInline(element: ElementNode) { + isInline(element) { for (const extension of extensionsEditor.extensions) { if (extension.isInline?.(element)) { return true; @@ -53,7 +53,7 @@ export function withExtensions( return parent.isInline(element); }, - isVoid(element: ElementNode) { + isVoid(element) { for (const extension of extensionsEditor.extensions) { if (extension.isVoid?.(element)) { return true; @@ -62,13 +62,28 @@ export function withExtensions( return parent.isVoid(element); }, - serialize(nodes: Descendant[]) { + normalizeNode(entry) { + const normalizers = extensionsEditor.extensions.flatMap( + (ext) => ext.normalizeNode ?? [], + ); + + for (const normalizer of normalizers) { + const normalized = normalizer(editor, entry); + + if (normalized) { + return; + } + } + + return parent.normalizeNode(entry); + }, + serialize(nodes) { return extensionsEditor.extensions.reduce( (result, extension) => extension.serialize?.(result) ?? result, nodes, ); }, - }); + } satisfies Partial); return extensionsEditor; } diff --git a/packages/slate-commons/src/plugins/index.ts b/packages/slate-commons/src/plugins/index.ts index 661a70a25..9c6092fb2 100644 --- a/packages/slate-commons/src/plugins/index.ts +++ b/packages/slate-commons/src/plugins/index.ts @@ -1,4 +1,3 @@ export { withBreaksOnExpandedSelection } from './withBreaksOnExpandedSelection'; export { withBreaksOnVoidNodes } from './withBreaksOnVoidNodes'; -export { withNormalization } from './withNormalization'; export { withUserFriendlyDeleteBehavior } from './withUserFriendlyDeleteBehavior'; diff --git a/packages/slate-commons/src/plugins/withNormalization.ts b/packages/slate-commons/src/plugins/withNormalization.ts deleted file mode 100644 index f96af5b70..000000000 --- a/packages/slate-commons/src/plugins/withNormalization.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable no-param-reassign */ -import type { Editor } from 'slate'; - -import type { Extension } from '../types'; - -export function withNormalization(getExtensions: () => Extension[]) { - return function (editor: T) { - const { normalizeNode } = editor; - - editor.normalizeNode = (entry) => { - const normalizers = getExtensions().flatMap(({ normalizeNode = [] }) => normalizeNode); - - for (const normalizer of normalizers) { - const normalized = normalizer(editor, entry); - - if (normalized) { - return; - } - } - - normalizeNode(entry); - }; - - return editor; - }; -} diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index 09490e401..3bf788a3a 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -23,7 +23,7 @@ export interface Extension { isInline?: (node: Node) => boolean; // OK isRichBlock?: (node: Node) => boolean; isVoid?: (node: Node) => boolean; // OK - normalizeNode?: Normalize | Normalize[]; + normalizeNode?: Normalize | Normalize[]; // OK onDOMBeforeInput?: OnDOMBeforeInput; // OK onKeyDown?: OnKeyDown; // OK renderElement?: RenderElement; // OK diff --git a/packages/slate-editor/src/extensions/image/withImages.test.tsx b/packages/slate-editor/src/extensions/image/withImages.test.tsx index 4b7b4c96c..d2eb9eed4 100644 --- a/packages/slate-editor/src/extensions/image/withImages.test.tsx +++ b/packages/slate-editor/src/extensions/image/withImages.test.tsx @@ -1,6 +1,6 @@ /** @jsx hyperscript */ -import { withNormalization } from '@prezly/slate-commons'; +import { withExtensions } from '@prezly/slate-commons'; import { ImageLayout } from '@prezly/slate-types'; import type { UploadedFile } from '@prezly/uploadcare'; import { Editor } from 'slate'; @@ -25,8 +25,11 @@ const getExtensions = () => [ }), ]; -const createEditor = (editor: JSX.Element): Editor => - withNormalization(getExtensions)(withImages(withReact(editor as unknown as Editor))); +const createEditor = (editor: JSX.Element): Editor => { + // FIXME: Enable ImageExtension for the test + console.log(getExtensions()); // FIXME: Remove this + return withExtensions(withImages(withReact(editor as unknown as Editor))); +}; describe('withImages - normalizeChildren', () => { it('unwraps deeply nested text objects', () => { diff --git a/packages/slate-editor/src/extensions/paragraphs/test-utils.ts b/packages/slate-editor/src/extensions/paragraphs/test-utils.ts index b368de48b..bddf0f037 100644 --- a/packages/slate-editor/src/extensions/paragraphs/test-utils.ts +++ b/packages/slate-editor/src/extensions/paragraphs/test-utils.ts @@ -1,4 +1,4 @@ -import { withNormalization } from '@prezly/slate-commons'; +import { withExtensions } from '@prezly/slate-commons'; import type { Editor } from 'slate'; import { ParagraphsExtension } from './ParagraphsExtension'; @@ -8,5 +8,7 @@ function getExtensions() { } export function createParagraphsEditor(input: JSX.Element) { - return withNormalization(getExtensions)(input as unknown as Editor); + // FIXME: Enable ParagraphsExtension for the test + console.log(getExtensions()); // FIXME: Remove this + return withExtensions(input as unknown as Editor); } diff --git a/packages/slate-editor/src/modules/editor/createEditor.ts b/packages/slate-editor/src/modules/editor/createEditor.ts index dadfc4251..913eb0c14 100644 --- a/packages/slate-editor/src/modules/editor/createEditor.ts +++ b/packages/slate-editor/src/modules/editor/createEditor.ts @@ -1,7 +1,6 @@ import { withBreaksOnExpandedSelection, withBreaksOnVoidNodes, - withNormalization, withUserFriendlyDeleteBehavior, } from '@prezly/slate-commons'; import type { Extension } from '@prezly/slate-commons'; @@ -37,7 +36,6 @@ export function createEditor( withBreaksOnExpandedSelection, withBreaksOnVoidNodes, withDefaultTextBlock(createParagraph), - withNormalization(getExtensions), withUserFriendlyDeleteBehavior, withDeserializeHtml(getExtensions), withRichBlocks(getExtensions), From 77ee3604d68eac5f540b3eb0f1550f080f3f973b Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 15:42:01 +0300 Subject: [PATCH 26/54] [CARE-1802] Move `editor.isRichBlock()` extensions checks to the ExtensionsEditor definition --- .../src/extensions/ExtensionsEditor.ts | 17 ++++++++++++++- packages/slate-commons/src/types/Extension.ts | 2 +- packages/slate-editor/src/index.ts | 3 +-- .../src/modules/editor/createEditor.ts | 7 +------ .../slate-editor/src/modules/editor/index.ts | 2 +- .../src/modules/editor/plugins/index.ts | 1 - .../modules/editor/plugins/withRichBlocks.ts | 21 ------------------- 7 files changed, 20 insertions(+), 33 deletions(-) delete mode 100644 packages/slate-editor/src/modules/editor/plugins/withRichBlocks.ts diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index 9eb145cce..a9c52473a 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -1,4 +1,4 @@ -import type { BaseEditor, Descendant, Element } from 'slate'; +import type { BaseEditor, Descendant, Element, Node } from 'slate'; import type { Extension } from '../types'; @@ -16,6 +16,13 @@ export interface ExtensionsEditor extends BaseEditor { */ isElementEqual(node: Element, another: Element): boolean | undefined; + /** + * Tell the Editor that the block is not a simple blob of styled text + * and is instead a typeof of rich block with additional decorations. + * Like, cards, or interactive elements. + */ + isRichBlock(node: Node): boolean; + /** * Convert internal editor document value for external consumers. * This is a convenient location for removing runtime-only editor elements @@ -62,6 +69,14 @@ export function withExtensions( return parent.isVoid(element); }, + isRichBlock(node): boolean { + for (const extension of extensionsEditor.extensions) { + if (extension.isRichBlock?.(node)) { + return true; + } + } + return false; + }, normalizeNode(entry) { const normalizers = extensionsEditor.extensions.flatMap( (ext) => ext.normalizeNode ?? [], diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index 3bf788a3a..15a4f11d1 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -21,7 +21,7 @@ export interface Extension { */ isElementEqual?: (node: Element, another: Element) => boolean | undefined; isInline?: (node: Node) => boolean; // OK - isRichBlock?: (node: Node) => boolean; + isRichBlock?: (node: Node) => boolean; // OK isVoid?: (node: Node) => boolean; // OK normalizeNode?: Normalize | Normalize[]; // OK onDOMBeforeInput?: OnDOMBeforeInput; // OK diff --git a/packages/slate-editor/src/index.ts b/packages/slate-editor/src/index.ts index ae049aca6..bad95592c 100644 --- a/packages/slate-editor/src/index.ts +++ b/packages/slate-editor/src/index.ts @@ -25,14 +25,13 @@ import type { HistoryEditor } from 'slate-history'; import type { ReactEditor } from 'slate-react'; import type { FlashEditor } from '#extensions/flash-nodes'; -import type { DefaultTextBlockEditor, RichBlocksAwareEditor } from '#modules/editor'; +import type { DefaultTextBlockEditor } from '#modules/editor'; type Editor = BaseEditor & ReactEditor & HistoryEditor & ExtensionsEditor & DefaultTextBlockEditor & - RichBlocksAwareEditor & FlashEditor; declare module 'slate' { diff --git a/packages/slate-editor/src/modules/editor/createEditor.ts b/packages/slate-editor/src/modules/editor/createEditor.ts index 913eb0c14..4b2b90f55 100644 --- a/packages/slate-editor/src/modules/editor/createEditor.ts +++ b/packages/slate-editor/src/modules/editor/createEditor.ts @@ -14,11 +14,7 @@ import { withReact } from 'slate-react'; import { createParagraph } from '#extensions/paragraphs'; import { withNodesHierarchy, hierarchySchema } from '#modules/nodes-hierarchy'; -import { - withDefaultTextBlock, - withDeserializeHtml, - withRichBlocks, -} from './plugins'; +import { withDefaultTextBlock, withDeserializeHtml } from './plugins'; export function createEditor( baseEditor: Editor, @@ -38,7 +34,6 @@ export function createEditor( withDefaultTextBlock(createParagraph), withUserFriendlyDeleteBehavior, withDeserializeHtml(getExtensions), - withRichBlocks(getExtensions), ...overrides, ...plugins, ])(baseEditor); diff --git a/packages/slate-editor/src/modules/editor/index.ts b/packages/slate-editor/src/modules/editor/index.ts index 0767fbc88..6e2104a8a 100644 --- a/packages/slate-editor/src/modules/editor/index.ts +++ b/packages/slate-editor/src/modules/editor/index.ts @@ -1,6 +1,6 @@ export { Editor } from './Editor'; export { createEditor } from './createEditor'; export { createEmptyValue } from './lib'; -export type { DefaultTextBlockEditor, RichBlocksAwareEditor } from './plugins'; +export type { DefaultTextBlockEditor } from './plugins'; export type { EditorRef, EditorProps, Value } from './types'; export { useEditorEvents } from './useEditorEvents'; diff --git a/packages/slate-editor/src/modules/editor/plugins/index.ts b/packages/slate-editor/src/modules/editor/plugins/index.ts index 4a014abbf..1fe0da6ae 100644 --- a/packages/slate-editor/src/modules/editor/plugins/index.ts +++ b/packages/slate-editor/src/modules/editor/plugins/index.ts @@ -1,3 +1,2 @@ export { type DefaultTextBlockEditor, withDefaultTextBlock } from './withDefaultTextBlock'; export { withDeserializeHtml } from './withDeserializeHtml'; -export { type RichBlocksAwareEditor, withRichBlocks } from './withRichBlocks'; diff --git a/packages/slate-editor/src/modules/editor/plugins/withRichBlocks.ts b/packages/slate-editor/src/modules/editor/plugins/withRichBlocks.ts deleted file mode 100644 index a243972e0..000000000 --- a/packages/slate-editor/src/modules/editor/plugins/withRichBlocks.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Extension } from '@prezly/slate-commons'; -import type { BaseEditor, Node } from 'slate'; - -export interface RichBlocksAwareEditor extends BaseEditor { - isRichBlock(node: Node): boolean; -} - -export function withRichBlocks(getExtensions: () => Extension[]) { - return function (editor: T): T & RichBlocksAwareEditor { - function isRichBlock(node: Node) { - for (const extension of getExtensions()) { - if (extension.isRichBlock?.(node)) return true; - } - return false; - } - - return Object.assign(editor, { - isRichBlock, - }); - }; -} From 4c033e43728fd1c8e03aa3d639e4d40ef679d3c6 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 16:20:05 +0300 Subject: [PATCH 27/54] [CARE-1802] Provide a way for extensions to hook into `editor.insertData()` function --- packages/slate-commons/package.json | 1 + .../src/extensions/ExtensionsEditor.ts | 23 +++++++++++++++++-- .../src/extensions/useRegisterExtension.ts | 2 ++ .../src/types/DataTransferHandler.ts | 4 ++++ packages/slate-commons/src/types/Extension.ts | 8 ++++++- packages/slate-commons/src/types/index.ts | 1 + 6 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 packages/slate-commons/src/types/DataTransferHandler.ts diff --git a/packages/slate-commons/package.json b/packages/slate-commons/package.json index 186d570a5..cbee360fe 100644 --- a/packages/slate-commons/package.json +++ b/packages/slate-commons/package.json @@ -50,6 +50,7 @@ "dependencies": { "@prezly/slate-types": "workspace:*", "@technically/is-not-null": "^1.0.0", + "@technically/is-not-undefined": "^1.0.0", "uuid": "^8.3.0" }, "devDependencies": { diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index a9c52473a..20192f35f 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -1,4 +1,6 @@ +import { isNotUndefined } from '@technically/is-not-undefined'; import type { BaseEditor, Descendant, Element, Node } from 'slate'; +import type { ReactEditor } from 'slate-react'; import type { Extension } from '../types'; @@ -31,13 +33,14 @@ export interface ExtensionsEditor extends BaseEditor { serialize(nodes: Descendant[]): Descendant[]; } -export function withExtensions( +export function withExtensions( editor: T, extensions: Extension[] = [], ): T & ExtensionsEditor { const parent = { isInline: editor.isInline, isVoid: editor.isVoid, + insertData: editor.insertData, normalizeNode: editor.normalizeNode, }; const extensionsEditor: T & ExtensionsEditor = Object.assign(editor, { @@ -77,6 +80,22 @@ export function withExtensions( } return false; }, + insertData(dataTransfer) { + const handlers = extensionsEditor.extensions + .map((ext) => ext.insertData) + .filter(isNotUndefined); + + function next(dataTransfer: DataTransfer) { + const handler = handlers.shift(); + if (handler) { + handler(dataTransfer, next); + } else { + parent.insertData(dataTransfer); + } + } + + next(dataTransfer); + }, normalizeNode(entry) { const normalizers = extensionsEditor.extensions.flatMap( (ext) => ext.normalizeNode ?? [], @@ -98,7 +117,7 @@ export function withExtensions( nodes, ); }, - } satisfies Partial); + } satisfies Partial); return extensionsEditor; } diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts index 5fe70130e..c783a4b1b 100644 --- a/packages/slate-commons/src/extensions/useRegisterExtension.ts +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -15,6 +15,7 @@ export function useRegisterExtension(extension: Extension): null { isInline, isRichBlock, isVoid, + insertData, normalizeNode, onDOMBeforeInput, onKeyDown, @@ -40,6 +41,7 @@ export function useRegisterExtension(extension: Extension): null { isInline, isRichBlock, isVoid, + insertData, normalizeNode, onDOMBeforeInput, onKeyDown, diff --git a/packages/slate-commons/src/types/DataTransferHandler.ts b/packages/slate-commons/src/types/DataTransferHandler.ts new file mode 100644 index 000000000..5dab8a6d7 --- /dev/null +++ b/packages/slate-commons/src/types/DataTransferHandler.ts @@ -0,0 +1,4 @@ +export type DataTransferHandler = ( + dataTransfer: DataTransfer, + next: (dataTransfer: DataTransfer) => void, +) => void; diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index 15a4f11d1..34e08db6d 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -1,5 +1,6 @@ import type { Element, Node } from 'slate'; +import type { DataTransferHandler } from './DataTransferHandler'; import type { DecorateFactory } from './DecorateFactory'; import type { DeserializeHtml } from './DeserializeHtml'; import type { Normalize } from './Normalize'; @@ -13,7 +14,12 @@ import type { WithOverrides } from './WithOverrides'; export interface Extension { id: string; decorate?: DecorateFactory; // OK - deserialize?: DeserializeHtml; + deserialize?: DeserializeHtml; // OK + /** + * Hook into ReactEditor's `insertData()` method. + * Call `next()` to allow other extensions (or the editor) handling the event. + */ + insertData?: DataTransferHandler; // OK /** * Compare two elements. * `children` arrays can be omitted from the comparison, diff --git a/packages/slate-commons/src/types/index.ts b/packages/slate-commons/src/types/index.ts index 2926f589c..9aee17227 100644 --- a/packages/slate-commons/src/types/index.ts +++ b/packages/slate-commons/src/types/index.ts @@ -1,3 +1,4 @@ +export type { DataTransferHandler } from './DataTransferHandler'; export type { Decorate } from './Decorate'; export type { DecorateFactory } from './DecorateFactory'; export type { DeserializeHtml, DeserializeElement, DeserializeMarks } from './DeserializeHtml'; From d38af1df755e4c40e8592d687df9700b7beedd77 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 16:22:36 +0300 Subject: [PATCH 28/54] [CARE-1802] Extract Paste event tracking into a separate extension --- .../paste-tracking/PasteTrackingExtension.ts | 25 +++++++++++++++++++ .../src/extensions/paste-tracking/index.ts | 1 + .../src/modules/editor/Editor.tsx | 10 ++++++++ .../withDeserializeHtml.ts | 5 ---- 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 packages/slate-editor/src/extensions/paste-tracking/PasteTrackingExtension.ts create mode 100644 packages/slate-editor/src/extensions/paste-tracking/index.ts diff --git a/packages/slate-editor/src/extensions/paste-tracking/PasteTrackingExtension.ts b/packages/slate-editor/src/extensions/paste-tracking/PasteTrackingExtension.ts new file mode 100644 index 000000000..0d02ceaaa --- /dev/null +++ b/packages/slate-editor/src/extensions/paste-tracking/PasteTrackingExtension.ts @@ -0,0 +1,25 @@ +import type { DataTransferHandler } from '@prezly/slate-commons'; +import { useRegisterExtension } from '@prezly/slate-commons'; +import { useCallback } from 'react'; + +import { useLatest } from '#lib'; + +export const EXTENSION_ID = 'PasteTrackingExtension'; + +export interface Parameters { + onPaste(dataTransfer: DataTransfer): void; +} + +export function PasteTrackingExtension({ onPaste }: Parameters) { + const callback = useLatest({ onPaste }); + + const insertData = useCallback((dataTransfer, next) => { + callback.current.onPaste(dataTransfer); + next(dataTransfer); + }, []); + + return useRegisterExtension({ + id: EXTENSION_ID, + insertData, + }); +} diff --git a/packages/slate-editor/src/extensions/paste-tracking/index.ts b/packages/slate-editor/src/extensions/paste-tracking/index.ts new file mode 100644 index 000000000..258c0bf70 --- /dev/null +++ b/packages/slate-editor/src/extensions/paste-tracking/index.ts @@ -0,0 +1 @@ +export { PasteTrackingExtension } from './PasteTrackingExtension'; diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 03cc6894e..27ba0705a 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -24,6 +24,7 @@ import { useFunction, useGetSet, useSize } from '#lib'; import { insertButtonBlock } from '#extensions/button-block'; import { FlashNodesExtension } from '#extensions/flash-nodes'; import { FloatingAddMenuExtension, type Option } from '#extensions/floating-add-menu'; +import { PasteTrackingExtension } from '#extensions/paste-tracking'; import { insertPlaceholder, PlaceholderNode } from '#extensions/placeholders'; import { RichFormattingMenuExtension, toggleBlock } from '#extensions/rich-formatting-menu'; import { SnippetsExtension } from '#extensions/snippet'; @@ -726,6 +727,15 @@ export const Editor = forwardRef((props, forwardedRef) = style={contentStyle} /> + { + EventsEditor.dispatchEvent(editor, 'paste', { + isEmpty: EditorCommands.isEmpty(editor), + pastedLength: data.getData('text/plain').length, + }); + }} + /> + Extension[]) { editor.insertData = function (data) { const slateFragment = data.getData('application/x-slate-fragment'); - EventsEditor.dispatchEvent(editor, 'paste', { - isEmpty: EditorCommands.isEmpty(editor), - pastedLength: data.getData('text/plain').length, - }); - if (slateFragment) { insertData(data); return; From f23d4b9ee5fdc55c745937a98622fb5f3724e185 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 01:06:09 +0300 Subject: [PATCH 29/54] [CARE-1802] Rewrite Slate Pasting extension to the new `insertData()` hook Without relying on `withOverrides()`  Conflicts:  packages/slate-editor/src/extensions/paste-slate-content/lib/withSlatePasting.ts  packages/slate-editor/src/modules/editor/Extensions.tsx --- .../PasteSlateContentExtension.ts | 26 +++++-- .../lib/createDataTransferHandler.ts | 67 +++++++++++++++++ .../paste-slate-content/lib/index.ts | 2 +- .../lib/{isFragment.ts => isValidFragment.ts} | 2 +- .../lib/withSlatePasting.test.tsx | 2 + .../lib/withSlatePasting.ts | 73 ------------------- .../extensions/paste-slate-content/types.ts | 3 + .../src/modules/editor/Editor.tsx | 15 +++- .../src/modules/editor/Extensions.tsx | 3 - 9 files changed, 109 insertions(+), 84 deletions(-) create mode 100644 packages/slate-editor/src/extensions/paste-slate-content/lib/createDataTransferHandler.ts rename packages/slate-editor/src/extensions/paste-slate-content/lib/{isFragment.ts => isValidFragment.ts} (81%) delete mode 100644 packages/slate-editor/src/extensions/paste-slate-content/lib/withSlatePasting.ts create mode 100644 packages/slate-editor/src/extensions/paste-slate-content/types.ts diff --git a/packages/slate-editor/src/extensions/paste-slate-content/PasteSlateContentExtension.ts b/packages/slate-editor/src/extensions/paste-slate-content/PasteSlateContentExtension.ts index fdf71c15b..b0197259d 100644 --- a/packages/slate-editor/src/extensions/paste-slate-content/PasteSlateContentExtension.ts +++ b/packages/slate-editor/src/extensions/paste-slate-content/PasteSlateContentExtension.ts @@ -1,20 +1,36 @@ +import type { DataTransferHandler } from '@prezly/slate-commons'; import { useRegisterExtension } from '@prezly/slate-commons'; +import { useMemo } from 'react'; +import { useSlateStatic } from 'slate-react'; -import { withSlatePasting } from './lib'; -import type { IsPreservedBlock } from './lib/withSlatePasting'; +import { useLatest } from '#lib'; + +import { createDataTransferHandler } from './lib'; +import type { IsPreservedBlock } from './types'; export const EXTENSION_ID = 'PasteSlateContentExtension'; -interface Options { +interface Parameters { /** * Defines which blocks should be preserved, if pasted empty content. */ isPreservedBlock?: IsPreservedBlock; } -export function PasteSlateContentExtension({ isPreservedBlock = () => false }: Options = {}) { +export function PasteSlateContentExtension({ isPreservedBlock = alwaysFalse }: Parameters = {}) { + const editor = useSlateStatic(); + const callbacks = useLatest({ isPreservedBlock }); + + const insertData = useMemo(() => { + return createDataTransferHandler(editor, callbacks.current.isPreservedBlock); + }, []); + return useRegisterExtension({ id: EXTENSION_ID, - withOverrides: withSlatePasting(isPreservedBlock), + insertData, }); } + +function alwaysFalse() { + return false; +} diff --git a/packages/slate-editor/src/extensions/paste-slate-content/lib/createDataTransferHandler.ts b/packages/slate-editor/src/extensions/paste-slate-content/lib/createDataTransferHandler.ts new file mode 100644 index 000000000..6f52fe228 --- /dev/null +++ b/packages/slate-editor/src/extensions/paste-slate-content/lib/createDataTransferHandler.ts @@ -0,0 +1,67 @@ +import type { DataTransferHandler } from '@prezly/slate-commons'; +import { EditorCommands } from '@prezly/slate-commons'; +import { Editor, Transforms } from 'slate'; + +import { decodeSlateFragment, filterDataTransferItems } from '#lib'; + +import type { IsPreservedBlock } from '../types'; + +import { isValidFragment, type Fragment } from './isValidFragment'; + +export function createDataTransferHandler( + editor: Editor, + isPreservedBlock: IsPreservedBlock, +): DataTransferHandler { + return (dataTransfer, next) => { + const slateFragment = dataTransfer.getData('application/x-slate-fragment'); + + if (slateFragment) { + const fragment = decodeSlateFragment(slateFragment); + + if (isValidFragment(fragment)) { + if (handlePastingIntoPreservedBlock(editor, fragment, isPreservedBlock)) { + return; + } + + if (editor.selection) { + Transforms.insertFragment(editor, fragment); + } else { + Transforms.insertNodes(editor, fragment); + } + } else { + // Trigger another iteration of pasted content handling, + // but without the Slate content property. + // Note: this is not the same as calling `next()`. + editor.insertData(withoutSlateFragmentData(dataTransfer)); + } + } + + return next(dataTransfer); + }; +} + +function withoutSlateFragmentData(data: DataTransfer): DataTransfer { + return filterDataTransferItems(data, (item) => item.type !== 'application/x-slate-fragment'); +} + +function handlePastingIntoPreservedBlock( + editor: Editor, + fragment: Fragment, + isPreservedBlock: IsPreservedBlock, +) { + const nodesAbove = Editor.nodes(editor, { + match: (node) => EditorCommands.isBlock(editor, node), + }); + const [nearestBlock] = Array.from(nodesAbove).at(-1) ?? []; + + if ( + nearestBlock && + EditorCommands.isNodeEmpty(editor, nearestBlock) && + isPreservedBlock(editor, nearestBlock) + ) { + Transforms.insertNodes(editor, fragment, { at: editor.selection?.anchor.path }); + return true; + } + + return false; +} diff --git a/packages/slate-editor/src/extensions/paste-slate-content/lib/index.ts b/packages/slate-editor/src/extensions/paste-slate-content/lib/index.ts index 77233d17d..f1e3120c2 100644 --- a/packages/slate-editor/src/extensions/paste-slate-content/lib/index.ts +++ b/packages/slate-editor/src/extensions/paste-slate-content/lib/index.ts @@ -1 +1 @@ -export { withSlatePasting } from './withSlatePasting'; +export { createDataTransferHandler } from './createDataTransferHandler'; diff --git a/packages/slate-editor/src/extensions/paste-slate-content/lib/isFragment.ts b/packages/slate-editor/src/extensions/paste-slate-content/lib/isValidFragment.ts similarity index 81% rename from packages/slate-editor/src/extensions/paste-slate-content/lib/isFragment.ts rename to packages/slate-editor/src/extensions/paste-slate-content/lib/isValidFragment.ts index d2eee019b..765ac1721 100644 --- a/packages/slate-editor/src/extensions/paste-slate-content/lib/isFragment.ts +++ b/packages/slate-editor/src/extensions/paste-slate-content/lib/isValidFragment.ts @@ -6,7 +6,7 @@ export type Fragment = Node[]; * Checks recursively whether all members of the fragment are Nodes. * It does not validate schema/hierarchy. */ -export function isFragment(value: unknown): value is Fragment { +export function isValidFragment(value: unknown): value is Fragment { if (!Node.isNodeList(value)) { return false; } diff --git a/packages/slate-editor/src/extensions/paste-slate-content/lib/withSlatePasting.test.tsx b/packages/slate-editor/src/extensions/paste-slate-content/lib/withSlatePasting.test.tsx index 8a9a3a605..5e2e37525 100644 --- a/packages/slate-editor/src/extensions/paste-slate-content/lib/withSlatePasting.test.tsx +++ b/packages/slate-editor/src/extensions/paste-slate-content/lib/withSlatePasting.test.tsx @@ -22,6 +22,8 @@ const FRAGMENT: object[] = [ const SINGLE_NODE_FRAGMENT: object = FRAGMENT[0]; +// FIXME: Reconstruct this test + describe('withSlatePasting', () => { it('should pick up "application/x-slate-fragment" content when editor is empty', () => { const editor = () as unknown as Editor; diff --git a/packages/slate-editor/src/extensions/paste-slate-content/lib/withSlatePasting.ts b/packages/slate-editor/src/extensions/paste-slate-content/lib/withSlatePasting.ts deleted file mode 100644 index eb4bcdd54..000000000 --- a/packages/slate-editor/src/extensions/paste-slate-content/lib/withSlatePasting.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { EditorCommands } from '@prezly/slate-commons'; -import type { Node } from 'slate'; -import { Editor, Transforms } from 'slate'; - -import { decodeSlateFragment, filterDataTransferItems } from '#lib'; - -import type { Fragment as SlateFragment } from './isFragment'; -import { isFragment as isValidFragment } from './isFragment'; - -export type IsPreservedBlock = (editor: Editor, node: Node) => boolean; - -export function withSlatePasting(isPreservedBlock: IsPreservedBlock) { - return function (editor: T) { - const { insertData } = editor; - - editor.insertData = (data) => { - const slateFragment = data.getData('application/x-slate-fragment'); - - if (slateFragment) { - const fragment = decodeSlateFragment(slateFragment); - - if (isValidFragment(fragment)) { - if (handlePastingIntoPreservedBlock(editor, fragment, isPreservedBlock)) { - return; - } - - if (editor.selection) { - Transforms.insertFragment(editor, fragment); - } else { - Transforms.insertNodes(editor, fragment); - } - } else { - editor.insertData(withoutSlateFragmentData(data)); - } - - return; - } - - insertData(data); - }; - - return editor; - }; -} - -function withoutSlateFragmentData(dataTransfer: DataTransfer): DataTransfer { - return filterDataTransferItems( - dataTransfer, - (item) => item.type !== 'application/x-slate-fragment', - ); -} - -function handlePastingIntoPreservedBlock( - editor: Editor, - fragment: SlateFragment, - isPreservedBlock: IsPreservedBlock, -) { - const nodesAbove = Editor.nodes(editor, { - match: (node) => EditorCommands.isBlock(editor, node), - }); - const [nearestBlock] = Array.from(nodesAbove).at(-1) ?? []; - - if ( - nearestBlock && - EditorCommands.isNodeEmpty(editor, nearestBlock) && - isPreservedBlock(editor, nearestBlock) - ) { - Transforms.insertNodes(editor, fragment, { at: editor.selection?.anchor.path }); - return true; - } - - return false; -} diff --git a/packages/slate-editor/src/extensions/paste-slate-content/types.ts b/packages/slate-editor/src/extensions/paste-slate-content/types.ts new file mode 100644 index 000000000..12afa11f4 --- /dev/null +++ b/packages/slate-editor/src/extensions/paste-slate-content/types.ts @@ -0,0 +1,3 @@ +import type { Editor, Node } from 'slate'; + +export type IsPreservedBlock = (editor: Editor, node: Node) => boolean; diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 27ba0705a..52b8544e9 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -11,6 +11,8 @@ import { HEADING_2_NODE_TYPE, PARAGRAPH_NODE_TYPE, QUOTE_NODE_TYPE, + isQuoteNode, + isImageNode, } from '@prezly/slate-types'; import { noop } from '@technically/lodash'; import classNames from 'classnames'; @@ -24,6 +26,7 @@ import { useFunction, useGetSet, useSize } from '#lib'; import { insertButtonBlock } from '#extensions/button-block'; import { FlashNodesExtension } from '#extensions/flash-nodes'; import { FloatingAddMenuExtension, type Option } from '#extensions/floating-add-menu'; +import { PasteSlateContentExtension } from '#extensions/paste-slate-content'; import { PasteTrackingExtension } from '#extensions/paste-tracking'; import { insertPlaceholder, PlaceholderNode } from '#extensions/placeholders'; import { RichFormattingMenuExtension, toggleBlock } from '#extensions/rich-formatting-menu'; @@ -736,9 +739,19 @@ export const Editor = forwardRef((props, forwardedRef) = }} /> + { + if (withBlockquotes && isQuoteNode(node)) { + return true; + } else if (withImages && isImageNode(node)) { + return true; + } + return false; + }} + /> + ; } & Pick< Required, | 'withAllowedBlocks' @@ -87,7 +85,6 @@ type Props = { export function Extensions({ availableWidth, - containerRef, withAllowedBlocks, withAttachments, withAutoformat, From 6d2a1a491c2d16aa11de6177628d93c13863a71d Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 16:42:01 +0300 Subject: [PATCH 30/54] [CARE-1802] Deprecate `withOverrides()` --- packages/slate-commons/src/types/Extension.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index 34e08db6d..b1d00aadb 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -35,5 +35,8 @@ export interface Extension { renderElement?: RenderElement; // OK renderLeaf?: RenderLeaf; // OK serialize?: Serialize; // OK + /** + * @deprecated Please do not use this. We're going to drop this functionality soon. + */ withOverrides?: WithOverrides; } From 1b5485d49d387f80c43a19bef3855a6dfbb63511 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 01:09:23 +0300 Subject: [PATCH 31/54] [CARE-1802] Convert `withDeserializeHtml` editor mutator function to an Extension  Conflicts:  packages/slate-editor/src/modules/editor/createEditor.ts  packages/slate-editor/src/modules/editor/plugins/index.ts  packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/withDeserializeHtml.ts --- .../PasteHtmlContentExtension.ts | 86 ++++++++++++++++++ .../extensions/paste-html-content/index.ts | 1 + ...01.wraps-orphan-list-items-into-lists.html | 0 ...01.wraps-orphan-list-items-into-lists.json | 0 ...aps-orphan-list-item-texts-into-lists.html | 0 ...aps-orphan-list-item-texts-into-lists.json | 0 .../03.paragraphs-with-newlines.html | 0 .../03.paragraphs-with-newlines.json | 0 .../lib}/__tests__/04.paragraph.html | 0 .../lib}/__tests__/04.paragraph.json | 0 .../__tests__/05.google-docs-divider.html | 0 .../__tests__/05.google-docs-divider.json | 0 .../__tests__/06.paragraphs-and-quotes.html | 0 .../__tests__/06.paragraphs-and-quotes.json | 0 .../lib}/__tests__/07.nested-lists.html | 0 .../lib}/__tests__/07.nested-lists.json | 0 .../lib}/__tests__/08.recursive-marks.html | 0 .../lib}/__tests__/08.recursive-marks.json | 0 .../09.elements-with-text-styling.html | 0 .../09.elements-with-text-styling.json | 0 .../lib}/__tests__/10.unknown-elements.html | 0 .../lib}/__tests__/10.unknown-elements.json | 0 .../createDeserialize/createDeserializer.ts | 0 .../createElementsDeserializer.ts | 0 .../createMarksDeserializer.ts | 0 .../createTextDeserializer.ts | 0 .../lib}/createDeserialize/dom.ts | 0 .../lib}/createDeserialize/index.ts | 0 .../replaceCarriageReturnWithLineFeed.test.ts | 0 .../replaceCarriageReturnWithLineFeed.ts | 0 .../lib}/deserializeHtml.test.ts | 0 .../lib}/deserializeHtml.ts | 7 +- .../paste-html-content/lib}/index.ts | 0 .../lib}/normalizers/index.ts | 0 .../normalizeGoogleDocsAppleNewlines.ts | 0 .../normalizeGoogleDocsDividers.ts | 0 .../normalizeOrphanListItemTexts.ts | 0 .../normalizers/normalizeOrphanListItems.ts | 0 .../normalizers/normalizeSlackLineBreaks.ts | 0 .../normalizers/normalizeUselessBodyTags.ts | 0 .../normalizers/normalizeZeroWidthSpaces.ts | 0 .../src/modules/editor/Editor.tsx | 13 ++- .../src/modules/editor/createEditor.ts | 3 +- .../src/modules/editor/plugins/index.ts | 1 - .../plugins/withDeserializeHtml/index.ts | 1 - .../withDeserializeHtml.ts | 91 ------------------- 46 files changed, 99 insertions(+), 104 deletions(-) create mode 100644 packages/slate-editor/src/extensions/paste-html-content/PasteHtmlContentExtension.ts create mode 100644 packages/slate-editor/src/extensions/paste-html-content/index.ts rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/01.wraps-orphan-list-items-into-lists.html (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/01.wraps-orphan-list-items-into-lists.json (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/02.wraps-orphan-list-item-texts-into-lists.html (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/02.wraps-orphan-list-item-texts-into-lists.json (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/03.paragraphs-with-newlines.html (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/03.paragraphs-with-newlines.json (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/04.paragraph.html (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/04.paragraph.json (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/05.google-docs-divider.html (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/05.google-docs-divider.json (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/06.paragraphs-and-quotes.html (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/06.paragraphs-and-quotes.json (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/07.nested-lists.html (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/07.nested-lists.json (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/08.recursive-marks.html (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/08.recursive-marks.json (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/09.elements-with-text-styling.html (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/09.elements-with-text-styling.json (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/10.unknown-elements.html (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/__tests__/10.unknown-elements.json (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/createDeserialize/createDeserializer.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/createDeserialize/createElementsDeserializer.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/createDeserialize/createMarksDeserializer.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/createDeserialize/createTextDeserializer.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/createDeserialize/dom.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/createDeserialize/index.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/createDeserialize/replaceCarriageReturnWithLineFeed.test.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/createDeserialize/replaceCarriageReturnWithLineFeed.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/deserializeHtml.test.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/deserializeHtml.ts (86%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml/deserializeHtml => extensions/paste-html-content/lib}/index.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/normalizers/index.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/normalizers/normalizeGoogleDocsAppleNewlines.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/normalizers/normalizeGoogleDocsDividers.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/normalizers/normalizeOrphanListItemTexts.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/normalizers/normalizeOrphanListItems.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/normalizers/normalizeSlackLineBreaks.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/normalizers/normalizeUselessBodyTags.ts (100%) rename packages/slate-editor/src/{modules/editor/plugins/withDeserializeHtml => extensions/paste-html-content/lib}/normalizers/normalizeZeroWidthSpaces.ts (100%) delete mode 100644 packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/index.ts delete mode 100644 packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/withDeserializeHtml.ts diff --git a/packages/slate-editor/src/extensions/paste-html-content/PasteHtmlContentExtension.ts b/packages/slate-editor/src/extensions/paste-html-content/PasteHtmlContentExtension.ts new file mode 100644 index 000000000..43cf18076 --- /dev/null +++ b/packages/slate-editor/src/extensions/paste-html-content/PasteHtmlContentExtension.ts @@ -0,0 +1,86 @@ +import { cleanDocx } from '@prezly/docx-cleaner'; +import type { DataTransferHandler } from '@prezly/slate-commons'; +import { EditorCommands, useRegisterExtension } from '@prezly/slate-commons'; +import { useCallback } from 'react'; +import type { Editor, Node } from 'slate'; +import { Element } from 'slate'; +import { useSlateStatic } from 'slate-react'; + +import { EventsEditor } from '#modules/events'; + +import { deserializeHtml } from './lib'; + +export const EXTENSION_ID = 'PasteHtmlContentExtension'; + +export function PasteHtmlContentExtension() { + const editor = useSlateStatic(); + + const insertData = useCallback((dataTransfer, next) => { + function handleError(error: unknown) { + return EventsEditor.dispatchEvent(editor, 'error', error); + } + + const html = dataTransfer.getData('text/html'); + + if (html) { + const rtf = dataTransfer.getData('text/rtf'); + const cleanHtml = tryCleanDocx(html, rtf, handleError); + const nodes = deserializeHtml(editor.extensions, cleanHtml, handleError); + + if (nodes.length === 0) { + // If there are no "nodes" then there is no interesting "text/html" in clipboard. + // Pass through to default "insertData" so that "text/plain" is used if available. + return next(dataTransfer); + } + + const singleTextBlockInserted = checkSingleTextBlockInserted(editor, nodes); + + if (singleTextBlockInserted) { + // If it's a single block inserted, inherit block formatting from the destination + // location, instead of overwriting it with the pasted block style. + // @see CARE-1853 + EditorCommands.insertNodes(editor, singleTextBlockInserted.children, { + ensureEmptyParagraphAfter: true, + mode: 'highest', + }); + return; + } + + EditorCommands.insertNodes(editor, nodes, { + ensureEmptyParagraphAfter: true, + mode: 'highest', + }); + return; + } + + next(dataTransfer); + }, []); + + return useRegisterExtension({ + id: EXTENSION_ID, + insertData, + }); +} + +function tryCleanDocx(html: string, rtf: string, onError: (error: unknown) => void): string { + try { + return cleanDocx(html, rtf); + } catch (error) { + onError(error); + return html; + } +} + +function checkSingleTextBlockInserted(editor: Editor, nodes: Node[]): Element | undefined { + const [node] = nodes; + + if ( + nodes.length === 1 && + Element.isElement(node) && + editor.isBlock(node) && + !editor.isRichBlock(node) + ) { + return node; + } + return undefined; +} diff --git a/packages/slate-editor/src/extensions/paste-html-content/index.ts b/packages/slate-editor/src/extensions/paste-html-content/index.ts new file mode 100644 index 000000000..edcc9f019 --- /dev/null +++ b/packages/slate-editor/src/extensions/paste-html-content/index.ts @@ -0,0 +1 @@ +export { PasteHtmlContentExtension, EXTENSION_ID } from './PasteHtmlContentExtension'; diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/01.wraps-orphan-list-items-into-lists.html b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/01.wraps-orphan-list-items-into-lists.html similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/01.wraps-orphan-list-items-into-lists.html rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/01.wraps-orphan-list-items-into-lists.html diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/01.wraps-orphan-list-items-into-lists.json b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/01.wraps-orphan-list-items-into-lists.json similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/01.wraps-orphan-list-items-into-lists.json rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/01.wraps-orphan-list-items-into-lists.json diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/02.wraps-orphan-list-item-texts-into-lists.html b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/02.wraps-orphan-list-item-texts-into-lists.html similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/02.wraps-orphan-list-item-texts-into-lists.html rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/02.wraps-orphan-list-item-texts-into-lists.html diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/02.wraps-orphan-list-item-texts-into-lists.json b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/02.wraps-orphan-list-item-texts-into-lists.json similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/02.wraps-orphan-list-item-texts-into-lists.json rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/02.wraps-orphan-list-item-texts-into-lists.json diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/03.paragraphs-with-newlines.html b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/03.paragraphs-with-newlines.html similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/03.paragraphs-with-newlines.html rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/03.paragraphs-with-newlines.html diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/03.paragraphs-with-newlines.json b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/03.paragraphs-with-newlines.json similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/03.paragraphs-with-newlines.json rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/03.paragraphs-with-newlines.json diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/04.paragraph.html b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/04.paragraph.html similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/04.paragraph.html rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/04.paragraph.html diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/04.paragraph.json b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/04.paragraph.json similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/04.paragraph.json rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/04.paragraph.json diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/05.google-docs-divider.html b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/05.google-docs-divider.html similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/05.google-docs-divider.html rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/05.google-docs-divider.html diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/05.google-docs-divider.json b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/05.google-docs-divider.json similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/05.google-docs-divider.json rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/05.google-docs-divider.json diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/06.paragraphs-and-quotes.html b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/06.paragraphs-and-quotes.html similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/06.paragraphs-and-quotes.html rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/06.paragraphs-and-quotes.html diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/06.paragraphs-and-quotes.json b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/06.paragraphs-and-quotes.json similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/06.paragraphs-and-quotes.json rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/06.paragraphs-and-quotes.json diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/07.nested-lists.html b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/07.nested-lists.html similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/07.nested-lists.html rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/07.nested-lists.html diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/07.nested-lists.json b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/07.nested-lists.json similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/07.nested-lists.json rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/07.nested-lists.json diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/08.recursive-marks.html b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/08.recursive-marks.html similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/08.recursive-marks.html rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/08.recursive-marks.html diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/08.recursive-marks.json b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/08.recursive-marks.json similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/08.recursive-marks.json rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/08.recursive-marks.json diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/09.elements-with-text-styling.html b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/09.elements-with-text-styling.html similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/09.elements-with-text-styling.html rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/09.elements-with-text-styling.html diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/09.elements-with-text-styling.json b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/09.elements-with-text-styling.json similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/09.elements-with-text-styling.json rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/09.elements-with-text-styling.json diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/10.unknown-elements.html b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/10.unknown-elements.html similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/10.unknown-elements.html rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/10.unknown-elements.html diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/10.unknown-elements.json b/packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/10.unknown-elements.json similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/__tests__/10.unknown-elements.json rename to packages/slate-editor/src/extensions/paste-html-content/lib/__tests__/10.unknown-elements.json diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/createDeserializer.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/createDeserializer.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/createDeserializer.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/createDeserializer.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/createElementsDeserializer.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/createElementsDeserializer.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/createElementsDeserializer.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/createElementsDeserializer.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/createMarksDeserializer.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/createMarksDeserializer.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/createMarksDeserializer.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/createMarksDeserializer.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/createTextDeserializer.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/createTextDeserializer.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/createTextDeserializer.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/createTextDeserializer.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/dom.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/dom.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/dom.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/dom.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/index.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/index.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/index.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/index.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/replaceCarriageReturnWithLineFeed.test.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/replaceCarriageReturnWithLineFeed.test.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/replaceCarriageReturnWithLineFeed.test.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/replaceCarriageReturnWithLineFeed.test.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/replaceCarriageReturnWithLineFeed.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/replaceCarriageReturnWithLineFeed.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/createDeserialize/replaceCarriageReturnWithLineFeed.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/createDeserialize/replaceCarriageReturnWithLineFeed.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/deserializeHtml.test.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/deserializeHtml.test.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/deserializeHtml.test.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/deserializeHtml.test.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/deserializeHtml.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/deserializeHtml.ts similarity index 86% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/deserializeHtml.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/deserializeHtml.ts index ac201775d..197709d56 100644 --- a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/deserializeHtml.ts +++ b/packages/slate-editor/src/extensions/paste-html-content/lib/deserializeHtml.ts @@ -1,7 +1,7 @@ import type { Extension } from '@prezly/slate-commons'; import type { Descendant } from 'slate'; -import { createDeserializer } from '../createDeserialize'; +import { createDeserializer } from './createDeserialize'; import { normalizeGoogleDocsAppleNewlines, normalizeGoogleDocsDividers, @@ -10,7 +10,7 @@ import { normalizeSlackLineBreaks, normalizeUselessBodyTags, normalizeZeroWidthSpaces, -} from '../normalizers'; +} from './normalizers'; const normalizers = [ normalizeGoogleDocsAppleNewlines, @@ -34,6 +34,5 @@ export function deserializeHtml( document, ); const deserialize = createDeserializer(extensions, onError); - const nodes = deserialize(normalizedDocument.body); - return nodes; + return deserialize(normalizedDocument.body); } diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/index.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/index.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/deserializeHtml/index.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/index.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/index.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/index.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/index.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/index.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeGoogleDocsAppleNewlines.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeGoogleDocsAppleNewlines.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeGoogleDocsAppleNewlines.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeGoogleDocsAppleNewlines.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeGoogleDocsDividers.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeGoogleDocsDividers.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeGoogleDocsDividers.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeGoogleDocsDividers.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeOrphanListItemTexts.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeOrphanListItemTexts.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeOrphanListItemTexts.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeOrphanListItemTexts.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeOrphanListItems.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeOrphanListItems.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeOrphanListItems.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeOrphanListItems.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeSlackLineBreaks.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeSlackLineBreaks.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeSlackLineBreaks.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeSlackLineBreaks.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeUselessBodyTags.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeUselessBodyTags.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeUselessBodyTags.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeUselessBodyTags.ts diff --git a/packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeZeroWidthSpaces.ts b/packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeZeroWidthSpaces.ts similarity index 100% rename from packages/slate-editor/src/modules/editor/plugins/withDeserializeHtml/normalizers/normalizeZeroWidthSpaces.ts rename to packages/slate-editor/src/extensions/paste-html-content/lib/normalizers/normalizeZeroWidthSpaces.ts diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 52b8544e9..6fcc4a763 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -3,16 +3,16 @@ import { Events } from '@prezly/events'; import { EditorCommands, ExtensionsManager } from '@prezly/slate-commons'; import { TablesEditor } from '@prezly/slate-tables'; import { - type HeadingNode, - type ParagraphNode, - type QuoteNode, Alignment, HEADING_1_NODE_TYPE, HEADING_2_NODE_TYPE, + type HeadingNode, + isImageNode, + isQuoteNode, PARAGRAPH_NODE_TYPE, + type ParagraphNode, QUOTE_NODE_TYPE, - isQuoteNode, - isImageNode, + type QuoteNode, } from '@prezly/slate-types'; import { noop } from '@technically/lodash'; import classNames from 'classnames'; @@ -26,6 +26,7 @@ import { useFunction, useGetSet, useSize } from '#lib'; import { insertButtonBlock } from '#extensions/button-block'; import { FlashNodesExtension } from '#extensions/flash-nodes'; import { FloatingAddMenuExtension, type Option } from '#extensions/floating-add-menu'; +import { PasteHtmlContentExtension } from '#extensions/paste-html-content'; import { PasteSlateContentExtension } from '#extensions/paste-slate-content'; import { PasteTrackingExtension } from '#extensions/paste-tracking'; import { insertPlaceholder, PlaceholderNode } from '#extensions/placeholders'; @@ -750,6 +751,8 @@ export const Editor = forwardRef((props, forwardedRef) = }} /> + + void): string { - try { - return cleanDocx(html, rtf); - } catch (error) { - onError(error); - return html; - } -} - -export function withDeserializeHtml(getExtensions: () => Extension[]) { - return function (editor: T) { - const { insertData } = editor; - - function handleError(error: unknown) { - return EventsEditor.dispatchEvent(editor, 'error', error); - } - - editor.insertData = function (data) { - const slateFragment = data.getData('application/x-slate-fragment'); - - if (slateFragment) { - insertData(data); - return; - } - - const html = data.getData('text/html'); - - if (html) { - const rtf = data.getData('text/rtf'); - const cleanHtml = tryCleanDocx(html, rtf, handleError); - const extensions = getExtensions(); - const nodes = deserializeHtml(extensions, cleanHtml, handleError); - - if (nodes.length === 0) { - // If there are no "nodes" then there is no interesting "text/html" in clipboard. - // Pass through to default "insertData" so that "text/plain" is used if available. - return insertData(data); - } - - const singleTextBlockInserted = checkSingleTextBlockInserted(editor, nodes); - - if (singleTextBlockInserted) { - // If it's a single block inserted, inherit block formatting from the destination - // location, instead of overwriting it with the pasted block style. - // @see CARE-1853 - EditorCommands.insertNodes(editor, singleTextBlockInserted.children, { - ensureEmptyParagraphAfter: true, - mode: 'highest', - }); - return; - } - - EditorCommands.insertNodes(editor, nodes, { - ensureEmptyParagraphAfter: true, - mode: 'highest', - }); - return; - } - - insertData(data); - }; - - return editor; - }; -} - -function checkSingleTextBlockInserted(editor: Editor, nodes: Node[]): Element | undefined { - const [node] = nodes; - - if ( - nodes.length === 1 && - Element.isElement(node) && - editor.isBlock(node) && - !editor.isRichBlock(node) - ) { - return node; - } - return undefined; -} From 0dbf50841428a4b08cf960dbebf853181f26b9bc Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 01:26:25 +0300 Subject: [PATCH 32/54] [CARE-1802] Rewrite Images & Files pasting to the new `insertData()` extension hook Instead of relying on the hard-to-maintain `withOverrides` extension feature  Conflicts:  packages/slate-editor/src/extensions/loader/index.ts  packages/slate-editor/src/extensions/loader/transforms/index.ts  packages/slate-editor/src/extensions/loader/transforms/insertUploadingFile.ts  packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts  packages/slate-editor/src/extensions/paste-files/lib/index.ts  packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts  packages/slate-editor/src/extensions/paste-images/lib/index.ts --- .../paste-files/PasteFilesExtension.ts | 21 ++++- .../lib/createDataTransferHandler.ts | 73 +++++++++++++++++ .../src/extensions/paste-files/lib/index.ts | 2 +- .../paste-files/lib/withFilesPasting.ts | 77 ------------------ .../paste-images/PasteImagesExtension.ts | 21 ++++- .../lib/createDataTransferHandler.ts | 75 +++++++++++++++++ .../src/extensions/paste-images/lib/index.ts | 2 +- .../paste-images/lib/withImagesPasting.ts | 81 ------------------- .../src/modules/editor/Editor.tsx | 28 +++++++ 9 files changed, 214 insertions(+), 166 deletions(-) create mode 100644 packages/slate-editor/src/extensions/paste-files/lib/createDataTransferHandler.ts delete mode 100644 packages/slate-editor/src/extensions/paste-files/lib/withFilesPasting.ts create mode 100644 packages/slate-editor/src/extensions/paste-images/lib/createDataTransferHandler.ts delete mode 100644 packages/slate-editor/src/extensions/paste-images/lib/withImagesPasting.ts diff --git a/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts b/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts index dfeb3a28e..ab10da49c 100644 --- a/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts +++ b/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts @@ -1,7 +1,12 @@ import { useRegisterExtension } from '@prezly/slate-commons'; +import { noop } from '@technically/lodash'; +import { useMemo } from 'react'; import type { Editor } from 'slate'; +import { useSlateStatic } from 'slate-react'; -import { withFilesPasting } from './lib'; +import { useLatest } from '#lib'; + +import { createDataTransferHandler } from './lib'; export const EXTENSION_ID = 'PasteFilesExtension'; @@ -9,9 +14,19 @@ export interface Parameters { onFilesPasted?: (editor: Editor, files: File[]) => void; } -export function PasteFilesExtension({ onFilesPasted }: Parameters = {}) { +export function PasteFilesExtension({ onFilesPasted = noop }: Parameters = {}) { + const editor = useSlateStatic(); + const callbacks = useLatest({ onFilesPasted }); + const insertData = useMemo(() => { + return createDataTransferHandler(editor, { + onFilesPasted: (editor, files) => { + callbacks.current.onFilesPasted(editor, files); + }, + }); + }, []); + return useRegisterExtension({ id: EXTENSION_ID, - withOverrides: withFilesPasting({ onFilesPasted }), + insertData, }); } diff --git a/packages/slate-editor/src/extensions/paste-files/lib/createDataTransferHandler.ts b/packages/slate-editor/src/extensions/paste-files/lib/createDataTransferHandler.ts new file mode 100644 index 000000000..f825fc4f4 --- /dev/null +++ b/packages/slate-editor/src/extensions/paste-files/lib/createDataTransferHandler.ts @@ -0,0 +1,73 @@ +/* eslint-disable no-param-reassign */ + +import type { DataTransferHandler } from '@prezly/slate-commons'; +import type { PrezlyFileInfo } from '@prezly/uploadcare'; +import { toProgressPromise, UPLOADCARE_FILE_DATA_KEY, UploadcareFile } from '@prezly/uploadcare'; +import uploadcare from '@prezly/uploadcare-widget'; +import { noop } from '@technically/lodash'; +import type { Editor } from 'slate'; + +import { filterDataTransferFiles, isFilesOnlyDataTransfer } from '#lib'; + +import { IMAGE_TYPES } from '#extensions/image'; +import { insertPlaceholders, PlaceholderNode, PlaceholdersManager } from '#extensions/placeholders'; + +interface Parameters { + onFilesPasted?: (editor: Editor, files: File[]) => void; +} + +export function createDataTransferHandler( + editor: Editor, + { onFilesPasted = noop }: Parameters, +): DataTransferHandler { + return (dataTransfer, next) => { + if (!isFilesOnlyDataTransfer(dataTransfer)) { + // Handle images, if the pasted content is containing files only. + return next(dataTransfer); + } + + const files = Array.from(dataTransfer.files).filter(isAttachmentFile); + + if (files.length === 0) { + return next(dataTransfer); + } + + onFilesPasted(editor, files); + + const placeholders = insertPlaceholders(editor, files.length, { + type: PlaceholderNode.Type.ATTACHMENT, + }); + + files.forEach((file, i) => { + const filePromise = uploadcare.fileFrom('object', file); + const uploading = toProgressPromise(filePromise).then((fileInfo: PrezlyFileInfo) => { + const file = UploadcareFile.createFromUploadcareWidgetPayload(fileInfo); + const caption = fileInfo[UPLOADCARE_FILE_DATA_KEY]?.caption || ''; + return { + file: file.toPrezlyStoragePayload(), + caption, + trigger: 'paste' as const, + }; + }); + PlaceholdersManager.register( + PlaceholderNode.Type.ATTACHMENT, + placeholders[i].uuid, + uploading, + ); + }); + + const filteredDataTransfer = withoutFiles(dataTransfer); + + if (filteredDataTransfer.files.length > 0) { + next(filteredDataTransfer); + } + }; +} + +function isAttachmentFile(file: File) { + return !IMAGE_TYPES.includes(file.type); +} + +function withoutFiles(dataTransfer: DataTransfer): DataTransfer { + return filterDataTransferFiles(dataTransfer, (file) => !isAttachmentFile(file)); +} diff --git a/packages/slate-editor/src/extensions/paste-files/lib/index.ts b/packages/slate-editor/src/extensions/paste-files/lib/index.ts index 1b75a2e01..f1e3120c2 100644 --- a/packages/slate-editor/src/extensions/paste-files/lib/index.ts +++ b/packages/slate-editor/src/extensions/paste-files/lib/index.ts @@ -1 +1 @@ -export { withFilesPasting } from './withFilesPasting'; +export { createDataTransferHandler } from './createDataTransferHandler'; diff --git a/packages/slate-editor/src/extensions/paste-files/lib/withFilesPasting.ts b/packages/slate-editor/src/extensions/paste-files/lib/withFilesPasting.ts deleted file mode 100644 index da9c5119b..000000000 --- a/packages/slate-editor/src/extensions/paste-files/lib/withFilesPasting.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { PrezlyFileInfo } from '@prezly/uploadcare'; -import { toProgressPromise, UPLOADCARE_FILE_DATA_KEY, UploadcareFile } from '@prezly/uploadcare'; -import uploadcare from '@prezly/uploadcare-widget'; -import { noop } from '@technically/lodash'; -import type { Editor } from 'slate'; - -import { filterDataTransferFiles, isFilesOnlyDataTransfer } from '#lib'; - -import { IMAGE_TYPES } from '#extensions/image'; -import { insertPlaceholders, PlaceholderNode, PlaceholdersManager } from '#extensions/placeholders'; - -interface Parameters { - onFilesPasted?: (editor: Editor, files: File[]) => void; -} - -export function withFilesPasting({ onFilesPasted = noop }: Parameters = {}) { - return (editor: T): T => { - const parent = { - insertData: editor.insertData, - }; - - editor.insertData = (dataTransfer: DataTransfer) => { - if (!isFilesOnlyDataTransfer(dataTransfer)) { - // Handle images, if the pasted content is containing files only. - return parent.insertData(dataTransfer); - } - - const files = Array.from(dataTransfer.files).filter(isAttachmentFile); - - if (files.length === 0) { - parent.insertData(dataTransfer); - } - - onFilesPasted(editor, files); - - const placeholders = insertPlaceholders(editor, files.length, { - type: PlaceholderNode.Type.ATTACHMENT, - }); - - files.forEach((file, i) => { - const filePromise = uploadcare.fileFrom('object', file); - const uploading = toProgressPromise(filePromise).then( - (fileInfo: PrezlyFileInfo) => { - const file = UploadcareFile.createFromUploadcareWidgetPayload(fileInfo); - const caption = fileInfo[UPLOADCARE_FILE_DATA_KEY]?.caption || ''; - return { - file: file.toPrezlyStoragePayload(), - caption, - trigger: 'paste' as const, - }; - }, - ); - PlaceholdersManager.register( - PlaceholderNode.Type.ATTACHMENT, - placeholders[i].uuid, - uploading, - ); - }); - - const filteredDataTransfer = withoutFiles(dataTransfer); - - if (filteredDataTransfer.files.length > 0) { - parent.insertData(filteredDataTransfer); - } - }; - - return editor; - }; -} - -function isAttachmentFile(file: File) { - return !IMAGE_TYPES.includes(file.type); -} - -function withoutFiles(dataTransfer: DataTransfer): DataTransfer { - return filterDataTransferFiles(dataTransfer, (file) => !isAttachmentFile(file)); -} diff --git a/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts b/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts index 08acf48db..7346046fe 100644 --- a/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts +++ b/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts @@ -1,7 +1,12 @@ import { useRegisterExtension } from '@prezly/slate-commons'; +import { noop } from '@technically/lodash'; +import { useMemo } from 'react'; import type { Editor } from 'slate'; +import { useSlateStatic } from 'slate-react'; -import { withImagesPasting } from './lib'; +import { useLatest } from '#lib'; + +import { createDataTransferHandler } from './lib'; export const EXTENSION_ID = 'PasteImagesExtension'; @@ -9,9 +14,19 @@ export interface Parameters { onImagesPasted?: (editor: Editor, images: File[]) => void; } -export function PasteImagesExtension({ onImagesPasted }: Parameters = {}) { +export function PasteImagesExtension({ onImagesPasted = noop }: Parameters = {}) { + const editor = useSlateStatic(); + const callbacks = useLatest({ onImagesPasted }); + const insertData = useMemo(() => { + return createDataTransferHandler(editor, { + onImagesPasted: (editor, images) => { + callbacks.current.onImagesPasted(editor, images); + }, + }); + }, []); + return useRegisterExtension({ id: EXTENSION_ID, - withOverrides: withImagesPasting({ onImagesPasted }), + insertData, }); } diff --git a/packages/slate-editor/src/extensions/paste-images/lib/createDataTransferHandler.ts b/packages/slate-editor/src/extensions/paste-images/lib/createDataTransferHandler.ts new file mode 100644 index 000000000..d9600255a --- /dev/null +++ b/packages/slate-editor/src/extensions/paste-images/lib/createDataTransferHandler.ts @@ -0,0 +1,75 @@ +/* eslint-disable no-param-reassign */ + +import type { DataTransferHandler } from '@prezly/slate-commons'; +import type { PrezlyFileInfo } from '@prezly/uploadcare'; +import { toProgressPromise, UPLOADCARE_FILE_DATA_KEY, UploadcareImage } from '@prezly/uploadcare'; +import uploadcare from '@prezly/uploadcare-widget'; +import type { Editor } from 'slate'; + +import { filterDataTransferFiles, isFilesOnlyDataTransfer } from '#lib'; + +import { createImage, IMAGE_TYPES } from '#extensions/image'; +import { insertPlaceholders, PlaceholderNode, PlaceholdersManager } from '#extensions/placeholders'; + +export interface Parameters { + onImagesPasted: (editor: Editor, images: File[]) => void; +} + +export function createDataTransferHandler( + editor: Editor, + { onImagesPasted }: Parameters, +): DataTransferHandler { + return (dataTransfer, next) => { + if (!isFilesOnlyDataTransfer(dataTransfer)) { + // Handle images, if the pasted content is containing files only. + return next(dataTransfer); + } + + const images = Array.from(dataTransfer.files).filter(isImageFile); + + if (images.length === 0) { + return next(dataTransfer); + } + + onImagesPasted(editor, images); + + const placeholders = insertPlaceholders(editor, images.length, { + type: PlaceholderNode.Type.IMAGE, + }); + + images.forEach((file, i) => { + const filePromise = uploadcare.fileFrom('object', file); + const uploading = toProgressPromise(filePromise).then((fileInfo: PrezlyFileInfo) => { + const image = UploadcareImage.createFromUploadcareWidgetPayload(fileInfo); + const caption: string = fileInfo[UPLOADCARE_FILE_DATA_KEY]?.caption || ''; + return { + image: createImage({ + file: image.toPrezlyStoragePayload(), + children: [{ text: caption }], + }), + operation: 'add' as const, + trigger: 'paste' as const, + }; + }); + PlaceholdersManager.register( + PlaceholderNode.Type.IMAGE, + placeholders[i].uuid, + uploading, + ); + }); + + const filteredDataTransfer = withoutImages(dataTransfer); + + if (filteredDataTransfer.files.length > 0) { + next(filteredDataTransfer); + } + }; +} + +function isImageFile(file: File) { + return IMAGE_TYPES.includes(file.type); +} + +function withoutImages(dataTransfer: DataTransfer): DataTransfer { + return filterDataTransferFiles(dataTransfer, (file) => !isImageFile(file)); +} diff --git a/packages/slate-editor/src/extensions/paste-images/lib/index.ts b/packages/slate-editor/src/extensions/paste-images/lib/index.ts index e3a2024a6..f1e3120c2 100644 --- a/packages/slate-editor/src/extensions/paste-images/lib/index.ts +++ b/packages/slate-editor/src/extensions/paste-images/lib/index.ts @@ -1 +1 @@ -export { withImagesPasting } from './withImagesPasting'; +export { createDataTransferHandler } from './createDataTransferHandler'; diff --git a/packages/slate-editor/src/extensions/paste-images/lib/withImagesPasting.ts b/packages/slate-editor/src/extensions/paste-images/lib/withImagesPasting.ts deleted file mode 100644 index 5f5761a46..000000000 --- a/packages/slate-editor/src/extensions/paste-images/lib/withImagesPasting.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { PrezlyFileInfo } from '@prezly/uploadcare'; -import { toProgressPromise, UPLOADCARE_FILE_DATA_KEY, UploadcareImage } from '@prezly/uploadcare'; -import uploadcare from '@prezly/uploadcare-widget'; -import { noop } from '@technically/lodash'; -import type { Editor } from 'slate'; - -import { filterDataTransferFiles, isFilesOnlyDataTransfer } from '#lib'; - -import { createImage, IMAGE_TYPES } from '#extensions/image'; - -import { insertPlaceholders, PlaceholderNode, PlaceholdersManager } from '../../placeholders'; - -export interface Parameters { - onImagesPasted?: (editor: Editor, images: File[]) => void; -} - -export function withImagesPasting({ onImagesPasted = noop }: Parameters = {}) { - return (editor: T): T => { - const parent = { - insertData: editor.insertData, - }; - - editor.insertData = (dataTransfer: DataTransfer) => { - if (!isFilesOnlyDataTransfer(dataTransfer)) { - // Handle images, if the pasted content is containing files only. - return parent.insertData(dataTransfer); - } - - const images = Array.from(dataTransfer.files).filter(isImageFile); - - if (images.length === 0) { - parent.insertData(dataTransfer); - } - - onImagesPasted(editor, images); - - const placeholders = insertPlaceholders(editor, images.length, { - type: PlaceholderNode.Type.IMAGE, - }); - - images.forEach((file, i) => { - const filePromise = uploadcare.fileFrom('object', file); - const uploading = toProgressPromise(filePromise).then( - (fileInfo: PrezlyFileInfo) => { - const image = UploadcareImage.createFromUploadcareWidgetPayload(fileInfo); - const caption: string = fileInfo[UPLOADCARE_FILE_DATA_KEY]?.caption || ''; - return { - image: createImage({ - file: image.toPrezlyStoragePayload(), - children: [{ text: caption }], - }), - operation: 'add' as const, - trigger: 'paste' as const, - }; - }, - ); - PlaceholdersManager.register( - PlaceholderNode.Type.IMAGE, - placeholders[i].uuid, - uploading, - ); - }); - - const filteredDataTransfer = withoutImages(dataTransfer); - - if (filteredDataTransfer.files.length > 0) { - parent.insertData(filteredDataTransfer); - } - }; - - return editor; - }; -} - -function isImageFile(file: File) { - return IMAGE_TYPES.includes(file.type); -} - -function withoutImages(dataTransfer: DataTransfer): DataTransfer { - return filterDataTransferFiles(dataTransfer, (file) => !isImageFile(file)); -} diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 6fcc4a763..165150b37 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -26,7 +26,9 @@ import { useFunction, useGetSet, useSize } from '#lib'; import { insertButtonBlock } from '#extensions/button-block'; import { FlashNodesExtension } from '#extensions/flash-nodes'; import { FloatingAddMenuExtension, type Option } from '#extensions/floating-add-menu'; +import { PasteFilesExtension } from '#extensions/paste-files'; import { PasteHtmlContentExtension } from '#extensions/paste-html-content'; +import { PasteImagesExtension } from '#extensions/paste-images'; import { PasteSlateContentExtension } from '#extensions/paste-slate-content'; import { PasteTrackingExtension } from '#extensions/paste-tracking'; import { insertPlaceholder, PlaceholderNode } from '#extensions/placeholders'; @@ -740,6 +742,32 @@ export const Editor = forwardRef((props, forwardedRef) = }} /> + {withAttachments && ( + { + EventsEditor.dispatchEvent(editor, 'files-pasted', { + filesCount: files.length, + isEmpty: EditorCommands.isEmpty(editor), + }); + }} + /> + )} + + {withImages && ( + { + EventsEditor.dispatchEvent( + editor, + 'images-pasted', + { + imagesCount: images.length, + isEmpty: EditorCommands.isEmpty(editor), + }, + ); + }} + /> + )} + { if (withBlockquotes && isQuoteNode(node)) { From 07a8cd685333237625080560902aa7697fefcebc Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 17:45:40 +0300 Subject: [PATCH 33/54] [CARE-1802] Replace `withResetFormattingOnBreak` editor mutator with new `insertBreak` extension hooks --- .../src/commands/getCurrentNode.ts | 14 ++ packages/slate-commons/src/commands/index.ts | 1 + .../src/extensions/ExtensionsEditor.ts | 9 ++ .../src/extensions/useRegisterExtension.ts | 2 + packages/slate-commons/src/types/Extension.ts | 4 +- .../src/types/LineBreakHandler.ts | 5 + packages/slate-commons/src/types/index.ts | 1 + .../blockquote/BlockquoteExtension.tsx | 15 +- .../extensions/heading/HeadingExtension.tsx | 14 +- packages/slate-editor/src/lib/index.ts | 2 +- .../src/lib/resetFormattingOnBreak.ts | 13 ++ .../lib/withResetFormattingOnBreak.test.tsx | 142 ------------------ .../src/lib/withResetFormattingOnBreak.ts | 26 ---- 13 files changed, 71 insertions(+), 177 deletions(-) create mode 100644 packages/slate-commons/src/commands/getCurrentNode.ts create mode 100644 packages/slate-commons/src/types/LineBreakHandler.ts create mode 100644 packages/slate-editor/src/lib/resetFormattingOnBreak.ts delete mode 100644 packages/slate-editor/src/lib/withResetFormattingOnBreak.test.tsx delete mode 100644 packages/slate-editor/src/lib/withResetFormattingOnBreak.ts diff --git a/packages/slate-commons/src/commands/getCurrentNode.ts b/packages/slate-commons/src/commands/getCurrentNode.ts new file mode 100644 index 000000000..ca0b4a727 --- /dev/null +++ b/packages/slate-commons/src/commands/getCurrentNode.ts @@ -0,0 +1,14 @@ +import type { Node } from 'slate'; +import { Editor } from 'slate'; + +import { isSelectionValid } from './isSelectionValid'; + +export function getCurrentNode(editor: Editor): Node | null { + if (!editor.selection || !isSelectionValid(editor)) { + return null; + } + + const entry = Editor.node(editor, editor.selection, { depth: 1 }); + + return entry ? entry[0] : null; +} diff --git a/packages/slate-commons/src/commands/index.ts b/packages/slate-commons/src/commands/index.ts index e4e605b84..796abf385 100644 --- a/packages/slate-commons/src/commands/index.ts +++ b/packages/slate-commons/src/commands/index.ts @@ -5,6 +5,7 @@ export { findDescendants } from './findDescendants'; export { focus } from './focus'; export { getCurrentDomNode } from './getCurrentDomNode'; export { getCurrentNodeEntry } from './getCurrentNodeEntry'; +export { getCurrentNode } from './getCurrentNode'; export { getEditorRange } from './getEditorRange'; export { getNodePath } from './getNodePath'; export { getPrevChars } from './getPrevChars'; diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index 20192f35f..af533409b 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -40,6 +40,7 @@ export function withExtensions( const parent = { isInline: editor.isInline, isVoid: editor.isVoid, + insertBreak: editor.insertBreak, insertData: editor.insertData, normalizeNode: editor.normalizeNode, }; @@ -80,6 +81,14 @@ export function withExtensions( } return false; }, + insertBreak() { + for (const extension of extensionsEditor.extensions) { + if (extension.insertBreak?.()) { + return; + } + } + parent.insertBreak(); + }, insertData(dataTransfer) { const handlers = extensionsEditor.extensions .map((ext) => ext.insertData) diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts index c783a4b1b..2dd5c1a42 100644 --- a/packages/slate-commons/src/extensions/useRegisterExtension.ts +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -15,6 +15,7 @@ export function useRegisterExtension(extension: Extension): null { isInline, isRichBlock, isVoid, + insertBreak, insertData, normalizeNode, onDOMBeforeInput, @@ -41,6 +42,7 @@ export function useRegisterExtension(extension: Extension): null { isInline, isRichBlock, isVoid, + insertBreak, insertData, normalizeNode, onDOMBeforeInput, diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index b1d00aadb..346407cd0 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -3,6 +3,7 @@ import type { Element, Node } from 'slate'; import type { DataTransferHandler } from './DataTransferHandler'; import type { DecorateFactory } from './DecorateFactory'; import type { DeserializeHtml } from './DeserializeHtml'; +import type { LineBreakHandler } from './LineBreakHandler'; import type { Normalize } from './Normalize'; import type { OnDOMBeforeInput } from './OnDOMBeforeInput'; import type { OnKeyDown } from './OnKeyDown'; @@ -15,9 +16,10 @@ export interface Extension { id: string; decorate?: DecorateFactory; // OK deserialize?: DeserializeHtml; // OK + insertBreak?: LineBreakHandler; /** * Hook into ReactEditor's `insertData()` method. - * Call `next()` to allow other extensions (or the editor) handling the event. + * Call `next()` to allow other extensions (or the editor) handling the call. */ insertData?: DataTransferHandler; // OK /** diff --git a/packages/slate-commons/src/types/LineBreakHandler.ts b/packages/slate-commons/src/types/LineBreakHandler.ts new file mode 100644 index 000000000..417b3b933 --- /dev/null +++ b/packages/slate-commons/src/types/LineBreakHandler.ts @@ -0,0 +1,5 @@ +/** + * Hook into ReactEditor's `insertBreak()` method. + * Return `true` to prevent handling this action by other extensions or the editor. + */ +export type LineBreakHandler = () => void; diff --git a/packages/slate-commons/src/types/index.ts b/packages/slate-commons/src/types/index.ts index 9aee17227..95b5b9de3 100644 --- a/packages/slate-commons/src/types/index.ts +++ b/packages/slate-commons/src/types/index.ts @@ -1,3 +1,4 @@ +export type { LineBreakHandler } from './LineBreakHandler'; export type { DataTransferHandler } from './DataTransferHandler'; export type { Decorate } from './Decorate'; export type { DecorateFactory } from './DecorateFactory'; diff --git a/packages/slate-editor/src/extensions/blockquote/BlockquoteExtension.tsx b/packages/slate-editor/src/extensions/blockquote/BlockquoteExtension.tsx index 341f39117..ea960e3f6 100644 --- a/packages/slate-editor/src/extensions/blockquote/BlockquoteExtension.tsx +++ b/packages/slate-editor/src/extensions/blockquote/BlockquoteExtension.tsx @@ -1,8 +1,9 @@ import { useRegisterExtension } from '@prezly/slate-commons'; import { isQuoteNode, QUOTE_NODE_TYPE } from '@prezly/slate-types'; -import React from 'react'; +import React, { useCallback } from 'react'; +import { useSlateStatic } from 'slate-react'; -import { onBackspaceResetFormattingAtDocumentStart, withResetFormattingOnBreak } from '#lib'; +import { onBackspaceResetFormattingAtDocumentStart, resetFormattingOnBreak } from '#lib'; import { composeElementDeserializer } from '#modules/html-deserialization'; @@ -12,6 +13,12 @@ import { normalizeRedundantAttributes } from './lib'; export const EXTENSION_ID = 'BlockquoteExtension'; export function BlockquoteExtension() { + const editor = useSlateStatic(); + + const insertBreak = useCallback(() => { + return resetFormattingOnBreak(editor, isQuoteNode); + }, []); + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { @@ -20,7 +27,8 @@ export function BlockquoteExtension() { BLOCKQUOTE: () => ({ type: QUOTE_NODE_TYPE }), }), }, - normalizeNode: [normalizeRedundantAttributes], + insertBreak, + normalizeNode: normalizeRedundantAttributes, onKeyDown(event, editor) { return onBackspaceResetFormattingAtDocumentStart(editor, isQuoteNode, event); }, @@ -34,6 +42,5 @@ export function BlockquoteExtension() { } return undefined; }, - withOverrides: withResetFormattingOnBreak(isQuoteNode), }); } diff --git a/packages/slate-editor/src/extensions/heading/HeadingExtension.tsx b/packages/slate-editor/src/extensions/heading/HeadingExtension.tsx index 0b23b12a8..42bfbc8b6 100644 --- a/packages/slate-editor/src/extensions/heading/HeadingExtension.tsx +++ b/packages/slate-editor/src/extensions/heading/HeadingExtension.tsx @@ -1,3 +1,4 @@ +import type { LineBreakHandler } from '@prezly/slate-commons'; import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import type { HeadingNode, HeadingRole } from '@prezly/slate-types'; import { @@ -7,10 +8,11 @@ import { isSubtitleHeadingNode, isTitleHeadingNode, } from '@prezly/slate-types'; -import React from 'react'; +import React, { useCallback } from 'react'; import type { Node } from 'slate'; +import { useSlateStatic } from 'slate-react'; -import { onBackspaceResetFormattingAtDocumentStart, withResetFormattingOnBreak } from '#lib'; +import { onBackspaceResetFormattingAtDocumentStart, resetFormattingOnBreak } from '#lib'; import { composeElementDeserializer } from '#modules/html-deserialization'; @@ -20,6 +22,12 @@ import { normalizeRedundantAttributes, onTabSwitchBlock, parseHeadingElement } f export const EXTENSION_ID = 'HeadingExtension'; export function HeadingExtension() { + const editor = useSlateStatic(); + + const insertBreak = useCallback(() => { + return resetFormattingOnBreak(editor, isHeadingNode); + }, []); + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { @@ -34,6 +42,7 @@ export function HeadingExtension() { H6: () => ({ type: HEADING_2_NODE_TYPE }), }), }, + insertBreak, normalizeNode: [normalizeRedundantAttributes], onKeyDown(event, editor) { return ( @@ -51,7 +60,6 @@ export function HeadingExtension() { } return undefined; }, - withOverrides: withResetFormattingOnBreak(isHeadingNode), }); } diff --git a/packages/slate-editor/src/lib/index.ts b/packages/slate-editor/src/lib/index.ts index 6884f448f..54213cbe2 100644 --- a/packages/slate-editor/src/lib/index.ts +++ b/packages/slate-editor/src/lib/index.ts @@ -23,10 +23,10 @@ export { isUrl } from './isUrl'; export { mergeRefs } from './mergeRefs'; export { Observable } from './Observable'; export { onBackspaceResetFormattingAtDocumentStart } from './onBackspaceResetFormattingAtDocumentStart'; +export { resetFormattingOnBreak } from './resetFormattingOnBreak'; export { scrollIntoView } from './scrollIntoView'; export { scrollTo } from './scrollTo'; export * from './isDeletingEvent'; export * from './stripTags'; export * as utils from './utils'; export { HREF_REGEXP, URL_WITH_OPTIONAL_PROTOCOL_REGEXP, normalizeHref, matchUrls } from './urls'; -export { withResetFormattingOnBreak } from './withResetFormattingOnBreak'; diff --git a/packages/slate-editor/src/lib/resetFormattingOnBreak.ts b/packages/slate-editor/src/lib/resetFormattingOnBreak.ts new file mode 100644 index 000000000..8e4e74f3b --- /dev/null +++ b/packages/slate-editor/src/lib/resetFormattingOnBreak.ts @@ -0,0 +1,13 @@ +import { EditorCommands } from '@prezly/slate-commons'; +import type { Editor, Node } from 'slate'; + +export function resetFormattingOnBreak(editor: Editor, match: (node: Node) => boolean) { + const currentNode = EditorCommands.getCurrentNode(editor); + + if (currentNode && match(currentNode) && EditorCommands.isSelectionAtBlockEnd(editor)) { + EditorCommands.insertEmptyParagraph(editor); + return true; // handled + } + + return false; +} diff --git a/packages/slate-editor/src/lib/withResetFormattingOnBreak.test.tsx b/packages/slate-editor/src/lib/withResetFormattingOnBreak.test.tsx deleted file mode 100644 index 251c66e67..000000000 --- a/packages/slate-editor/src/lib/withResetFormattingOnBreak.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/** @jsx hyperscript */ - -import { Editor } from 'slate'; - -import { hyperscript } from '../hyperscript'; - -describe('withResetFormattingOnBreak', () => { - it('Inserts a new default paragraph when inserting a break inside a heading', () => { - const editor = ( - -

- - lorem ipsum - - -

-
- ) as unknown as Editor; - - const expected = ( - -

- lorem ipsum -

- - - - - -
- ) as unknown as Editor; - - Editor.insertBreak(editor); - - expect(editor.children).toEqual(expected.children); - expect(editor.selection).toEqual(expected.selection); - }); - - it('Inserts a new default paragraph when inserting a break inside a paragraph', () => { - const editor = ( - - - - lorem ipsum - - - - - ) as unknown as Editor; - - const expected = ( - - - lorem ipsum - - - - - - - - ) as unknown as Editor; - - Editor.insertBreak(editor); - - expect(editor.children).toEqual(expected.children); - expect(editor.selection).toEqual(expected.selection); - }); - - it('Inserts a new list item when inserting a break inside a list item', () => { - const editor = ( - -
    -
  • - - - lorem ipsum - - - -
  • -
-
- ) as unknown as Editor; - - const expected = ( - -
    -
  • - - lorem ipsum - -
  • -
  • - - - - - -
  • -
-
- ) as unknown as Editor; - - Editor.insertBreak(editor); - - expect(editor.children).toEqual(expected.children); - expect(editor.selection).toEqual(expected.selection); - }); - - it('Inserts a new default paragraph when inserting a break while selecting a heading', () => { - const editor = ( - -

- - lorem - ipsum - - -

-
- ) as unknown as Editor; - - const expected = ( - -

- lorem -

- - - - - -
- ) as unknown as Editor; - - Editor.insertBreak(editor); - - expect(editor.children).toEqual(expected.children); - expect(editor.selection).toEqual(expected.selection); - }); -}); diff --git a/packages/slate-editor/src/lib/withResetFormattingOnBreak.ts b/packages/slate-editor/src/lib/withResetFormattingOnBreak.ts deleted file mode 100644 index 32d4d5213..000000000 --- a/packages/slate-editor/src/lib/withResetFormattingOnBreak.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { EditorCommands } from '@prezly/slate-commons'; -import type { Editor, Node } from 'slate'; - -export function withResetFormattingOnBreak(match: (node: Node) => boolean) { - return function (editor: T): T { - const { insertBreak } = editor; - - editor.insertBreak = () => { - /** - * The `currentNode` is the top-level block, which means when the - * cursor is at a list item, the type is bulleted or numbered list. - * This is why we have to perform `isList` check and not `isListItem`. - */ - const [currentNode] = EditorCommands.getCurrentNodeEntry(editor) || []; - - if (currentNode && match(currentNode) && EditorCommands.isSelectionAtBlockEnd(editor)) { - EditorCommands.insertEmptyParagraph(editor); - return; - } - - insertBreak(); - }; - - return editor; - }; -} From a3627f270a62a75a8f07d66a55e9163ff161893b Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 17:47:55 +0300 Subject: [PATCH 34/54] [CARE-1802] Add a todo --- packages/slate-commons/src/extensions/useRegisterExtension.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts index 2dd5c1a42..31a41b3b4 100644 --- a/packages/slate-commons/src/extensions/useRegisterExtension.ts +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -31,6 +31,8 @@ export function useRegisterExtension(extension: Extension): null { throw new Error(`Unsupported keys passed: ${Object.keys(rest).join(', ')}.`); } + // FIXME: [Optimization] Replace callback dependencies with refs + useEffect( () => manager.register(extension), [ From 454673db86f2259a35d354b2d5a5be4403be831b Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 17:52:58 +0300 Subject: [PATCH 35/54] [CARE-1802] Rewrite LinksExtension autolinking handling to `insertData()` hooks --- .../inline-links/InlineLinksExtension.tsx | 18 +++++-- .../handlePastedContentAutolinking.ts | 35 +++++++++++++ .../behaviour/handleSelectionAutolinking.ts | 43 ++++++++++++++++ .../inline-links/behaviour/index.ts | 4 +- .../behaviour/withPastedContentAutolinking.ts | 41 --------------- .../behaviour/withSelectionAutolinking.ts | 51 ------------------- 6 files changed, 93 insertions(+), 99 deletions(-) create mode 100644 packages/slate-editor/src/extensions/inline-links/behaviour/handlePastedContentAutolinking.ts create mode 100644 packages/slate-editor/src/extensions/inline-links/behaviour/handleSelectionAutolinking.ts delete mode 100644 packages/slate-editor/src/extensions/inline-links/behaviour/withPastedContentAutolinking.ts delete mode 100644 packages/slate-editor/src/extensions/inline-links/behaviour/withSelectionAutolinking.ts diff --git a/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx b/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx index 327d000bb..c84f89ac1 100644 --- a/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx +++ b/packages/slate-editor/src/extensions/inline-links/InlineLinksExtension.tsx @@ -1,13 +1,14 @@ +import type { DataTransferHandler } from '@prezly/slate-commons'; import { createDeserializeElement, useRegisterExtension } from '@prezly/slate-commons'; import type { LinkNode } from '@prezly/slate-types'; import { isLinkNode, LINK_NODE_TYPE } from '@prezly/slate-types'; -import { flow } from '@technically/lodash'; -import React from 'react'; +import React, { useCallback } from 'react'; import type { RenderElementProps } from 'slate-react'; +import { useSlateStatic } from 'slate-react'; import { composeElementDeserializer } from '#modules/html-deserialization'; -import { withPastedContentAutolinking, withSelectionAutolinking } from './behaviour'; +import { handlePastedContentAutolinking, handleSelectionAutolinking } from './behaviour'; import { LinkElement } from './components'; import { escapeLinksBoundaries, @@ -20,6 +21,14 @@ import { export const EXTENSION_ID = 'InlineLinksExtension'; export function InlineLinksExtension() { + const editor = useSlateStatic(); + + const insertData = useCallback((dataTransfer, next) => { + handlePastedContentAutolinking(editor, dataTransfer) || + handleSelectionAutolinking(editor, dataTransfer) || + next(dataTransfer); + }, []); + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { @@ -39,6 +48,7 @@ export function InlineLinksExtension() { }), }, isInline: isLinkNode, + insertData, normalizeNode: [normalizeEmptyLink, normalizeNestedLink, normalizeRedundantLinkAttributes], onKeyDown: function (event, editor) { escapeLinksBoundaries(event, editor); @@ -54,7 +64,5 @@ export function InlineLinksExtension() { return undefined; }, - withOverrides: (editor) => - flow([withPastedContentAutolinking, withSelectionAutolinking])(editor), }); } diff --git a/packages/slate-editor/src/extensions/inline-links/behaviour/handlePastedContentAutolinking.ts b/packages/slate-editor/src/extensions/inline-links/behaviour/handlePastedContentAutolinking.ts new file mode 100644 index 000000000..d3c1f0311 --- /dev/null +++ b/packages/slate-editor/src/extensions/inline-links/behaviour/handlePastedContentAutolinking.ts @@ -0,0 +1,35 @@ +import { EditorCommands } from '@prezly/slate-commons'; +import { isLinkNode } from '@prezly/slate-types'; +import { Editor, Transforms } from 'slate'; + +import { isUrl } from '#lib'; + +import { autolinkPlaintext, createLink } from '../lib'; + +/** + * Automatically link pasted content if it's a URL. // See DEV-11519 + */ +export function handlePastedContentAutolinking(editor: Editor, data: DataTransfer): boolean { + const pasted = data.getData('text'); + + const hasHtml = Boolean(data.getData('text/html')); + + if (isUrl(pasted) && EditorCommands.isSelectionEmpty(editor)) { + const isInsideLink = Array.from(Editor.nodes(editor, { match: isLinkNode })).length > 0; + + if (!isInsideLink) { + Transforms.insertNodes(editor, createLink({ href: pasted })); + return true; // handled + } + } + + if (!hasHtml) { + const autolinked = autolinkPlaintext(pasted); + if (autolinked) { + Transforms.insertNodes(editor, autolinked); + return true; // handled; + } + } + + return false; +} diff --git a/packages/slate-editor/src/extensions/inline-links/behaviour/handleSelectionAutolinking.ts b/packages/slate-editor/src/extensions/inline-links/behaviour/handleSelectionAutolinking.ts new file mode 100644 index 000000000..3dedbc9b7 --- /dev/null +++ b/packages/slate-editor/src/extensions/inline-links/behaviour/handleSelectionAutolinking.ts @@ -0,0 +1,43 @@ +import { EditorCommands } from '@prezly/slate-commons'; +import { + HEADING_1_NODE_TYPE, + HEADING_2_NODE_TYPE, + isElementNode, + PARAGRAPH_NODE_TYPE, + QUOTE_NODE_TYPE, +} from '@prezly/slate-types'; +import { Editor } from 'slate'; + +import { isUrl, normalizeHref } from '#lib'; + +import { unwrapLink, wrapInLink } from '../lib'; + +/** + * Automatically link selected text if the pasted content is a URL. + */ +export function handleSelectionAutolinking(editor: Editor, data: DataTransfer) { + const href = data.getData('text'); + + if (isUrl(href) && !EditorCommands.isSelectionEmpty(editor)) { + const nodes = Array.from(Editor.nodes(editor, { match: isElementNode, mode: 'highest' })); + + const isOnlyAllowedNodes = nodes.every(([node]) => + isElementNode(node, [ + PARAGRAPH_NODE_TYPE, + QUOTE_NODE_TYPE, + HEADING_1_NODE_TYPE, + HEADING_2_NODE_TYPE, + ]), + ); + + if (isOnlyAllowedNodes) { + // Unwrap any links in the current selection, otherwise multiple links + // would overlap + unwrapLink(editor); + wrapInLink(editor, { href: normalizeHref(href), new_tab: true }); + return true; // handled + } + } + + return false; +} diff --git a/packages/slate-editor/src/extensions/inline-links/behaviour/index.ts b/packages/slate-editor/src/extensions/inline-links/behaviour/index.ts index 67ed9a71e..ae882e451 100644 --- a/packages/slate-editor/src/extensions/inline-links/behaviour/index.ts +++ b/packages/slate-editor/src/extensions/inline-links/behaviour/index.ts @@ -1,2 +1,2 @@ -export { withPastedContentAutolinking } from './withPastedContentAutolinking'; -export { withSelectionAutolinking } from './withSelectionAutolinking'; +export { handlePastedContentAutolinking } from './handlePastedContentAutolinking'; +export { handleSelectionAutolinking } from './handleSelectionAutolinking'; diff --git a/packages/slate-editor/src/extensions/inline-links/behaviour/withPastedContentAutolinking.ts b/packages/slate-editor/src/extensions/inline-links/behaviour/withPastedContentAutolinking.ts deleted file mode 100644 index 10a9eb345..000000000 --- a/packages/slate-editor/src/extensions/inline-links/behaviour/withPastedContentAutolinking.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { EditorCommands } from '@prezly/slate-commons'; -import { isLinkNode } from '@prezly/slate-types'; -import { Editor, Transforms } from 'slate'; - -import { isUrl } from '#lib'; - -import { autolinkPlaintext, createLink } from '../lib'; - -/** - * Automatically link pasted content if it's a URL. // See DEV-11519 - */ -export function withPastedContentAutolinking(editor: T): T { - const { insertData } = editor; - - editor.insertData = (data) => { - const pasted = data.getData('text'); - - const hasHtml = Boolean(data.getData('text/html')); - - if (isUrl(pasted) && EditorCommands.isSelectionEmpty(editor)) { - const isInsideLink = Array.from(Editor.nodes(editor, { match: isLinkNode })).length > 0; - - if (!isInsideLink) { - Transforms.insertNodes(editor, createLink({ href: pasted })); - return; - } - } - - if (!hasHtml) { - const autolinked = autolinkPlaintext(pasted); - if (autolinked) { - Transforms.insertNodes(editor, autolinked); - return; - } - } - - insertData(data); - }; - - return editor; -} diff --git a/packages/slate-editor/src/extensions/inline-links/behaviour/withSelectionAutolinking.ts b/packages/slate-editor/src/extensions/inline-links/behaviour/withSelectionAutolinking.ts deleted file mode 100644 index 152fe623f..000000000 --- a/packages/slate-editor/src/extensions/inline-links/behaviour/withSelectionAutolinking.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { EditorCommands } from '@prezly/slate-commons'; -import { - HEADING_1_NODE_TYPE, - HEADING_2_NODE_TYPE, - isElementNode, - PARAGRAPH_NODE_TYPE, - QUOTE_NODE_TYPE, -} from '@prezly/slate-types'; -import { Editor } from 'slate'; - -import { isUrl, normalizeHref } from '#lib'; - -import { unwrapLink, wrapInLink } from '../lib'; - -/** - * Automatically link selected text if the pasted content is a URL. - */ -export function withSelectionAutolinking(editor: T): T { - const { insertData } = editor; - - editor.insertData = (data) => { - const href = data.getData('text'); - - if (isUrl(href) && !EditorCommands.isSelectionEmpty(editor)) { - const nodes = Array.from( - Editor.nodes(editor, { match: isElementNode, mode: 'highest' }), - ); - - const isOnlyAllowedNodes = nodes.every(([node]) => - isElementNode(node, [ - PARAGRAPH_NODE_TYPE, - QUOTE_NODE_TYPE, - HEADING_1_NODE_TYPE, - HEADING_2_NODE_TYPE, - ]), - ); - - if (isOnlyAllowedNodes) { - // Unwrap any links in the current selection, otherwise multiple links - // would overlap - unwrapLink(editor); - wrapInLink(editor, { href: normalizeHref(href), new_tab: true }); - return; - } - } - - insertData(data); - }; - - return editor; -} From a5603de532ef0d24d5ad9c9b6cb12eeb8f49524d Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Mon, 18 Sep 2023 18:02:13 +0300 Subject: [PATCH 36/54] [CARE-1802] Convert `withPastedUrlsUnfurling` mutator to `insertData` hook --- .../placeholders/PlaceholdersExtension.tsx | 34 ++++++++-- .../placeholders/behaviour/index.ts | 2 +- .../behaviour/unfurlPastedUrls.ts | 49 +++++++++++++++ .../behaviour/withPastedUrlsUnfurling.ts | 62 ------------------- 4 files changed, 78 insertions(+), 69 deletions(-) create mode 100644 packages/slate-editor/src/extensions/placeholders/behaviour/unfurlPastedUrls.ts delete mode 100644 packages/slate-editor/src/extensions/placeholders/behaviour/withPastedUrlsUnfurling.ts diff --git a/packages/slate-editor/src/extensions/placeholders/PlaceholdersExtension.tsx b/packages/slate-editor/src/extensions/placeholders/PlaceholdersExtension.tsx index 042d785d4..56803eaea 100644 --- a/packages/slate-editor/src/extensions/placeholders/PlaceholdersExtension.tsx +++ b/packages/slate-editor/src/extensions/placeholders/PlaceholdersExtension.tsx @@ -1,8 +1,12 @@ import type { NewsroomRef } from '@prezly/sdk'; +import type { DataTransferHandler } from '@prezly/slate-commons'; import { useRegisterExtension } from '@prezly/slate-commons'; -import React from 'react'; +import React, { useCallback } from 'react'; +import { useSlateStatic } from 'slate-react'; -import { withPastedUrlsUnfurling } from './behaviour'; +import { useLatest } from '#lib'; + +import { unfurlPastedUrls } from './behaviour'; import { AttachmentPlaceholderElement, ContactPlaceholderElement, @@ -112,10 +116,30 @@ export function PlaceholdersExtension({ withStoryBookmarkPlaceholders = false, withStoryEmbedPlaceholders = false, withSocialPostPlaceholders = false, - withPastedUrlsUnfurling: isUnfurlingPastedUrls = false, + withPastedUrlsUnfurling = false, withVideoPlaceholders = false, withWebBookmarkPlaceholders = false, }: Parameters = {}) { + const editor = useSlateStatic(); + const callbacks = useLatest({ + withPastedUrlsUnfurling: withPastedUrlsUnfurling || undefined, + }); + + const isPastedUrlsUnfurlingEnabled = Boolean(withPastedUrlsUnfurling); + const insertData = useCallback( + (dataTransfer, next) => { + const fetchOembed = callbacks.current.withPastedUrlsUnfurling?.fetchOembed; + let handled = false; + if (isPastedUrlsUnfurlingEnabled && fetchOembed) { + handled = unfurlPastedUrls(editor, fetchOembed, dataTransfer); + } + if (!handled) { + next(dataTransfer); + } + }, + [isPastedUrlsUnfurlingEnabled], + ); + return useRegisterExtension({ id: EXTENSION_ID, isElementEqual(element, another) { @@ -128,6 +152,7 @@ export function PlaceholdersExtension({ }, isRichBlock: PlaceholderNode.isPlaceholderNode, isVoid: PlaceholderNode.isPlaceholderNode, + insertData, normalizeNode: [ fixDuplicatePlaceholderUuid, removeDisabledPlaceholders({ @@ -365,8 +390,5 @@ export function PlaceholdersExtension({ } return undefined; }, - withOverrides: withPastedUrlsUnfurling( - isUnfurlingPastedUrls ? isUnfurlingPastedUrls.fetchOembed : undefined, - ), }); } diff --git a/packages/slate-editor/src/extensions/placeholders/behaviour/index.ts b/packages/slate-editor/src/extensions/placeholders/behaviour/index.ts index e4e30ddbf..1a9870369 100644 --- a/packages/slate-editor/src/extensions/placeholders/behaviour/index.ts +++ b/packages/slate-editor/src/extensions/placeholders/behaviour/index.ts @@ -1 +1 @@ -export { withPastedUrlsUnfurling } from './withPastedUrlsUnfurling'; +export { unfurlPastedUrls } from './unfurlPastedUrls'; diff --git a/packages/slate-editor/src/extensions/placeholders/behaviour/unfurlPastedUrls.ts b/packages/slate-editor/src/extensions/placeholders/behaviour/unfurlPastedUrls.ts new file mode 100644 index 000000000..ac6a92498 --- /dev/null +++ b/packages/slate-editor/src/extensions/placeholders/behaviour/unfurlPastedUrls.ts @@ -0,0 +1,49 @@ +import { EditorCommands } from '@prezly/slate-commons'; +import { Editor, Transforms } from 'slate'; + +import { isUrl } from '#lib'; + +import { insertPlaceholder } from '../lib'; +import { PlaceholderNode } from '../PlaceholderNode'; +import { PlaceholdersManager } from '../PlaceholdersManager'; +import type { FetchOEmbedFn } from '../types'; + +/** + * Automatically link pasted content if it's a URL. // See DEV-11519 + */ +export function unfurlPastedUrls(editor: Editor, fetchOembed: FetchOEmbedFn, data: DataTransfer) { + const hasHtml = Boolean(data.getData('text/html')); + const pasted = data.getData('text'); + + if (!hasHtml && isUrl(pasted) && EditorCommands.isSelectionEmpty(editor)) { + Editor.withoutNormalizing(editor, () => { + const placeholder = insertPlaceholder(editor, { + type: PlaceholderNode.Type.EMBED, + }); + PlaceholdersManager.register( + placeholder.type, + placeholder.uuid, + bootstrap(fetchOembed, pasted), + ); + const path = EditorCommands.getNodePath(editor, { + at: [], + match: PlaceholderNode.isSameAs(placeholder), + }); + if (path) { + Transforms.select(editor, path); + } + }); + return true; // handled + } + + return false; +} + +async function bootstrap(fetchOembed: FetchOEmbedFn, url: string) { + try { + const oembed = await fetchOembed(url); + return { oembed, url }; + } catch { + return { url, fallback: 'link' } as const; + } +} diff --git a/packages/slate-editor/src/extensions/placeholders/behaviour/withPastedUrlsUnfurling.ts b/packages/slate-editor/src/extensions/placeholders/behaviour/withPastedUrlsUnfurling.ts deleted file mode 100644 index 2d21ff7f4..000000000 --- a/packages/slate-editor/src/extensions/placeholders/behaviour/withPastedUrlsUnfurling.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { WithOverrides } from '@prezly/slate-commons'; -import { EditorCommands } from '@prezly/slate-commons'; -import { Editor, Transforms } from 'slate'; - -import { isUrl } from '#lib'; - -import { insertPlaceholder } from '../lib'; -import { PlaceholderNode } from '../PlaceholderNode'; -import { PlaceholdersManager } from '../PlaceholdersManager'; -import type { FetchOEmbedFn } from '../types'; - -/** - * Automatically link pasted content if it's a URL. // See DEV-11519 - */ -export function withPastedUrlsUnfurling(fetchOembed: FetchOEmbedFn | undefined): WithOverrides { - if (!fetchOembed) { - return (editor) => editor; - } - - return (editor) => { - const { insertData } = editor; - - editor.insertData = (data) => { - const hasHtml = Boolean(data.getData('text/html')); - const pasted = data.getData('text'); - - if (!hasHtml && isUrl(pasted) && EditorCommands.isSelectionEmpty(editor)) { - Editor.withoutNormalizing(editor, () => { - const placeholder = insertPlaceholder(editor, { - type: PlaceholderNode.Type.EMBED, - }); - PlaceholdersManager.register( - placeholder.type, - placeholder.uuid, - bootstrap(fetchOembed, pasted), - ); - const path = EditorCommands.getNodePath(editor, { - at: [], - match: PlaceholderNode.isSameAs(placeholder), - }); - if (path) { - Transforms.select(editor, path); - } - }); - return; - } - - insertData(data); - }; - - return editor; - }; -} - -async function bootstrap(fetchOembed: FetchOEmbedFn, url: string) { - try { - const oembed = await fetchOembed(url); - return { oembed, url }; - } catch { - return { url, fallback: 'link' } as const; - } -} From c0846c14ad0ac94eeaa7d8c4d5adb6ad23e738e1 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Tue, 19 Sep 2023 00:39:42 +0300 Subject: [PATCH 37/54] [CARE-1802] Add an extension hook into `editor.insertText()` method --- .../src/extensions/ExtensionsEditor.ts | 17 +++++++++++++++++ .../src/extensions/useRegisterExtension.ts | 2 ++ packages/slate-commons/src/types/Extension.ts | 2 ++ .../src/types/TextInsertionHandler.ts | 5 +++++ packages/slate-commons/src/types/index.ts | 1 + 5 files changed, 27 insertions(+) create mode 100644 packages/slate-commons/src/types/TextInsertionHandler.ts diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index af533409b..223ed6fc0 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -42,6 +42,7 @@ export function withExtensions( isVoid: editor.isVoid, insertBreak: editor.insertBreak, insertData: editor.insertData, + insertText: editor.insertText, normalizeNode: editor.normalizeNode, }; const extensionsEditor: T & ExtensionsEditor = Object.assign(editor, { @@ -105,6 +106,22 @@ export function withExtensions( next(dataTransfer); }, + insertText(text) { + const handlers = extensionsEditor.extensions + .map((ext) => ext.insertText) + .filter(isNotUndefined); + + function next(text: string) { + const handler = handlers.shift(); + if (handler) { + handler(text, next); + } else { + parent.insertText(text); + } + } + + next(text); + }, normalizeNode(entry) { const normalizers = extensionsEditor.extensions.flatMap( (ext) => ext.normalizeNode ?? [], diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts index 31a41b3b4..a49e6d8c0 100644 --- a/packages/slate-commons/src/extensions/useRegisterExtension.ts +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -17,6 +17,7 @@ export function useRegisterExtension(extension: Extension): null { isVoid, insertBreak, insertData, + insertText, normalizeNode, onDOMBeforeInput, onKeyDown, @@ -46,6 +47,7 @@ export function useRegisterExtension(extension: Extension): null { isVoid, insertBreak, insertData, + insertText, normalizeNode, onDOMBeforeInput, onKeyDown, diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index 346407cd0..d4e60cce0 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -10,6 +10,7 @@ import type { OnKeyDown } from './OnKeyDown'; import type { RenderElement } from './RenderElement'; import type { RenderLeaf } from './RenderLeaf'; import type { Serialize } from './Serialize'; +import type { TextInsertionHandler } from './TextInsertionHandler'; import type { WithOverrides } from './WithOverrides'; export interface Extension { @@ -22,6 +23,7 @@ export interface Extension { * Call `next()` to allow other extensions (or the editor) handling the call. */ insertData?: DataTransferHandler; // OK + insertText?: TextInsertionHandler; // OK /** * Compare two elements. * `children` arrays can be omitted from the comparison, diff --git a/packages/slate-commons/src/types/TextInsertionHandler.ts b/packages/slate-commons/src/types/TextInsertionHandler.ts new file mode 100644 index 000000000..03079c35c --- /dev/null +++ b/packages/slate-commons/src/types/TextInsertionHandler.ts @@ -0,0 +1,5 @@ +/** + * Hook into ReactEditor's `insertText()` method. + * Call `next()` to allow other extensions (or the editor) handling the call. + */ +export type TextInsertionHandler = (text: string, next: (text: string) => void) => void; diff --git a/packages/slate-commons/src/types/index.ts b/packages/slate-commons/src/types/index.ts index 95b5b9de3..0bbf5c31a 100644 --- a/packages/slate-commons/src/types/index.ts +++ b/packages/slate-commons/src/types/index.ts @@ -9,4 +9,5 @@ export type { OnDOMBeforeInput } from './OnDOMBeforeInput'; export type { OnKeyDown } from './OnKeyDown'; export type { RenderElement } from './RenderElement'; export type { RenderLeaf } from './RenderLeaf'; +export type { TextInsertionHandler } from './TextInsertionHandler'; export type { WithOverrides } from './WithOverrides'; From 698fb74ac98690aeb5c856d0dbc1cb65773230cc Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Tue, 19 Sep 2023 00:43:11 +0300 Subject: [PATCH 38/54] [CARE-1802] Rewrite `withAutoformat` editor mutator to `insertText()` hook --- .../autoformat/AutoformatExtension.ts | 56 ++++++++++++++++++- .../extensions/autoformat/withAutoformat.ts | 53 ------------------ 2 files changed, 53 insertions(+), 56 deletions(-) delete mode 100644 packages/slate-editor/src/extensions/autoformat/withAutoformat.ts diff --git a/packages/slate-editor/src/extensions/autoformat/AutoformatExtension.ts b/packages/slate-editor/src/extensions/autoformat/AutoformatExtension.ts index baa7cb0af..ac72ef7e8 100644 --- a/packages/slate-editor/src/extensions/autoformat/AutoformatExtension.ts +++ b/packages/slate-editor/src/extensions/autoformat/AutoformatExtension.ts @@ -1,13 +1,63 @@ -import { useRegisterExtension } from '@prezly/slate-commons'; +import type { TextInsertionHandler } from '@prezly/slate-commons'; +import { EditorCommands, useRegisterExtension } from '@prezly/slate-commons'; +import { isParagraphNode } from '@prezly/slate-types'; +import { useCallback } from 'react'; +import { useSlateStatic } from 'slate-react'; +import { useLatest } from '#lib'; + +import { autoformatBlock, autoformatMark, autoformatText } from './transforms'; import type { AutoformatParameters } from './types'; -import { withAutoformat } from './withAutoformat'; export const EXTENSION_ID = 'AutoformatExtension'; +const AUTOFORMATTERS = { + block: autoformatBlock, + mark: autoformatMark, + text: autoformatText, +}; + export function AutoformatExtension(params: AutoformatParameters) { + const editor = useSlateStatic(); + const refs = useLatest(params); + + const insertText = useCallback((text, next) => { + next(text); + + if (text !== ' ') { + return; + } + + const textBefore = EditorCommands.getPrevChars(editor, 2).slice(0, -1); + + for (const rule of refs.current.rules) { + const { mode = 'text', query } = rule; + + if (query && !query(editor, { ...rule, text })) continue; + + if (editor.selection) { + const [currentNode] = EditorCommands.getCurrentNodeEntry(editor) ?? []; + + if (mode === 'block' && !isParagraphNode(currentNode)) { + continue; + } + + const formatter = AUTOFORMATTERS[mode]; + + const formatResult = formatter?.(editor, { + ...(rule as any), + text: textBefore, + }); + + if (formatResult) { + return; + } + } + } + }, []); + return useRegisterExtension({ id: EXTENSION_ID, - withOverrides: (editor) => withAutoformat(editor, params.rules), + insertText, }); } diff --git a/packages/slate-editor/src/extensions/autoformat/withAutoformat.ts b/packages/slate-editor/src/extensions/autoformat/withAutoformat.ts deleted file mode 100644 index 8ef1cb257..000000000 --- a/packages/slate-editor/src/extensions/autoformat/withAutoformat.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { EditorCommands } from '@prezly/slate-commons'; -import { isParagraphNode } from '@prezly/slate-types'; -import type { Editor } from 'slate'; - -import { autoformatBlock, autoformatMark, autoformatText } from './transforms'; -import type { AutoformatRule } from './types'; - -export function withAutoformat(editor: T, rules: AutoformatRule[]): T { - const { insertText } = editor; - - const autoformatters = { - block: autoformatBlock, - mark: autoformatMark, - text: autoformatText, - }; - - editor.insertText = (text) => { - insertText(text); - - if (text !== ' ') { - return; - } - - const textBefore = EditorCommands.getPrevChars(editor, 2).slice(0, -1); - - for (const rule of rules) { - const { mode = 'text', query } = rule; - - if (query && !query(editor, { ...rule, text })) continue; - - if (editor.selection) { - const [currentNode] = EditorCommands.getCurrentNodeEntry(editor) ?? []; - - if (mode === 'block' && !isParagraphNode(currentNode)) { - continue; - } - - const formatter = autoformatters[mode]; - - const formatResult = formatter?.(editor, { - ...(rule as any), - text: textBefore, - }); - - if (formatResult) { - return; - } - } - } - }; - - return editor; -} From b3d6c8a3e932be587c8585e4e984d92477e8a7fd Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Tue, 19 Sep 2023 00:51:24 +0300 Subject: [PATCH 39/54] [CARE-1802] Add a way to hook into `HistoryEditor` `undo()` and `redo()` methods --- .../src/extensions/ExtensionsEditor.ts | 39 ++++++++++++++++++- .../src/extensions/useRegisterExtension.ts | 4 ++ packages/slate-commons/src/types/Extension.ts | 5 +++ .../slate-commons/src/types/HistoryHandler.ts | 5 +++ packages/slate-commons/src/types/index.ts | 1 + 5 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 packages/slate-commons/src/types/HistoryHandler.ts diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index 223ed6fc0..65e661fe8 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -1,5 +1,6 @@ import { isNotUndefined } from '@technically/is-not-undefined'; import type { BaseEditor, Descendant, Element, Node } from 'slate'; +import type { HistoryEditor } from 'slate-history'; import type { ReactEditor } from 'slate-react'; import type { Extension } from '../types'; @@ -33,7 +34,7 @@ export interface ExtensionsEditor extends BaseEditor { serialize(nodes: Descendant[]): Descendant[]; } -export function withExtensions( +export function withExtensions( editor: T, extensions: Extension[] = [], ): T & ExtensionsEditor { @@ -44,6 +45,8 @@ export function withExtensions( insertData: editor.insertData, insertText: editor.insertText, normalizeNode: editor.normalizeNode, + undo: editor.undo, + redo: editor.redo, }; const extensionsEditor: T & ExtensionsEditor = Object.assign(editor, { extensions, @@ -143,7 +146,39 @@ export function withExtensions( nodes, ); }, - } satisfies Partial); + undo() { + const handlers = extensionsEditor.extensions + .map((ext) => ext.undo) + .filter(isNotUndefined); + + function next() { + const handler = handlers.shift(); + if (handler) { + handler(next); + } else { + parent.undo(); + } + } + + next(); + }, + redo() { + const handlers = extensionsEditor.extensions + .map((ext) => ext.redo) + .filter(isNotUndefined); + + function next() { + const handler = handlers.shift(); + if (handler) { + handler(next); + } else { + parent.redo(); + } + } + + next(); + }, + } satisfies Partial); return extensionsEditor; } diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts index a49e6d8c0..118873b7b 100644 --- a/packages/slate-commons/src/extensions/useRegisterExtension.ts +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -24,6 +24,8 @@ export function useRegisterExtension(extension: Extension): null { renderElement, renderLeaf, serialize, + undo, + redo, withOverrides, ...rest } = extension; @@ -54,6 +56,8 @@ export function useRegisterExtension(extension: Extension): null { renderElement, renderLeaf, serialize, + undo, + redo, withOverrides, ], ); diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index d4e60cce0..ea1416d77 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -3,6 +3,7 @@ import type { Element, Node } from 'slate'; import type { DataTransferHandler } from './DataTransferHandler'; import type { DecorateFactory } from './DecorateFactory'; import type { DeserializeHtml } from './DeserializeHtml'; +import type { HistoryHandler } from './HistoryHandler'; import type { LineBreakHandler } from './LineBreakHandler'; import type { Normalize } from './Normalize'; import type { OnDOMBeforeInput } from './OnDOMBeforeInput'; @@ -39,6 +40,10 @@ export interface Extension { renderElement?: RenderElement; // OK renderLeaf?: RenderLeaf; // OK serialize?: Serialize; // OK + + undo?: HistoryHandler; + redo?: HistoryHandler; + /** * @deprecated Please do not use this. We're going to drop this functionality soon. */ diff --git a/packages/slate-commons/src/types/HistoryHandler.ts b/packages/slate-commons/src/types/HistoryHandler.ts new file mode 100644 index 000000000..c594f8cab --- /dev/null +++ b/packages/slate-commons/src/types/HistoryHandler.ts @@ -0,0 +1,5 @@ +/** + * Hook into HistoryEditors's `undo()` & `redo()` methods. + * Return `true` to prevent handling this action by other extensions or the editor. + */ +export type HistoryHandler = (next: () => void) => void; diff --git a/packages/slate-commons/src/types/index.ts b/packages/slate-commons/src/types/index.ts index 0bbf5c31a..223a1ab11 100644 --- a/packages/slate-commons/src/types/index.ts +++ b/packages/slate-commons/src/types/index.ts @@ -4,6 +4,7 @@ export type { Decorate } from './Decorate'; export type { DecorateFactory } from './DecorateFactory'; export type { DeserializeHtml, DeserializeElement, DeserializeMarks } from './DeserializeHtml'; export type { Extension } from './Extension'; +export type { HistoryHandler } from './HistoryHandler'; export type { Normalize } from './Normalize'; export type { OnDOMBeforeInput } from './OnDOMBeforeInput'; export type { OnKeyDown } from './OnKeyDown'; From 1264170bd0e5ec40ae128552a646ece45beb966b Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 01:40:32 +0300 Subject: [PATCH 40/54] [CARE-1802] Simplify event-tracking callbacks Consolidate file attachments management inside `FileAttachmentExtension` module  Conflicts:  packages/slate-editor/src/extensions/paste-files/lib/createDataTransferHandler.ts  packages/slate-editor/src/extensions/placeholders/PlaceholdersManager.ts  packages/slate-editor/src/extensions/placeholders/elements/AttachmentPlaceholderElement.tsx  packages/slate-editor/src/extensions/placeholders/index.ts  packages/slate-editor/src/modules/events/types.ts --- .../file-attachment/FileAttachmentExtension.tsx | 5 ++--- .../components/FileAttachmentMenu.tsx | 9 ++++----- .../extensions/galleries/GalleriesExtension.tsx | 4 +--- .../galleries/components/GalleryElement.tsx | 11 +++++------ .../extensions/paste-files/PasteFilesExtension.ts | 7 +++---- .../paste-files/lib/createDataTransferHandler.ts | 4 ++-- .../paste-images/PasteImagesExtension.ts | 7 +++---- .../paste-images/lib/createDataTransferHandler.ts | 4 ++-- .../slate-editor/src/modules/editor/Editor.tsx | 4 ++-- .../slate-editor/src/modules/editor/Extensions.tsx | 14 +++++++++----- packages/slate-editor/src/modules/events/types.ts | 1 + 11 files changed, 34 insertions(+), 36 deletions(-) diff --git a/packages/slate-editor/src/extensions/file-attachment/FileAttachmentExtension.tsx b/packages/slate-editor/src/extensions/file-attachment/FileAttachmentExtension.tsx index cce95aed8..d0b5f4c54 100644 --- a/packages/slate-editor/src/extensions/file-attachment/FileAttachmentExtension.tsx +++ b/packages/slate-editor/src/extensions/file-attachment/FileAttachmentExtension.tsx @@ -3,7 +3,6 @@ import type { AttachmentNode } from '@prezly/slate-types'; import { ATTACHMENT_NODE_TYPE, isAttachmentNode } from '@prezly/slate-types'; import { isEqual, noop } from '@technically/lodash'; import React from 'react'; -import type { Editor } from 'slate'; import type { RenderElementProps } from 'slate-react'; import { EditorBlock } from '#components'; @@ -14,8 +13,8 @@ import { FileAttachment, FileAttachmentMenu } from './components'; import { normalizeRedundantFileAttachmentAttributes, parseSerializedElement } from './lib'; export interface Parameters { - onEdited?: (editor: Editor, updated: AttachmentNode) => void; - onRemoved?: (editor: Editor, element: AttachmentNode) => void; + onEdited?: (updated: AttachmentNode) => void; + onRemoved?: (element: AttachmentNode) => void; } export const EXTENSION_ID = 'FileAttachmentExtension'; diff --git a/packages/slate-editor/src/extensions/file-attachment/components/FileAttachmentMenu.tsx b/packages/slate-editor/src/extensions/file-attachment/components/FileAttachmentMenu.tsx index fd012c170..d8458da84 100644 --- a/packages/slate-editor/src/extensions/file-attachment/components/FileAttachmentMenu.tsx +++ b/packages/slate-editor/src/extensions/file-attachment/components/FileAttachmentMenu.tsx @@ -2,7 +2,6 @@ import type { AttachmentNode } from '@prezly/slate-types'; import { isAttachmentNode } from '@prezly/slate-types'; import { UploadcareFile } from '@prezly/uploadcare'; import React, { useCallback } from 'react'; -import type { Editor } from 'slate'; import { Transforms } from 'slate'; import { useSelected, useSlate } from 'slate-react'; @@ -14,8 +13,8 @@ import { removeFileAttachment } from '../transforms'; interface Props { element: AttachmentNode; onClose: () => void; - onEdited: (editor: Editor, updated: AttachmentNode) => void; - onRemoved: (editor: Editor, element: AttachmentNode) => void; + onEdited: (updated: AttachmentNode, original: AttachmentNode) => void; + onRemoved: (element: AttachmentNode) => void; } export function FileAttachmentMenu({ element, onClose, onEdited, onRemoved }: Props) { @@ -29,7 +28,7 @@ export function FileAttachmentMenu({ element, onClose, onEdited, onRemoved }: Pr const removedElement = removeFileAttachment(editor); if (removedElement) { - onRemoved(editor, removedElement); + onRemoved(removedElement); } }; @@ -48,7 +47,7 @@ export function FileAttachmentMenu({ element, onClose, onEdited, onRemoved }: Pr match: isAttachmentNode, }); - onEdited(editor, { ...element, ...update }); + onEdited({ ...element, ...update }, element); onClose(); }; diff --git a/packages/slate-editor/src/extensions/galleries/GalleriesExtension.tsx b/packages/slate-editor/src/extensions/galleries/GalleriesExtension.tsx index 2e3b2a54c..f25759cff 100644 --- a/packages/slate-editor/src/extensions/galleries/GalleriesExtension.tsx +++ b/packages/slate-editor/src/extensions/galleries/GalleriesExtension.tsx @@ -4,7 +4,6 @@ import type { GalleryNode } from '@prezly/slate-types'; import { GALLERY_NODE_TYPE, isGalleryNode } from '@prezly/slate-types'; import { isEqual } from '@technically/lodash'; import React from 'react'; -import type { Editor } from 'slate'; import type { RenderElementProps } from 'slate-react'; import { composeElementDeserializer } from '#modules/html-deserialization'; @@ -19,14 +18,13 @@ import { interface Parameters { availableWidth: number; onEdited?: ( - editor: Editor, gallery: GalleryNode, extra: { successfulUploads: number; failedUploads: Error[]; }, ) => void; - onShuffled?: (editor: Editor, updated: GalleryNode, original: GalleryNode) => void; + onShuffled?: (updated: GalleryNode, original: GalleryNode) => void; withMediaGalleryTab: false | { enabled: true; newsroom: NewsroomRef }; withLayoutOptions: boolean | undefined; } diff --git a/packages/slate-editor/src/extensions/galleries/components/GalleryElement.tsx b/packages/slate-editor/src/extensions/galleries/components/GalleryElement.tsx index 75225b1f8..de2186932 100644 --- a/packages/slate-editor/src/extensions/galleries/components/GalleryElement.tsx +++ b/packages/slate-editor/src/extensions/galleries/components/GalleryElement.tsx @@ -23,16 +23,15 @@ import { GalleryMenu } from './GalleryMenu'; interface Props extends RenderElementProps { availableWidth: number; element: GalleryNode; - onEdit?: (editor: Editor, gallery: GalleryNode) => void; + onEdit?: (gallery: GalleryNode) => void; onEdited?: ( - editor: Editor, gallery: GalleryNode, extra: { successfulUploads: number; failedUploads: Error[]; }, ) => void; - onShuffled?: (editor: Editor, updated: GalleryNode, original: GalleryNode) => void; + onShuffled?: (updated: GalleryNode, original: GalleryNode) => void; withMediaGalleryTab: false | { enabled: true; newsroom: NewsroomRef }; withLayoutOptions: boolean; } @@ -54,7 +53,7 @@ export function GalleryElement({ const callbacks = useLatest({ onEdit, onEdited, onShuffled }); async function handleEdit() { - callbacks.current.onEdit(editor, element); + callbacks.current.onEdit(element); const files = element.images.map(({ caption, file }) => { const uploadcareImage = UploadcareImage.createFromPrezlyStoragePayload(file); @@ -103,7 +102,7 @@ export function GalleryElement({ { match: (node) => node === element }, ); - callbacks.current.onEdited(editor, element, { + callbacks.current.onEdited(element, { successfulUploads: successfulUploads.length, failedUploads, }); @@ -119,7 +118,7 @@ export function GalleryElement({ updateGallery(editor, update); - callbacks.current.onShuffled(editor, element, { ...element, ...update }); + callbacks.current.onShuffled(element, { ...element, ...update }); } return ( diff --git a/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts b/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts index ab10da49c..5e71e1362 100644 --- a/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts +++ b/packages/slate-editor/src/extensions/paste-files/PasteFilesExtension.ts @@ -1,7 +1,6 @@ import { useRegisterExtension } from '@prezly/slate-commons'; import { noop } from '@technically/lodash'; import { useMemo } from 'react'; -import type { Editor } from 'slate'; import { useSlateStatic } from 'slate-react'; import { useLatest } from '#lib'; @@ -11,7 +10,7 @@ import { createDataTransferHandler } from './lib'; export const EXTENSION_ID = 'PasteFilesExtension'; export interface Parameters { - onFilesPasted?: (editor: Editor, files: File[]) => void; + onFilesPasted?: (files: File[]) => void; } export function PasteFilesExtension({ onFilesPasted = noop }: Parameters = {}) { @@ -19,8 +18,8 @@ export function PasteFilesExtension({ onFilesPasted = noop }: Parameters = {}) { const callbacks = useLatest({ onFilesPasted }); const insertData = useMemo(() => { return createDataTransferHandler(editor, { - onFilesPasted: (editor, files) => { - callbacks.current.onFilesPasted(editor, files); + onFilesPasted: (files) => { + callbacks.current.onFilesPasted(files); }, }); }, []); diff --git a/packages/slate-editor/src/extensions/paste-files/lib/createDataTransferHandler.ts b/packages/slate-editor/src/extensions/paste-files/lib/createDataTransferHandler.ts index f825fc4f4..6e33766b1 100644 --- a/packages/slate-editor/src/extensions/paste-files/lib/createDataTransferHandler.ts +++ b/packages/slate-editor/src/extensions/paste-files/lib/createDataTransferHandler.ts @@ -13,7 +13,7 @@ import { IMAGE_TYPES } from '#extensions/image'; import { insertPlaceholders, PlaceholderNode, PlaceholdersManager } from '#extensions/placeholders'; interface Parameters { - onFilesPasted?: (editor: Editor, files: File[]) => void; + onFilesPasted?: (files: File[]) => void; } export function createDataTransferHandler( @@ -32,7 +32,7 @@ export function createDataTransferHandler( return next(dataTransfer); } - onFilesPasted(editor, files); + onFilesPasted(files); const placeholders = insertPlaceholders(editor, files.length, { type: PlaceholderNode.Type.ATTACHMENT, diff --git a/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts b/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts index 7346046fe..ea098c703 100644 --- a/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts +++ b/packages/slate-editor/src/extensions/paste-images/PasteImagesExtension.ts @@ -1,7 +1,6 @@ import { useRegisterExtension } from '@prezly/slate-commons'; import { noop } from '@technically/lodash'; import { useMemo } from 'react'; -import type { Editor } from 'slate'; import { useSlateStatic } from 'slate-react'; import { useLatest } from '#lib'; @@ -11,7 +10,7 @@ import { createDataTransferHandler } from './lib'; export const EXTENSION_ID = 'PasteImagesExtension'; export interface Parameters { - onImagesPasted?: (editor: Editor, images: File[]) => void; + onImagesPasted?: (images: File[]) => void; } export function PasteImagesExtension({ onImagesPasted = noop }: Parameters = {}) { @@ -19,8 +18,8 @@ export function PasteImagesExtension({ onImagesPasted = noop }: Parameters = {}) const callbacks = useLatest({ onImagesPasted }); const insertData = useMemo(() => { return createDataTransferHandler(editor, { - onImagesPasted: (editor, images) => { - callbacks.current.onImagesPasted(editor, images); + onImagesPasted: (images) => { + callbacks.current.onImagesPasted(images); }, }); }, []); diff --git a/packages/slate-editor/src/extensions/paste-images/lib/createDataTransferHandler.ts b/packages/slate-editor/src/extensions/paste-images/lib/createDataTransferHandler.ts index d9600255a..2bed17a00 100644 --- a/packages/slate-editor/src/extensions/paste-images/lib/createDataTransferHandler.ts +++ b/packages/slate-editor/src/extensions/paste-images/lib/createDataTransferHandler.ts @@ -12,7 +12,7 @@ import { createImage, IMAGE_TYPES } from '#extensions/image'; import { insertPlaceholders, PlaceholderNode, PlaceholdersManager } from '#extensions/placeholders'; export interface Parameters { - onImagesPasted: (editor: Editor, images: File[]) => void; + onImagesPasted: (images: File[]) => void; } export function createDataTransferHandler( @@ -31,7 +31,7 @@ export function createDataTransferHandler( return next(dataTransfer); } - onImagesPasted(editor, images); + onImagesPasted(images); const placeholders = insertPlaceholders(editor, images.length, { type: PlaceholderNode.Type.IMAGE, diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 165150b37..9ff074668 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -744,7 +744,7 @@ export const Editor = forwardRef((props, forwardedRef) = {withAttachments && ( { + onFilesPasted={(files) => { EventsEditor.dispatchEvent(editor, 'files-pasted', { filesCount: files.length, isEmpty: EditorCommands.isEmpty(editor), @@ -755,7 +755,7 @@ export const Editor = forwardRef((props, forwardedRef) = {withImages && ( { + onImagesPasted={(images) => { EventsEditor.dispatchEvent( editor, 'images-pasted', diff --git a/packages/slate-editor/src/modules/editor/Extensions.tsx b/packages/slate-editor/src/modules/editor/Extensions.tsx index d02aad3d8..ceeda5e35 100644 --- a/packages/slate-editor/src/modules/editor/Extensions.tsx +++ b/packages/slate-editor/src/modules/editor/Extensions.tsx @@ -2,6 +2,7 @@ import { EditorCommands } from '@prezly/slate-commons'; import { isImageNode, isQuoteNode } from '@prezly/slate-types'; import React from 'react'; import { Node } from 'slate'; +import { useSlateStatic } from 'slate-react'; import { AllowedBlocksExtension } from '#extensions/allowed-blocks'; import { AutoformatExtension } from '#extensions/autoformat'; @@ -111,6 +112,8 @@ export function Extensions({ withVideos, withWebBookmarks, }: Props) { + const editor = useSlateStatic(); + if (withPressContacts && withInlineContacts) { throw new Error( `Using 'withPressContacts' and 'withInlineContacts' at the same time is not supported.`, @@ -172,7 +175,7 @@ export function Extensions({ {withAttachments && ( <> { + onFilesPasted={(files) => { EventsEditor.dispatchEvent(editor, 'files-pasted', { filesCount: files.length, isEmpty: EditorCommands.isEmpty(editor), @@ -180,16 +183,17 @@ export function Extensions({ }} /> { + onEdited={(updated) => { // TODO: It seems it would be more useful to only provide the changeset patch in the event payload. EventsEditor.dispatchEvent(editor, 'attachment-edited', { + filename: updated.file.filename, description: updated.description, mimeType: updated.file.mime_type, size: updated.file.size, uuid: updated.file.uuid, }); }} - onRemoved={(editor, attachment) => { + onRemoved={(attachment) => { EventsEditor.dispatchEvent(editor, 'attachment-removed', { uuid: attachment.file.uuid, }); @@ -203,7 +207,7 @@ export function Extensions({ {withGalleries && ( { + onEdited={(gallery, { failedUploads }) => { EventsEditor.dispatchEvent(editor, 'gallery-edited', { imagesCount: gallery.images.length, }); @@ -219,7 +223,7 @@ export function Extensions({ }); } }} - onShuffled={(editor, updated) => { + onShuffled={(updated) => { EventsEditor.dispatchEvent(editor, 'gallery-images-shuffled', { imagesCount: updated.images.length, }); diff --git a/packages/slate-editor/src/modules/events/types.ts b/packages/slate-editor/src/modules/events/types.ts index 06fa8bf5d..192ee2c32 100644 --- a/packages/slate-editor/src/modules/events/types.ts +++ b/packages/slate-editor/src/modules/events/types.ts @@ -27,6 +27,7 @@ export type EditorEventMap = { trigger: string; }; 'attachment-edited': { + filename: string; description: string; mimeType: string; size: number; From 8a02a3af95a6771c9f8061299b8732dee3285fae Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 02:05:08 +0300 Subject: [PATCH 41/54] [CARE-1802] Add an extension hook into `editor.setFragmentData()` method --- .../src/extensions/ExtensionsEditor.ts | 17 +++++++++++++++++ .../src/extensions/useRegisterExtension.ts | 2 ++ packages/slate-commons/src/types/Extension.ts | 7 +++++++ 3 files changed, 26 insertions(+) diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index 65e661fe8..5c9824083 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -45,6 +45,7 @@ export function withExtensions ext.setFragmentData) + .filter(isNotUndefined); + + function next(dataTransfer: DataTransfer) { + const handler = handlers.shift(); + if (handler) { + handler(dataTransfer, next); + } else { + parent.setFragmentData(dataTransfer, originEvent); + } + } + + next(dataTransfer); + }, undo() { const handlers = extensionsEditor.extensions .map((ext) => ext.undo) diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts index 118873b7b..2ed82ab97 100644 --- a/packages/slate-commons/src/extensions/useRegisterExtension.ts +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -24,6 +24,7 @@ export function useRegisterExtension(extension: Extension): null { renderElement, renderLeaf, serialize, + setFragmentData, undo, redo, withOverrides, @@ -56,6 +57,7 @@ export function useRegisterExtension(extension: Extension): null { renderElement, renderLeaf, serialize, + setFragmentData, undo, redo, withOverrides, diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index ea1416d77..29c31a211 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -22,6 +22,7 @@ export interface Extension { /** * Hook into ReactEditor's `insertData()` method. * Call `next()` to allow other extensions (or the editor) handling the call. + * @see https://docs.slatejs.org/libraries/slate-react/react-editor */ insertData?: DataTransferHandler; // OK insertText?: TextInsertionHandler; // OK @@ -40,6 +41,12 @@ export interface Extension { renderElement?: RenderElement; // OK renderLeaf?: RenderLeaf; // OK serialize?: Serialize; // OK + /** + * Hook into ReactEditor's `setFragmentData()` method. + * Call `next()` to allow other extensions (or the editor) handling the call. + * @see https://docs.slatejs.org/libraries/slate-react/react-editor + */ + setFragmentData?: DataTransferHandler; undo?: HistoryHandler; redo?: HistoryHandler; From dfef2fac38ed0e8c9f9215b10ac8749807bf246c Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Wed, 20 Sep 2023 02:13:23 +0300 Subject: [PATCH 42/54] [CARE-1802] Convert `withImages()` editor mutator plugin to `setFragmentData()` hook --- ...mages.test.tsx => ImageExtension.test.tsx} | 4 ++-- .../src/extensions/image/ImageExtension.tsx | 10 +++++++--- .../createFragmentDataSetter.ts} | 19 ++++++++----------- .../src/extensions/image/lib/index.ts | 1 + 4 files changed, 18 insertions(+), 16 deletions(-) rename packages/slate-editor/src/extensions/image/{withImages.test.tsx => ImageExtension.test.tsx} (95%) rename packages/slate-editor/src/extensions/image/{withImages.ts => lib/createFragmentDataSetter.ts} (68%) diff --git a/packages/slate-editor/src/extensions/image/withImages.test.tsx b/packages/slate-editor/src/extensions/image/ImageExtension.test.tsx similarity index 95% rename from packages/slate-editor/src/extensions/image/withImages.test.tsx rename to packages/slate-editor/src/extensions/image/ImageExtension.test.tsx index d2eb9eed4..a1eaec541 100644 --- a/packages/slate-editor/src/extensions/image/withImages.test.tsx +++ b/packages/slate-editor/src/extensions/image/ImageExtension.test.tsx @@ -20,7 +20,7 @@ const file: UploadedFile = { const getExtensions = () => [ ImageExtension({ - captions: true, + withCaptions: true, withLayoutOptions: true, }), ]; @@ -31,7 +31,7 @@ const createEditor = (editor: JSX.Element): Editor => { return withExtensions(withImages(withReact(editor as unknown as Editor))); }; -describe('withImages - normalizeChildren', () => { +describe('ImageExtension - normalizeChildren', () => { it('unwraps deeply nested text objects', () => { const editor = createEditor( diff --git a/packages/slate-editor/src/extensions/image/ImageExtension.tsx b/packages/slate-editor/src/extensions/image/ImageExtension.tsx index 47bae546b..1a43353c8 100644 --- a/packages/slate-editor/src/extensions/image/ImageExtension.tsx +++ b/packages/slate-editor/src/extensions/image/ImageExtension.tsx @@ -9,10 +9,11 @@ import { toProgressPromise, UploadcareImage } from '@prezly/uploadcare'; import { isEqual, noop } from '@technically/lodash'; import { isHotkey } from 'is-hotkey'; import type { KeyboardEvent } from 'react'; -import React from 'react'; +import React, { useMemo } from 'react'; import type { Editor } from 'slate'; import { Path, Transforms } from 'slate'; import type { RenderElementProps } from 'slate-react'; +import { useSlateStatic } from 'slate-react'; import { isDeletingEvent, isDeletingEventBackward } from '#lib'; @@ -22,6 +23,7 @@ import { composeElementDeserializer } from '#modules/html-deserialization'; import { ImageElement } from './components'; import { + createFragmentDataSetter, createImage, getAncestorAnchor, normalizeRedundantImageAttributes, @@ -29,7 +31,6 @@ import { toFilePromise, } from './lib'; import type { ImageExtensionConfiguration } from './types'; -import { withImages } from './withImages'; const HOLDING_BACKSPACE_THRESHOLD = 100; @@ -58,6 +59,9 @@ export function ImageExtension({ withNewTabOption = true, withSizeOptions = false, }: Parameters) { + const editor = useSlateStatic(); + const setFragmentData = useMemo(() => createFragmentDataSetter(editor), []); + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { @@ -212,7 +216,7 @@ export function ImageExtension({ return undefined; }, - withOverrides: withImages, + setFragmentData, }); } diff --git a/packages/slate-editor/src/extensions/image/withImages.ts b/packages/slate-editor/src/extensions/image/lib/createFragmentDataSetter.ts similarity index 68% rename from packages/slate-editor/src/extensions/image/withImages.ts rename to packages/slate-editor/src/extensions/image/lib/createFragmentDataSetter.ts index 698a92fca..8660b7c07 100644 --- a/packages/slate-editor/src/extensions/image/withImages.ts +++ b/packages/slate-editor/src/extensions/image/lib/createFragmentDataSetter.ts @@ -1,14 +1,12 @@ -import { EditorCommands } from '@prezly/slate-commons'; +import { type DataTransferHandler, EditorCommands } from '@prezly/slate-commons'; import { isImageNode } from '@prezly/slate-types'; import { Editor, Range } from 'slate'; import { ReactEditor } from 'slate-react'; import { convertToHtml, encodeSlateFragment } from '#lib'; -export function withImages(editor: T): T { - const { setFragmentData } = editor; - - editor.setFragmentData = (data): void => { +export function createFragmentDataSetter(editor: Editor): DataTransferHandler { + return (dataTransfer, next) => { if (editor.selection && Range.isCollapsed(editor.selection)) { const [currentNode] = EditorCommands.getCurrentNodeEntry(editor) || []; @@ -22,17 +20,16 @@ export function withImages(editor: T): T { const domRange = ReactEditor.toDOMRange(editor, editor.selection); const contents = domRange.cloneContents(); const encodedFragment = encodeSlateFragment(editor.getFragment()); - data.setData('application/x-slate-fragment', encodedFragment); - data.setData('text/html', convertToHtml(contents)); + + dataTransfer.setData('application/x-slate-fragment', encodedFragment); + dataTransfer.setData('text/html', convertToHtml(contents)); if (contents.textContent) { - data.setData('text/plain', contents.textContent); + dataTransfer.setData('text/plain', contents.textContent); } return; } } - setFragmentData(data); + next(dataTransfer); }; - - return editor; } diff --git a/packages/slate-editor/src/extensions/image/lib/index.ts b/packages/slate-editor/src/extensions/image/lib/index.ts index 5c568edb3..7f8a7d958 100644 --- a/packages/slate-editor/src/extensions/image/lib/index.ts +++ b/packages/slate-editor/src/extensions/image/lib/index.ts @@ -1,3 +1,4 @@ +export { createFragmentDataSetter } from './createFragmentDataSetter'; export { createImage } from './createImage'; export { getAncestorAnchor } from './getAncestorAnchor'; export { getCurrentImageNodeEntry } from './getCurrentImageNodeEntry'; From 461f2923023975347870164b68c77f652aa965ea Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 11:33:54 +0300 Subject: [PATCH 43/54] [CARE-1802] Change `ListExtension` to work without `withOverrides` --- .../src/extensions/list/ListExtension.tsx | 18 +++++++++++++----- packages/slate-lists/src/index.ts | 15 ++++++++++++--- packages/slate-lists/src/withListsSchema.ts | 12 +++++++++--- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/slate-editor/src/extensions/list/ListExtension.tsx b/packages/slate-editor/src/extensions/list/ListExtension.tsx index 5efe39ea5..ab06d8203 100644 --- a/packages/slate-editor/src/extensions/list/ListExtension.tsx +++ b/packages/slate-editor/src/extensions/list/ListExtension.tsx @@ -3,8 +3,8 @@ import { ListsEditor, normalizeNode, onKeyDown, - withListsReact, - withListsSchema, + registerListsSchema, + withRangeCloneContentsPatched, } from '@prezly/slate-lists'; import { TablesEditor } from '@prezly/slate-tables'; import { @@ -16,7 +16,8 @@ import { LIST_ITEM_TEXT_NODE_TYPE, NUMBERED_LIST_NODE_TYPE, } from '@prezly/slate-types'; -import React from 'react'; +import React, { useEffect } from 'react'; +import { useSlateStatic } from 'slate-react'; import { composeElementDeserializer } from '#modules/html-deserialization'; @@ -27,6 +28,11 @@ import { schema } from './schema'; export const EXTENSION_ID = 'ListExtension'; export function ListExtension() { + const editor = useSlateStatic(); + useEffect(() => { + registerListsSchema(editor, schema); + }, [editor]); + return useRegisterExtension({ id: EXTENSION_ID, deserialize: { @@ -90,8 +96,10 @@ export function ListExtension() { } return undefined; }, - withOverrides: (editor) => { - return withListsReact(withListsSchema(schema)(editor)); + setFragmentData(dataTransfer, next) { + withRangeCloneContentsPatched(() => { + next(dataTransfer); + }); }, }); } diff --git a/packages/slate-lists/src/index.ts b/packages/slate-lists/src/index.ts index 587b729e8..946292b62 100644 --- a/packages/slate-lists/src/index.ts +++ b/packages/slate-lists/src/index.ts @@ -1,8 +1,17 @@ +// Types export { ListsEditor } from './ListsEditor'; -export { normalizeNode } from './normalizeNode'; -export { onKeyDown } from './onKeyDown'; export { ListType, type ListsSchema } from './types'; + +// All-in-one Slate editor plugin to enable Lists functionality export { withLists } from './withLists'; -export { withListsSchema } from './withListsSchema'; +// Keystrokes handler +export { onKeyDown } from './onKeyDown'; + +// Separate Slate plugins to pick, if the all-in-one `withLists()` plugin does more than you need. export { withListsNormalization } from './withListsNormalization'; export { withListsReact } from './withListsReact'; +export { withListsSchema, registerListsSchema } from './withListsSchema'; + +// Lower-level utils to wire to the Editor without `withXxx()` plugins. +export { normalizeNode } from './normalizeNode'; +export { withRangeCloneContentsPatched } from './util'; diff --git a/packages/slate-lists/src/withListsSchema.ts b/packages/slate-lists/src/withListsSchema.ts index f0426c9aa..ea45b45ad 100644 --- a/packages/slate-lists/src/withListsSchema.ts +++ b/packages/slate-lists/src/withListsSchema.ts @@ -4,12 +4,18 @@ import * as Registry from './registry'; import type { ListsSchema } from './types'; /** - * Enables normalizations that enforce schema constraints and recover from unsupported cases. + * Slate plugin to bind the ListsSchema definition to the editor instance. */ export function withListsSchema(schema: ListsSchema) { return function (editor: T): T { - Registry.register(editor, schema); - + registerListsSchema(editor, schema); return editor; }; } + +/** + * Bind the ListsSchema definition to the editor instance. + */ +export function registerListsSchema(editor: Editor, schema: ListsSchema) { + Registry.register(editor, schema); +} From 7c4f8896b3341247b2db0ea8d82e04056115f857 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 11:47:07 +0300 Subject: [PATCH 44/54] [CARE-1802] Add a hook to `Editor.getFragment()` method --- .../src/extensions/ExtensionsEditor.ts | 17 +++++++++++++++++ packages/slate-commons/src/types/Extension.ts | 9 ++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index 5c9824083..b389ac40c 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -45,6 +45,7 @@ export function withExtensions ext.getFragment) + .filter(isNotUndefined); + + function next() { + const handler = handlers.shift(); + if (handler) { + return handler(next); + } else { + return parent.getFragment(); + } + } + + return next(); + }, setFragmentData(dataTransfer: DataTransfer, originEvent?: 'drag' | 'copy' | 'cut') { const handlers = extensionsEditor.extensions .map((ext) => ext.setFragmentData) diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index 29c31a211..a17879c98 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -1,4 +1,4 @@ -import type { Element, Node } from 'slate'; +import type { Descendant, Element, Node } from 'slate'; import type { DataTransferHandler } from './DataTransferHandler'; import type { DecorateFactory } from './DecorateFactory'; @@ -41,6 +41,13 @@ export interface Extension { renderElement?: RenderElement; // OK renderLeaf?: RenderLeaf; // OK serialize?: Serialize; // OK + + /** + * Hook into Editor's `getFragment()` method. + * Call `next()` to allow other extensions (or the editor) handling the call. + * @see https://docs.slatejs.org/api/nodes/editor#getfragment-method + */ + getFragment?: (next: () => Descendant[]) => Descendant[]; /** * Hook into ReactEditor's `setFragmentData()` method. * Call `next()` to allow other extensions (or the editor) handling the call. From 70418c68fc52d5baa7ce41b8ff06641ae99bae28 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 12:12:54 +0300 Subject: [PATCH 45/54] [CARE-1802] Export table plugin's editor overrides as separate parts --- packages/slate-tables/src/core/index.ts | 12 +++- .../src/core/withTablesCopyPasteBehavior.ts | 55 +++++++++++-------- .../src/core/withTablesDeleteBehavior.ts | 31 ++++++++--- packages/slate-tables/src/index.ts | 15 ++++- .../src/normalization/insertMissingCells.ts | 13 ++--- .../src/normalization/removeEmptyRows.ts | 7 +-- .../src/normalization/splitColSpanCells.ts | 7 +-- .../src/normalization/splitRowSpanCells.ts | 8 +-- .../slate-tables/src/withNormalization.ts | 43 ++++++++------- 9 files changed, 110 insertions(+), 81 deletions(-) diff --git a/packages/slate-tables/src/core/index.ts b/packages/slate-tables/src/core/index.ts index be5413ab0..a303f1de2 100644 --- a/packages/slate-tables/src/core/index.ts +++ b/packages/slate-tables/src/core/index.ts @@ -1,5 +1,11 @@ export * from './Matrix'; export * from './Traverse'; -export * from './onKeyDown'; -export * from './withTablesCopyPasteBehavior'; -export * from './withTablesDeleteBehavior'; + +export { onKeyDown } from './onKeyDown'; + +export { withTablesCopyPasteBehavior, getFragment } from './withTablesCopyPasteBehavior'; +export { + withTablesDeleteBehavior, + deleteBackward, + deleteForward, +} from './withTablesDeleteBehavior'; diff --git a/packages/slate-tables/src/core/withTablesCopyPasteBehavior.ts b/packages/slate-tables/src/core/withTablesCopyPasteBehavior.ts index 1112e4ff7..5491668ea 100644 --- a/packages/slate-tables/src/core/withTablesCopyPasteBehavior.ts +++ b/packages/slate-tables/src/core/withTablesCopyPasteBehavior.ts @@ -1,33 +1,42 @@ -import { type Range, Node, Path } from 'slate'; +import { type Range, Node, Path, Editor } from 'slate'; import { findParentCell } from '../queries'; import type { TablesEditor } from '../TablesEditor'; +export function getFragment( + editor: TablesEditor, + next: Editor['getFragment'], +): ReturnType { + if (editor.selection) { + const cellEntry = findParentCell(editor); + + if (cellEntry && isRangeInside(editor.selection, cellEntry[1])) { + const [cell, cellPath] = cellEntry; + const { focus, anchor } = editor.selection; + + return Node.fragment(cell, { + anchor: { + offset: anchor.offset, + path: Path.relative(anchor.path, cellPath), + }, + focus: { + offset: focus.offset, + path: Path.relative(focus.path, cellPath), + }, + }); + } + } + + return next(); +} + export function withTablesCopyPasteBehavior(editor: T): T { - const { getFragment } = editor; + const parent = { + getFragment: editor.getFragment, + }; editor.getFragment = () => { - if (editor.selection) { - const cellEntry = findParentCell(editor); - - if (cellEntry && isRangeInside(editor.selection, cellEntry[1])) { - const [cell, cellPath] = cellEntry; - const { focus, anchor } = editor.selection; - - return Node.fragment(cell, { - anchor: { - offset: anchor.offset, - path: Path.relative(anchor.path, cellPath), - }, - focus: { - offset: focus.offset, - path: Path.relative(focus.path, cellPath), - }, - }); - } - } - - return getFragment(); + return getFragment(editor, parent.getFragment); }; return editor; diff --git a/packages/slate-tables/src/core/withTablesDeleteBehavior.ts b/packages/slate-tables/src/core/withTablesDeleteBehavior.ts index 892e67e87..624fb0705 100644 --- a/packages/slate-tables/src/core/withTablesDeleteBehavior.ts +++ b/packages/slate-tables/src/core/withTablesDeleteBehavior.ts @@ -1,21 +1,36 @@ -import type { Location } from 'slate'; +import type { Location, TextUnit } from 'slate'; import { Editor, Range, Point } from 'slate'; import type { TablesEditor } from '../TablesEditor'; +export function deleteBackward( + editor: TablesEditor, + unit: TextUnit, + next: Editor['deleteBackward'], +) { + if (canDeleteInTableCell(editor, Editor.start)) { + next(unit); + } +} + +export function deleteForward(editor: TablesEditor, unit: TextUnit, next: Editor['deleteForward']) { + if (canDeleteInTableCell(editor, Editor.end)) { + next(unit); + } +} + export function withTablesDeleteBehavior(editor: T): T { - const { deleteBackward, deleteForward } = editor; + const parent = { + deleteBackward: editor.deleteBackward, + deleteForward: editor.deleteForward, + }; editor.deleteBackward = (unit) => { - if (canDeleteInTableCell(editor, Editor.start)) { - deleteBackward(unit); - } + deleteBackward(editor, unit, parent.deleteBackward); }; editor.deleteForward = (unit) => { - if (canDeleteInTableCell(editor, Editor.end)) { - deleteForward(unit); - } + deleteForward(editor, unit, parent.deleteForward); }; return editor; diff --git a/packages/slate-tables/src/index.ts b/packages/slate-tables/src/index.ts index d23295d14..55fd18c5b 100644 --- a/packages/slate-tables/src/index.ts +++ b/packages/slate-tables/src/index.ts @@ -1,3 +1,14 @@ export * from './TablesEditor'; -export * from './withTables'; -export * from './core'; + +// The all-in-one Slate editor plugin for tables +export { withTables } from './withTables'; +// Keystroke handler +export { onKeyDown } from './core'; + +// Separate parts of the `withTables()` plugin, if you need more control of how they're applied +// - Copy & Paste behavior +export { withTablesCopyPasteBehavior, getFragment } from './core'; +// - Backspace & Delete behavior +export { withTablesDeleteBehavior, deleteBackward, deleteForward } from './core'; +// - Normalization +export { withNormalization, normalizeNode } from './withNormalization'; diff --git a/packages/slate-tables/src/normalization/insertMissingCells.ts b/packages/slate-tables/src/normalization/insertMissingCells.ts index 3c940b08d..ba5c93c1b 100644 --- a/packages/slate-tables/src/normalization/insertMissingCells.ts +++ b/packages/slate-tables/src/normalization/insertMissingCells.ts @@ -1,21 +1,16 @@ import { times } from '@technically/lodash'; -import { Node, Transforms } from 'slate'; -import { Path } from 'slate'; +import { Node, type NodeEntry, Path, Transforms } from 'slate'; import type { TableRowNode } from '../nodes'; import { TablesEditor } from '../TablesEditor'; -export function insertMissingCells(editor: TablesEditor, path: Path) { - const table = Node.get(editor, path); - - if (!editor.isTableNode(table)) { +export function insertMissingCells(editor: TablesEditor, [node, path]: NodeEntry) { + if (!editor.isTableNode(node)) { return false; } const maxWidth = Math.max( - ...table.children.map((node) => - editor.isTableRowNode(node) ? calculateRowWidth(node) : 0, - ), + ...node.children.map((node) => (editor.isTableRowNode(node) ? calculateRowWidth(node) : 0)), ); for (const [row, rowPath] of Node.children(editor, path)) { diff --git a/packages/slate-tables/src/normalization/removeEmptyRows.ts b/packages/slate-tables/src/normalization/removeEmptyRows.ts index 97743f6ab..d022dc4ac 100644 --- a/packages/slate-tables/src/normalization/removeEmptyRows.ts +++ b/packages/slate-tables/src/normalization/removeEmptyRows.ts @@ -1,11 +1,8 @@ -import { Node, Transforms } from 'slate'; -import type { Path } from 'slate'; +import { Node, type NodeEntry, Transforms } from 'slate'; import type { TablesEditor } from '../TablesEditor'; -export function removeEmptyRows(editor: TablesEditor, path: Path) { - const table = Node.get(editor, path); - +export function removeEmptyRows(editor: TablesEditor, [table, path]: NodeEntry) { if (!editor.isTableNode(table)) { return false; } diff --git a/packages/slate-tables/src/normalization/splitColSpanCells.ts b/packages/slate-tables/src/normalization/splitColSpanCells.ts index b4d0fb011..598a1ec8c 100644 --- a/packages/slate-tables/src/normalization/splitColSpanCells.ts +++ b/packages/slate-tables/src/normalization/splitColSpanCells.ts @@ -1,14 +1,11 @@ import { times } from '@technically/lodash'; -import { Path } from 'slate'; -import { Node, Transforms } from 'slate'; +import { Node, type NodeEntry, Transforms, Path } from 'slate'; import { Editor } from 'slate'; import type { TableCellNode } from '../nodes'; import { TablesEditor } from '../TablesEditor'; -export function splitColSpanCells(editor: TablesEditor, path: Path) { - const node = Node.get(editor, path); - +export function splitColSpanCells(editor: TablesEditor, [node, path]: NodeEntry) { if (!editor.isTableRowNode(node)) { return false; } diff --git a/packages/slate-tables/src/normalization/splitRowSpanCells.ts b/packages/slate-tables/src/normalization/splitRowSpanCells.ts index b4748e2be..927122d3d 100644 --- a/packages/slate-tables/src/normalization/splitRowSpanCells.ts +++ b/packages/slate-tables/src/normalization/splitRowSpanCells.ts @@ -1,14 +1,10 @@ import { times } from '@technically/lodash'; -import { Path } from 'slate'; -import { Node, Transforms } from 'slate'; -import { Editor } from 'slate'; +import { Editor, Node, type NodeEntry, Path, Transforms } from 'slate'; import type { TableCellNode } from '../nodes'; import { TablesEditor } from '../TablesEditor'; -export function splitRowSpanCells(editor: TablesEditor, path: Path) { - const node = Node.get(editor, path); - +export function splitRowSpanCells(editor: TablesEditor, [node, path]: NodeEntry) { if (!editor.isTableNode(node)) { return false; } diff --git a/packages/slate-tables/src/withNormalization.ts b/packages/slate-tables/src/withNormalization.ts index 62a61257e..0495a95d9 100644 --- a/packages/slate-tables/src/withNormalization.ts +++ b/packages/slate-tables/src/withNormalization.ts @@ -1,29 +1,32 @@ -import * as normalization from './normalization'; -import type { TablesEditor } from './TablesEditor'; +import type { NodeEntry } from 'slate'; -const normalizers = [ - normalization.removeEmptyRows, - normalization.splitRowSpanCells, - normalization.splitColSpanCells, - normalization.insertMissingCells, -]; +import * as Normalizations from './normalization'; +import type { TablesEditor } from './TablesEditor'; export function withNormalization(editor: TablesEditor) { - const { normalizeNode } = editor; + const parent = { + normalizeNode: editor.normalizeNode, + }; editor.normalizeNode = (entry) => { - const [, path] = entry; - - for (const normalize of normalizers) { - const changed = normalize(editor, path); - - if (changed) { - return; - } - } - - normalizeNode(entry); + return normalizeNode(editor, entry) || parent.normalizeNode(entry); }; return editor; } + +export function normalizeNode(editor: TablesEditor, entry: NodeEntry): boolean { + return ( + normalizeNode.insertMissingCells(editor, entry) || + normalizeNode.removeEmptyRows(editor, entry) || + normalizeNode.splitColSpanCells(editor, entry) || + normalizeNode.splitRowSpanCells(editor, entry) + ); +} + +export namespace normalizeNode { + export const insertMissingCells = Normalizations.insertMissingCells; + export const splitColSpanCells = Normalizations.splitColSpanCells; + export const splitRowSpanCells = Normalizations.splitRowSpanCells; + export const removeEmptyRows = Normalizations.removeEmptyRows; +} From a5bf2f58e0528836db52bcf121dbd4b3bd20312f Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 12:18:51 +0300 Subject: [PATCH 46/54] [CARE-1802] Add hooks for Editor `deleteBackward()` and `deleteForward()` methods --- .../src/extensions/ExtensionsEditor.ts | 39 ++++++++++++++++++- packages/slate-commons/src/types/Extension.ts | 4 +- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index b389ac40c..15c9e97ba 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -1,5 +1,5 @@ import { isNotUndefined } from '@technically/is-not-undefined'; -import type { BaseEditor, Descendant, Element, Node } from 'slate'; +import type { BaseEditor, Descendant, Editor, Element, Node, TextUnit } from 'slate'; import type { HistoryEditor } from 'slate-history'; import type { ReactEditor } from 'slate-react'; @@ -39,6 +39,8 @@ export function withExtensions; + const extensionsEditor: T & ExtensionsEditor = Object.assign(editor, { extensions, + deleteBackward(unit) { + const handlers = extensionsEditor.extensions + .map((ext) => ext.deleteBackward) + .filter(isNotUndefined); + + function next(unit: TextUnit) { + const handler = handlers.shift(); + if (handler) { + handler(unit, next); + } else { + parent.deleteBackward(unit); + } + } + + next(unit); + }, + deleteForward(unit) { + const handlers = extensionsEditor.extensions + .map((ext) => ext.deleteForward) + .filter(isNotUndefined); + + function next(unit: TextUnit) { + const handler = handlers.shift(); + if (handler) { + handler(unit, next); + } else { + parent.deleteForward(unit); + } + } + + next(unit); + }, isElementEqual(node, another): boolean | undefined { for (const extension of extensionsEditor.extensions) { const ret = extension.isElementEqual?.(node, another); diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index a17879c98..b36062c54 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -1,4 +1,4 @@ -import type { Descendant, Element, Node } from 'slate'; +import type { Descendant, Editor, Element, Node, TextUnit } from 'slate'; import type { DataTransferHandler } from './DataTransferHandler'; import type { DecorateFactory } from './DecorateFactory'; @@ -17,6 +17,8 @@ import type { WithOverrides } from './WithOverrides'; export interface Extension { id: string; decorate?: DecorateFactory; // OK + deleteBackward?: (unit: TextUnit, next: Editor['deleteBackward']) => void; + deleteForward?: (unit: TextUnit, next: Editor['deleteForward']) => void; deserialize?: DeserializeHtml; // OK insertBreak?: LineBreakHandler; /** From de130cd0f70e31bc189cf92212e94370cd765f44 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 12:22:57 +0300 Subject: [PATCH 47/54] [CARE-1802] Change `TablesExtension` to work without `withOverrides` call --- .../src/extensions/tables/TablesExtension.tsx | 72 ++++++++++++------- packages/slate-tables/src/index.ts | 2 + .../slate-tables/src/withNormalization.ts | 2 +- packages/slate-tables/src/withTables.ts | 12 ++-- packages/slate-tables/src/withTablesSchema.ts | 12 ++++ 5 files changed, 67 insertions(+), 33 deletions(-) create mode 100644 packages/slate-tables/src/withTablesSchema.ts diff --git a/packages/slate-editor/src/extensions/tables/TablesExtension.tsx b/packages/slate-editor/src/extensions/tables/TablesExtension.tsx index a1c025340..ffa3ac140 100644 --- a/packages/slate-editor/src/extensions/tables/TablesExtension.tsx +++ b/packages/slate-editor/src/extensions/tables/TablesExtension.tsx @@ -2,9 +2,10 @@ import { useRegisterExtension } from '@prezly/slate-commons'; import { onKeyDown, TablesEditor, - withTables, - withTablesCopyPasteBehavior, - withTablesDeleteBehavior, + withTablesSchema, + getFragment, + deleteBackward, + deleteForward, } from '@prezly/slate-tables'; import { isTableCellNode, @@ -14,10 +15,9 @@ import { type TableNode, type TableRowNode, } from '@prezly/slate-types'; -import { flow } from '@technically/lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; import type { Element } from 'slate'; -import type { RenderElementProps } from 'slate-react'; +import { useSlateStatic, type RenderElementProps } from 'slate-react'; import { composeElementDeserializer } from '#modules/html-deserialization'; @@ -37,10 +37,45 @@ interface Parameters { } export function TablesExtension({ createDefaultElement }: Parameters) { + const editor = useSlateStatic(); + + // Register Tables editor extension schema + useEffect(() => { + withTablesSchema(editor, { + createContentNode: createDefaultElement, + createTableNode: ({ children, ...props }) => + createTableNode({ + ...props, + children: children as TableNode['children'] | undefined, + }), + createTableRowNode: ({ children, ...props }) => + createTableRowNode({ + ...props, + children: children as TableRowNode['children'] | undefined, + }), + createTableCellNode, + isTableNode, + isTableRowNode, + isTableCellNode, + }); + }, [editor]); + return useRegisterExtension({ id: EXTENSION_ID, isRichBlock: isTableNode, normalizeNode: [normalizeTableAttributes, normalizeRowAttributes, normalizeCellAttributes], + deleteBackward: (unit, next) => { + if (TablesEditor.isTablesEditor(editor)) { + return deleteBackward(editor, unit, next); + } + next(unit); + }, + deleteForward: (unit, next) => { + if (TablesEditor.isTablesEditor(editor)) { + return deleteForward(editor, unit, next); + } + next(unit); + }, deserialize: { element: composeElementDeserializer({ TABLE: (): TableNode => { @@ -103,26 +138,11 @@ export function TablesExtension({ createDefaultElement }: Parameters) { return undefined; }, - withOverrides: (editor) => { - const tablesEditor = withTables(editor, { - createContentNode: createDefaultElement, - createTableNode: ({ children, ...props }) => - createTableNode({ - ...props, - children: children as TableNode['children'] | undefined, - }), - createTableRowNode: ({ children, ...props }) => - createTableRowNode({ - ...props, - children: children as TableRowNode['children'] | undefined, - }), - createTableCellNode, - isTableNode, - isTableRowNode, - isTableCellNode, - }); - - return flow([withTablesCopyPasteBehavior, withTablesDeleteBehavior])(tablesEditor); + getFragment: (next) => { + if (TablesEditor.isTablesEditor(editor)) { + return getFragment(editor, next); + } + return next(); }, }); } diff --git a/packages/slate-tables/src/index.ts b/packages/slate-tables/src/index.ts index 55fd18c5b..2f712d2bc 100644 --- a/packages/slate-tables/src/index.ts +++ b/packages/slate-tables/src/index.ts @@ -6,6 +6,8 @@ export { withTables } from './withTables'; export { onKeyDown } from './core'; // Separate parts of the `withTables()` plugin, if you need more control of how they're applied +// - Bind the TablesSchema definition +export { withTablesSchema } from './withTablesSchema'; // - Copy & Paste behavior export { withTablesCopyPasteBehavior, getFragment } from './core'; // - Backspace & Delete behavior diff --git a/packages/slate-tables/src/withNormalization.ts b/packages/slate-tables/src/withNormalization.ts index 0495a95d9..ccd9942e9 100644 --- a/packages/slate-tables/src/withNormalization.ts +++ b/packages/slate-tables/src/withNormalization.ts @@ -3,7 +3,7 @@ import type { NodeEntry } from 'slate'; import * as Normalizations from './normalization'; import type { TablesEditor } from './TablesEditor'; -export function withNormalization(editor: TablesEditor) { +export function withNormalization(editor: T): T { const parent = { normalizeNode: editor.normalizeNode, }; diff --git a/packages/slate-tables/src/withTables.ts b/packages/slate-tables/src/withTables.ts index 12df1ee84..a34385229 100644 --- a/packages/slate-tables/src/withTables.ts +++ b/packages/slate-tables/src/withTables.ts @@ -1,12 +1,12 @@ import type { Editor } from 'slate'; +import { withTablesCopyPasteBehavior, withTablesDeleteBehavior } from './core'; import type { TablesEditor, TablesSchema } from './TablesEditor'; import { withNormalization } from './withNormalization'; +import { withTablesSchema } from './withTablesSchema'; -export function withTables(editor: T, schema: TablesSchema) { - const tablesEditor = Object.assign(editor, { - ...schema, - }) as T & TablesEditor; - - return withNormalization(tablesEditor); +export function withTables(editor: T, schema: TablesSchema): T & TablesEditor { + return withTablesDeleteBehavior( + withTablesCopyPasteBehavior(withNormalization(withTablesSchema(editor, schema))), + ); } diff --git a/packages/slate-tables/src/withTablesSchema.ts b/packages/slate-tables/src/withTablesSchema.ts new file mode 100644 index 000000000..df6718596 --- /dev/null +++ b/packages/slate-tables/src/withTablesSchema.ts @@ -0,0 +1,12 @@ +import type { Editor } from 'slate'; + +import type { TablesEditor, TablesSchema } from './TablesEditor'; + +export function withTablesSchema( + editor: T, + schema: TablesSchema, +): T & TablesEditor { + return Object.assign(editor, { + ...schema, + }) as T & TablesEditor; +} From af19597baaf41a80b84e07877dd7e5f416b597eb Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 12:23:49 +0300 Subject: [PATCH 48/54] [CARE-1802] Fix TS type error --- packages/slate-editor/src/modules/editor/Extensions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/slate-editor/src/modules/editor/Extensions.tsx b/packages/slate-editor/src/modules/editor/Extensions.tsx index ceeda5e35..d1bffb647 100644 --- a/packages/slate-editor/src/modules/editor/Extensions.tsx +++ b/packages/slate-editor/src/modules/editor/Extensions.tsx @@ -236,7 +236,7 @@ export function Extensions({ {withImages && ( <> { + onImagesPasted={(images) => { EventsEditor.dispatchEvent(editor, 'images-pasted', { imagesCount: images.length, isEmpty: EditorCommands.isEmpty(editor), From 30c054a84f1202f2977f5dbc384109cf5a0323d2 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 12:31:57 +0300 Subject: [PATCH 49/54] [CARE-1802] Rewrite `FlashNodesExtension` to work without `withOverrides` --- .../flash-nodes/FlashNodesEditor.ts | 13 ++++++++++ .../flash-nodes/FlashNodesExtension.tsx | 26 +++++++------------ .../src/extensions/flash-nodes/index.ts | 5 ++-- .../src/extensions/flash-nodes/types.ts | 6 ----- .../extensions/flash-nodes/withFlashNodes.ts | 20 ++++++++++++++ .../snippet/lib/useFloatingSnippetInput.ts | 5 +++- packages/slate-editor/src/index.ts | 4 +-- 7 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 packages/slate-editor/src/extensions/flash-nodes/FlashNodesEditor.ts delete mode 100644 packages/slate-editor/src/extensions/flash-nodes/types.ts create mode 100644 packages/slate-editor/src/extensions/flash-nodes/withFlashNodes.ts diff --git a/packages/slate-editor/src/extensions/flash-nodes/FlashNodesEditor.ts b/packages/slate-editor/src/extensions/flash-nodes/FlashNodesEditor.ts new file mode 100644 index 000000000..7fc757ef3 --- /dev/null +++ b/packages/slate-editor/src/extensions/flash-nodes/FlashNodesEditor.ts @@ -0,0 +1,13 @@ +import type { Node, BaseEditor, Editor } from 'slate'; + +export interface FlashNodesEditor extends BaseEditor { + flashNodes(from: Node | undefined, to: Node | undefined): void; + nodesToFlash: Array<[top: Node, bottom: Node]>; +} + +export namespace FlashNodesEditor { + export function isFlashEditor(editor: T): editor is T & FlashNodesEditor { + const candidate = editor as T & Partial; + return typeof candidate.flashNodes === 'function' && Array.isArray(candidate.nodesToFlash); + } +} diff --git a/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx b/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx index 3af078e1c..4f89dded9 100644 --- a/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx +++ b/packages/slate-editor/src/extensions/flash-nodes/FlashNodesExtension.tsx @@ -1,29 +1,21 @@ -import { useRegisterExtension } from '@prezly/slate-commons'; -import React from 'react'; +import React, { useEffect } from 'react'; +import { useSlateStatic } from 'slate-react'; import { FlashNodes } from './components/FlashNodes'; +import { withFlashNodes } from './withFlashNodes'; + +export const EXTENSION_ID = 'FlashNodesExtension'; export interface Parameters { containerElement: HTMLElement | null | undefined; } export function FlashNodesExtension({ containerElement }: Parameters) { - useRegisterExtension({ - id: 'FlashNodesExtension', - withOverrides: (editor) => { - editor.nodesToFlash = []; - - editor.flash = (from, to) => { - if (!from || !to) { - return; - } - - editor.nodesToFlash.push([from, to]); - }; + const editor = useSlateStatic(); - return editor; - }, - }); + useEffect(() => { + withFlashNodes(editor); + }, [editor]); return ; } diff --git a/packages/slate-editor/src/extensions/flash-nodes/index.ts b/packages/slate-editor/src/extensions/flash-nodes/index.ts index 4900cb8cf..64e3c1c4e 100644 --- a/packages/slate-editor/src/extensions/flash-nodes/index.ts +++ b/packages/slate-editor/src/extensions/flash-nodes/index.ts @@ -1,3 +1,2 @@ -export * from './FlashNodesExtension'; -export * from './components/FlashNodes'; -export * from './types'; +export { FlashNodesEditor } from './FlashNodesEditor'; +export { FlashNodesExtension, EXTENSION_ID, type Parameters } from './FlashNodesExtension'; diff --git a/packages/slate-editor/src/extensions/flash-nodes/types.ts b/packages/slate-editor/src/extensions/flash-nodes/types.ts deleted file mode 100644 index 84ffe575f..000000000 --- a/packages/slate-editor/src/extensions/flash-nodes/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Node, BaseEditor } from 'slate'; - -export interface FlashEditor extends BaseEditor { - flash(from: Node | undefined, to: Node | undefined): void; - nodesToFlash: Array<[top: Node, bottom: Node]>; -} diff --git a/packages/slate-editor/src/extensions/flash-nodes/withFlashNodes.ts b/packages/slate-editor/src/extensions/flash-nodes/withFlashNodes.ts new file mode 100644 index 000000000..1ba93b8ab --- /dev/null +++ b/packages/slate-editor/src/extensions/flash-nodes/withFlashNodes.ts @@ -0,0 +1,20 @@ +import type { Editor } from 'slate'; + +import type { FlashNodesEditor } from './FlashNodesEditor'; + +export function withFlashNodes(editor: T): T & FlashNodesEditor { + const candidate: T & Partial = editor; + + const { + nodesToFlash = [], + flashNodes = (from, to) => { + if (!from || !to) { + return; + } + + nodesToFlash.push([from, to]); + }, + } = candidate; + + return Object.assign(editor, { nodesToFlash, flashNodes: flash }); +} diff --git a/packages/slate-editor/src/extensions/snippet/lib/useFloatingSnippetInput.ts b/packages/slate-editor/src/extensions/snippet/lib/useFloatingSnippetInput.ts index 80ac60fa0..46ea6bfbd 100644 --- a/packages/slate-editor/src/extensions/snippet/lib/useFloatingSnippetInput.ts +++ b/packages/slate-editor/src/extensions/snippet/lib/useFloatingSnippetInput.ts @@ -3,6 +3,7 @@ import type { DocumentNode } from '@prezly/slate-types'; import { useCallback, useState } from 'react'; import { useSlateStatic } from 'slate-react'; +import { FlashNodesEditor } from '#extensions/flash-nodes'; import { EventsEditor } from '#modules/events'; export function useFloatingSnippetInput() { @@ -37,7 +38,9 @@ export function useFloatingSnippetInput() { EditorCommands.insertNodes(editor, node.children, { mode: 'highest' }); - editor.flash(node.children.at(0), node.children.at(-1)); + if (FlashNodesEditor.isFlashEditor(editor)) { + editor.flashNodes(node.children.at(0), node.children.at(-1)); + } savedSelection.restore(editor, { focus: true }); } catch (error) { console.error(error); diff --git a/packages/slate-editor/src/index.ts b/packages/slate-editor/src/index.ts index bad95592c..d9d0cff85 100644 --- a/packages/slate-editor/src/index.ts +++ b/packages/slate-editor/src/index.ts @@ -24,15 +24,13 @@ import type { BaseEditor } from 'slate'; import type { HistoryEditor } from 'slate-history'; import type { ReactEditor } from 'slate-react'; -import type { FlashEditor } from '#extensions/flash-nodes'; import type { DefaultTextBlockEditor } from '#modules/editor'; type Editor = BaseEditor & ReactEditor & HistoryEditor & ExtensionsEditor & - DefaultTextBlockEditor & - FlashEditor; + DefaultTextBlockEditor; declare module 'slate' { interface CustomTypes { From 144d476741c8471304f17c6b06f356cb5ac058e8 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 12:32:56 +0300 Subject: [PATCH 50/54] [CARE-1802] Completely drop `Extension.withOverrides()` feature It's no longer used by any of the extensions --- .../slate-commons/src/extensions/useRegisterExtension.ts | 2 -- packages/slate-commons/src/types/Extension.ts | 6 ------ packages/slate-commons/src/types/WithOverrides.ts | 3 --- packages/slate-commons/src/types/index.ts | 1 - 4 files changed, 12 deletions(-) delete mode 100644 packages/slate-commons/src/types/WithOverrides.ts diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts index 2ed82ab97..c8c4eebd3 100644 --- a/packages/slate-commons/src/extensions/useRegisterExtension.ts +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -27,7 +27,6 @@ export function useRegisterExtension(extension: Extension): null { setFragmentData, undo, redo, - withOverrides, ...rest } = extension; @@ -60,7 +59,6 @@ export function useRegisterExtension(extension: Extension): null { setFragmentData, undo, redo, - withOverrides, ], ); diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index b36062c54..6e855df44 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -12,7 +12,6 @@ import type { RenderElement } from './RenderElement'; import type { RenderLeaf } from './RenderLeaf'; import type { Serialize } from './Serialize'; import type { TextInsertionHandler } from './TextInsertionHandler'; -import type { WithOverrides } from './WithOverrides'; export interface Extension { id: string; @@ -59,9 +58,4 @@ export interface Extension { undo?: HistoryHandler; redo?: HistoryHandler; - - /** - * @deprecated Please do not use this. We're going to drop this functionality soon. - */ - withOverrides?: WithOverrides; } diff --git a/packages/slate-commons/src/types/WithOverrides.ts b/packages/slate-commons/src/types/WithOverrides.ts deleted file mode 100644 index 4fa3d7284..000000000 --- a/packages/slate-commons/src/types/WithOverrides.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { Editor } from 'slate'; - -export type WithOverrides = (editor: T) => T; diff --git a/packages/slate-commons/src/types/index.ts b/packages/slate-commons/src/types/index.ts index 223a1ab11..988bb5339 100644 --- a/packages/slate-commons/src/types/index.ts +++ b/packages/slate-commons/src/types/index.ts @@ -11,4 +11,3 @@ export type { OnKeyDown } from './OnKeyDown'; export type { RenderElement } from './RenderElement'; export type { RenderLeaf } from './RenderLeaf'; export type { TextInsertionHandler } from './TextInsertionHandler'; -export type { WithOverrides } from './WithOverrides'; From 1f8606a16edd6921260b58a46055f2a29499be99 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 12:46:22 +0300 Subject: [PATCH 51/54] [CARE-1802] Fix useRegisterExtension effect dependencies --- .../slate-commons/src/extensions/useRegisterExtension.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts index c8c4eebd3..0035c7878 100644 --- a/packages/slate-commons/src/extensions/useRegisterExtension.ts +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -10,6 +10,8 @@ export function useRegisterExtension(extension: Extension): null { const { id, decorate, + deleteBackward, + deleteForward, deserialize, isElementEqual, isInline, @@ -24,6 +26,7 @@ export function useRegisterExtension(extension: Extension): null { renderElement, renderLeaf, serialize, + getFragment, setFragmentData, undo, redo, @@ -42,6 +45,8 @@ export function useRegisterExtension(extension: Extension): null { manager, id, decorate, + deleteBackward, + deleteForward, deserialize, isElementEqual, isInline, @@ -56,6 +61,7 @@ export function useRegisterExtension(extension: Extension): null { renderElement, renderLeaf, serialize, + getFragment, setFragmentData, undo, redo, From 9552dac17611138f22366aa4777222b807f72708 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 13:00:17 +0300 Subject: [PATCH 52/54] [CARE-1802] Make ExtensionsEditor code type-strict to catch possible human errors --- .../src/extensions/ExtensionsEditor.ts | 58 ++++++++++--------- packages/slate-commons/src/types/Extension.ts | 58 ++++++++++++------- 2 files changed, 67 insertions(+), 49 deletions(-) diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index 15c9e97ba..2f62cea0c 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -3,7 +3,7 @@ import type { BaseEditor, Descendant, Editor, Element, Node, TextUnit } from 'sl import type { HistoryEditor } from 'slate-history'; import type { ReactEditor } from 'slate-react'; -import type { Extension } from '../types'; +import type { EditorMethodsHooks, Extension } from '../types/Extension'; // a deep import is necessary to keep `EditorMethodsHooks` package-scoped export interface ExtensionsEditor extends BaseEditor { extensions: Extension[]; @@ -53,8 +53,7 @@ export function withExtensions; - const extensionsEditor: T & ExtensionsEditor = Object.assign(editor, { - extensions, + const methodsHooks = { deleteBackward(unit) { const handlers = extensionsEditor.extensions .map((ext) => ext.deleteBackward) @@ -87,15 +86,6 @@ export function withExtensions extension.serialize?.(result) ?? result, - nodes, - ); - }, getFragment() { const handlers = extensionsEditor.extensions .map((ext) => ext.getFragment) @@ -247,7 +223,35 @@ export function withExtensions); + } satisfies Pick; + + const extensionsEditor: T & ExtensionsEditor = Object.assign(editor, { + extensions, + isElementEqual(node, another): boolean | undefined { + for (const extension of extensionsEditor.extensions) { + const ret = extension.isElementEqual?.(node, another); + if (typeof ret !== 'undefined') { + return ret; + } + } + return undefined; + }, + isRichBlock(node): boolean { + for (const extension of extensionsEditor.extensions) { + if (extension.isRichBlock?.(node)) { + return true; + } + } + return false; + }, + serialize(nodes) { + return extensionsEditor.extensions.reduce( + (result, extension) => extension.serialize?.(result) ?? result, + nodes, + ); + }, + ...methodsHooks, + } satisfies Omit); return extensionsEditor; } diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index 6e855df44..9475421d1 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -13,36 +13,34 @@ import type { RenderLeaf } from './RenderLeaf'; import type { Serialize } from './Serialize'; import type { TextInsertionHandler } from './TextInsertionHandler'; -export interface Extension { - id: string; - decorate?: DecorateFactory; // OK +/** + * Extension hooks that affect component props and thus should be triggering a React re-render. + */ +export interface EditorPropsHooks { + decorate?: DecorateFactory; + onDOMBeforeInput?: OnDOMBeforeInput; + onKeyDown?: OnKeyDown; + renderElement?: RenderElement; + renderLeaf?: RenderLeaf; +} + +/** + * Extension hooks that affect the Editor singleton callback-based functionality, and don't require a re-render. + */ +export interface EditorMethodsHooks { deleteBackward?: (unit: TextUnit, next: Editor['deleteBackward']) => void; deleteForward?: (unit: TextUnit, next: Editor['deleteForward']) => void; - deserialize?: DeserializeHtml; // OK insertBreak?: LineBreakHandler; /** * Hook into ReactEditor's `insertData()` method. * Call `next()` to allow other extensions (or the editor) handling the call. * @see https://docs.slatejs.org/libraries/slate-react/react-editor */ - insertData?: DataTransferHandler; // OK - insertText?: TextInsertionHandler; // OK - /** - * Compare two elements. - * `children` arrays can be omitted from the comparison, - * as the outer code will compare them anyway. - */ - isElementEqual?: (node: Element, another: Element) => boolean | undefined; - isInline?: (node: Node) => boolean; // OK - isRichBlock?: (node: Node) => boolean; // OK - isVoid?: (node: Node) => boolean; // OK - normalizeNode?: Normalize | Normalize[]; // OK - onDOMBeforeInput?: OnDOMBeforeInput; // OK - onKeyDown?: OnKeyDown; // OK - renderElement?: RenderElement; // OK - renderLeaf?: RenderLeaf; // OK - serialize?: Serialize; // OK - + insertData?: DataTransferHandler; + insertText?: TextInsertionHandler; + isInline?: (node: Node) => boolean; + isVoid?: (node: Node) => boolean; + normalizeNode?: Normalize | Normalize[]; /** * Hook into Editor's `getFragment()` method. * Call `next()` to allow other extensions (or the editor) handling the call. @@ -59,3 +57,19 @@ export interface Extension { undo?: HistoryHandler; redo?: HistoryHandler; } + +export interface Extension extends EditorMethodsHooks, EditorPropsHooks { + id: string; + + deserialize?: DeserializeHtml; + serialize?: Serialize; + + /** + * Compare two elements. + * `children` arrays can be omitted from the comparison, + * as the outer code will compare them anyway. + */ + isElementEqual?: (node: Element, another: Element) => boolean | undefined; + + isRichBlock?: (node: Node) => boolean; +} From fff36d8f8d31c36561270d0aebc5e8c0626261e6 Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 13:28:27 +0300 Subject: [PATCH 53/54] [CARE-1802] Update editor factory to not require `extensions` or `plugins` from outside --- .../src/modules/editor/Editor.tsx | 7 +--- .../src/modules/editor/createEditor.ts | 17 ++------- .../slate-editor/src/modules/editor/types.ts | 7 +--- .../src/modules/editor/useCreateEditor.ts | 38 +++---------------- 4 files changed, 10 insertions(+), 59 deletions(-) diff --git a/packages/slate-editor/src/modules/editor/Editor.tsx b/packages/slate-editor/src/modules/editor/Editor.tsx index 9ff074668..fa76959d3 100644 --- a/packages/slate-editor/src/modules/editor/Editor.tsx +++ b/packages/slate-editor/src/modules/editor/Editor.tsx @@ -72,7 +72,6 @@ export const Editor = forwardRef((props, forwardedRef) = blurOnOutsideClick = false, onKeyDown = noop, placeholder, - plugins, popperMenuOptions = {}, readOnly, style, @@ -120,11 +119,7 @@ export const Editor = forwardRef((props, forwardedRef) = // TODO: Wire `onOperationStart` and `onOperationEnd` to the Placeholder extension // const { onOperationEnd, onOperationStart } = usePendingOperation(onIsOperationPendingChange); - const editor = useCreateEditor({ - events, - // extensions, // FIXME - plugins, - }); + const editor = useCreateEditor({ events }); const [getInitialValue, setInitialValue] = useGetSet(() => EditorCommands.roughlyNormalizeValue(editor, externalInitialValue), diff --git a/packages/slate-editor/src/modules/editor/createEditor.ts b/packages/slate-editor/src/modules/editor/createEditor.ts index 0c2a022ac..af65bae3a 100644 --- a/packages/slate-editor/src/modules/editor/createEditor.ts +++ b/packages/slate-editor/src/modules/editor/createEditor.ts @@ -1,11 +1,9 @@ import { withBreaksOnExpandedSelection, withBreaksOnVoidNodes, + withExtensions, withUserFriendlyDeleteBehavior, } from '@prezly/slate-commons'; -import type { Extension } from '@prezly/slate-commons'; -import type { WithOverrides } from '@prezly/slate-commons'; -import { isNotUndefined } from '@technically/is-not-undefined'; import { flow } from '@technically/lodash'; import type { Editor } from 'slate'; import { withHistory } from 'slate-history'; @@ -16,24 +14,15 @@ import { withNodesHierarchy, hierarchySchema } from '#modules/nodes-hierarchy'; import { withDefaultTextBlock } from './plugins'; -export function createEditor( - baseEditor: Editor, - getExtensions: () => Extension[], - plugins: WithOverrides[] = [], -) { - const overrides = getExtensions() - .map(({ withOverrides }) => withOverrides) - .filter(isNotUndefined); - +export function createEditor(baseEditor: Editor) { return flow([ withReact, withHistory, + withExtensions, withNodesHierarchy(hierarchySchema), withBreaksOnExpandedSelection, withBreaksOnVoidNodes, withDefaultTextBlock(createParagraph), withUserFriendlyDeleteBehavior, - ...overrides, - ...plugins, ])(baseEditor); } diff --git a/packages/slate-editor/src/modules/editor/types.ts b/packages/slate-editor/src/modules/editor/types.ts index d9436a497..a0c30894f 100644 --- a/packages/slate-editor/src/modules/editor/types.ts +++ b/packages/slate-editor/src/modules/editor/types.ts @@ -2,7 +2,7 @@ import type { Events } from '@prezly/events'; import type { Decorate, EditorCommands } from '@prezly/slate-commons'; import type { Alignment } from '@prezly/slate-types'; import type { CSSProperties, KeyboardEvent, ReactNode } from 'react'; -import type { Editor, Element, Node } from 'slate'; +import type { Element, Node } from 'slate'; import type { Transforms } from 'slate'; import type { AllowedBlocksExtensionConfiguration } from '#extensions/allowed-blocks'; @@ -91,11 +91,6 @@ export interface EditorProps { onChange: (value: Value) => void; onKeyDown?: (event: KeyboardEvent) => void; placeholder?: ReactNode; - /** - * [WARNING] this prop is read by EditorV4 only once, when mounting. - * Any changes to it will be ignored. - */ - plugins?: ((editor: T) => T)[]; popperMenuOptions?: PopperOptionsContextType; readOnly?: boolean; style?: CSSProperties; diff --git a/packages/slate-editor/src/modules/editor/useCreateEditor.ts b/packages/slate-editor/src/modules/editor/useCreateEditor.ts index 2cdd2da0a..f506dff6a 100644 --- a/packages/slate-editor/src/modules/editor/useCreateEditor.ts +++ b/packages/slate-editor/src/modules/editor/useCreateEditor.ts @@ -1,12 +1,8 @@ import type { Events } from '@prezly/events'; -import type { Extension } from '@prezly/slate-commons'; -import { withExtensions } from '@prezly/slate-commons'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useState } from 'react'; import type { Editor } from 'slate'; import { createEditor as createSlateEditor } from 'slate'; -import { useLatest } from '#lib'; - import type { EditorEventMap } from '../events'; import { withEvents } from '../events'; @@ -14,36 +10,12 @@ import { createEditor } from './createEditor'; interface Parameters { events: Events; - extensions: Extension[]; - plugins?: ((editor: T) => T)[] | undefined; } -type NonUndefined = T extends undefined ? never : T; - -const DEFAULT_PLUGINS: NonUndefined = []; - -export function useCreateEditor({ - events, - extensions, - plugins = DEFAULT_PLUGINS, -}: Parameters): Editor { - // We have to make sure that editor is created only once. - // We do it by ensuring dependencies of `useMemo` returning the editor never change. - const extensionsRef = useLatest(extensions); - const getExtensions = useCallback(() => extensionsRef.current, [extensionsRef]); - const [userPlugins] = useState(plugins); - const finalPlugins = useMemo(() => [withEvents(events), ...userPlugins], [userPlugins, events]); - const editor = useMemo(() => { - return withExtensions(createEditor(createSlateEditor(), getExtensions, finalPlugins)); - }, [getExtensions, userPlugins]); - - useEffect(() => { - if (plugins !== userPlugins) { - console.warn( - 'EditorV4: "plugins" prop has changed. This will have no effect (plugins are initialized on mount only).', - ); - } - }, [plugins, userPlugins]); +export function useCreateEditor({ events }: Parameters): Editor { + const [editor] = useState(() => { + return withEvents(events)(createEditor(createSlateEditor())); + }); return editor; } From cf841245dae2b5c7ea4e192b963998dc5677fdbb Mon Sep 17 00:00:00 2001 From: Ivan Voskoboinyk Date: Fri, 22 Sep 2023 14:57:42 +0300 Subject: [PATCH 54/54] [CARE-1802] Rewrite ExtensionsManager to not trigger a re-render on extension method hooks changes --- .../src/extensions/ExtensionManager.tsx | 120 ++++++++++++++---- .../src/extensions/ExtensionsEditor.ts | 80 +++++++----- .../src/extensions/useRegisterExtension.ts | 98 +++++++------- packages/slate-commons/src/types/Extension.ts | 25 +++- .../editable/EditableWithExtensions.tsx | 24 ++-- 5 files changed, 215 insertions(+), 132 deletions(-) diff --git a/packages/slate-commons/src/extensions/ExtensionManager.tsx b/packages/slate-commons/src/extensions/ExtensionManager.tsx index a4c6f4aa0..8d5d6ca22 100644 --- a/packages/slate-commons/src/extensions/ExtensionManager.tsx +++ b/packages/slate-commons/src/extensions/ExtensionManager.tsx @@ -1,27 +1,45 @@ -import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react'; -import type { BaseEditor } from 'slate'; +import React, { + createContext, + type ReactNode, + RefObject, + useContext, + useEffect, + useState, +} from 'react'; +import type { Editor } from 'slate'; import type { Extension } from '../types'; +import type { + AdditionalEditorMethods, + EditorMethodsHooks, + EditorRenderHooks, +} from '../types/Extension'; import type { ExtensionsEditor } from './ExtensionsEditor'; export interface ExtensionsManager { - register(extension: Extension): UnregisterFn; + register( + id: Extension['id'], + methodsHooks: RefObject, + propsHooks: EditorRenderHooks, + ): void; + unregister(id: Extension['id']): void; } -type UnregisterFn = () => void; - /** * -- CONTEXT -- * ============= */ +function requireExtensionsManagerProvider(): never { + throw new Error( + 'It is required to wrap any code using ExtensionsManager into ExtensionsManagerProvider.', + ); +} + export const ManagerContext = createContext({ - register() { - throw new Error( - 'It is required to wrap any code using ExtensionsManager into ExtensionsManagerProvider.', - ); - }, + register: requireExtensionsManagerProvider, + unregister: requireExtensionsManagerProvider, }); // FIXME: Introduce ManagerSyncContext to only render the Editor itself after all sub-tree extensions are already mounted. @@ -45,30 +63,53 @@ interface Props { editor: T; } -type Entry = { extension: Extension }; +type Entry = { + id: Extension['id']; + methodHooks: RefObject; + renderHooks: EditorRenderHooks; +}; + +type EntriesMap = Map; -const EDITOR_EXTENSIONS = new WeakMap(); +const EDITOR_HOOKS_ENTRIES = new WeakMap(); export function ExtensionsManager({ children, editor }: Props) { const [counter, setCounter] = useState(0); const [manager] = useState(() => { - function updateEntries(editor: T, updater: (entries: Entry[]) => Entry[]) { - const entries = EDITOR_EXTENSIONS.get(editor) ?? []; - const updatedEntries = updater(entries); + function updateEntries(editor: T, updater: (entries: EntriesMap) => void) { + const entries = EDITOR_HOOKS_ENTRIES.get(editor) ?? new Map(); + + const prevRenderHooks = [...entries.values()].map((entry) => entry.renderHooks); + + updater(entries); + + EDITOR_HOOKS_ENTRIES.set(editor, entries); + + const methodHooks = [...entries.values()].map((entry) => entry.methodHooks); + const renderHooks = [...entries.values()].map((entry) => entry.renderHooks); + + editor.methodHooks = methodHooks; - EDITOR_EXTENSIONS.set(editor, updatedEntries); - editor.extensions = updatedEntries.map(({ extension }) => extension); - setCounter((c) => c + 1); + if (!isRenderHooksEqual(prevRenderHooks, renderHooks)) { + editor.renderHooks = renderHooks; + setCounter((c) => c + 1); + } } return { - register(extension) { - const entry = { extension }; - updateEntries(editor, (entries) => [...entries, entry]); - - return () => { - updateEntries(editor, (entries) => entries.filter((e) => e !== entry)); - }; + register( + id: Extension['id'], + methodHooks: RefObject, + renderHooks: EditorRenderHooks, + ) { + updateEntries(editor, (entries) => { + entries.set(id, { id, methodHooks, renderHooks }); + }); + }, + unregister(id: Extension['id']) { + updateEntries(editor, (entries) => { + entries.delete(id); + }); }, }; }); @@ -82,3 +123,32 @@ export function ExtensionsManager({ children, editor return {children}; } + +function isRenderHooksEqual(hooks: Entry['renderHooks'][], prevHooks: Entry['renderHooks'][]) { + if (hooks.length !== prevHooks.length) { + return false; + } + + for (let i = 0; i < hooks.length; i++) { + const hooksParts = parts(hooks[i]); + const prevHookParts = parts(prevHooks[i]); + const isEqual = prevHookParts.every((part, index) => part === hooksParts[index]); + if (!isEqual) { + return false; + } + } + + return true; +} + +function parts(hook: Entry['renderHooks']) { + const { decorate, renderLeaf, renderElement, onDOMBeforeInput, onKeyDown, ...rest } = hook; + if (Object.keys(rest).length > 0) { + throw new Error( + `Logic error: one or more properties are ignored for renderHooks comparison: ${Object.keys( + rest, + ).join(', ')}.`, + ); + } + return [decorate, renderLeaf, renderElement, onKeyDown, onDOMBeforeInput]; +} diff --git a/packages/slate-commons/src/extensions/ExtensionsEditor.ts b/packages/slate-commons/src/extensions/ExtensionsEditor.ts index 2f62cea0c..b53b5fbe5 100644 --- a/packages/slate-commons/src/extensions/ExtensionsEditor.ts +++ b/packages/slate-commons/src/extensions/ExtensionsEditor.ts @@ -1,12 +1,24 @@ import { isNotUndefined } from '@technically/is-not-undefined'; +import type { RefObject } from 'react'; import type { BaseEditor, Descendant, Editor, Element, Node, TextUnit } from 'slate'; import type { HistoryEditor } from 'slate-history'; import type { ReactEditor } from 'slate-react'; -import type { EditorMethodsHooks, Extension } from '../types/Extension'; // a deep import is necessary to keep `EditorMethodsHooks` package-scoped +import type { + AdditionalEditorMethods, + EditorMethodsHooks, + EditorRenderHooks, +} from '../types/Extension'; // a deep import is necessary to keep cominterfaces package-scoped export interface ExtensionsEditor extends BaseEditor { - extensions: Extension[]; + /** + * Method hooks are separate to not trigger a re-render, when changed. + */ + methodHooks: RefObject[]; + /** + * Render hooks are separate to force a re-render, when changed. + */ + renderHooks: EditorRenderHooks[]; /** * Compare two elements. @@ -36,7 +48,6 @@ export interface ExtensionsEditor extends BaseEditor { export function withExtensions( editor: T, - extensions: Extension[] = [], ): T & ExtensionsEditor { const parent = { deleteBackward: editor.deleteBackward, @@ -55,8 +66,8 @@ export function withExtensions ext.deleteBackward) + const handlers = extensionsEditor.methodHooks + .map((hook) => hook.current?.deleteBackward) .filter(isNotUndefined); function next(unit: TextUnit) { @@ -71,8 +82,8 @@ export function withExtensions ext.deleteForward) + const handlers = extensionsEditor.methodHooks + .map((hook) => hook.current?.deleteForward) .filter(isNotUndefined); function next(unit: TextUnit) { @@ -87,8 +98,8 @@ export function withExtensions ext.insertData) + const handlers = extensionsEditor.methodHooks + .map((hook) => hook.current?.insertData) .filter(isNotUndefined); function next(dataTransfer: DataTransfer) { @@ -129,8 +140,8 @@ export function withExtensions ext.insertText) + const handlers = extensionsEditor.methodHooks + .map((hook) => hook.current?.insertText) .filter(isNotUndefined); function next(text: string) { @@ -145,8 +156,8 @@ export function withExtensions ext.normalizeNode ?? [], + const normalizers = extensionsEditor.methodHooks.flatMap( + (hook) => hook.current?.normalizeNode ?? [], ); for (const normalizer of normalizers) { @@ -160,8 +171,8 @@ export function withExtensions ext.getFragment) + const handlers = extensionsEditor.methodHooks + .map((hook) => hook.current?.getFragment) .filter(isNotUndefined); function next() { @@ -176,8 +187,8 @@ export function withExtensions ext.setFragmentData) + const handlers = extensionsEditor.methodHooks + .map((hook) => hook.current?.setFragmentData) .filter(isNotUndefined); function next(dataTransfer: DataTransfer) { @@ -192,8 +203,8 @@ export function withExtensions ext.undo) + const handlers = extensionsEditor.methodHooks + .map((hook) => hook.current?.undo) .filter(isNotUndefined); function next() { @@ -208,8 +219,8 @@ export function withExtensions ext.redo) + const handlers = extensionsEditor.methodHooks + .map((hook) => hook.current?.redo) .filter(isNotUndefined); function next() { @@ -226,10 +237,11 @@ export function withExtensions; const extensionsEditor: T & ExtensionsEditor = Object.assign(editor, { - extensions, + methodHooks: [], + renderHooks: [], isElementEqual(node, another): boolean | undefined { - for (const extension of extensionsEditor.extensions) { - const ret = extension.isElementEqual?.(node, another); + for (const hook of extensionsEditor.methodHooks) { + const ret = hook.current?.isElementEqual?.(node, another); if (typeof ret !== 'undefined') { return ret; } @@ -237,16 +249,16 @@ export function withExtensions extension.serialize?.(result) ?? result, + return extensionsEditor.methodHooks.reduce( + (result, hook) => hook.current?.serialize?.(result) ?? result, nodes, ); }, diff --git a/packages/slate-commons/src/extensions/useRegisterExtension.ts b/packages/slate-commons/src/extensions/useRegisterExtension.ts index 0035c7878..114287e1f 100644 --- a/packages/slate-commons/src/extensions/useRegisterExtension.ts +++ b/packages/slate-commons/src/extensions/useRegisterExtension.ts @@ -1,72 +1,64 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import type { Extension } from '../types'; import { useExtensionsManager } from './ExtensionManager'; +const SUPPORTED_KEYS = Object.keys({ + id: true, + decorate: true, + deleteBackward: true, + deleteForward: true, + deserialize: true, + isElementEqual: true, + isInline: true, + isRichBlock: true, + isVoid: true, + insertBreak: true, + insertData: true, + insertText: true, + normalizeNode: true, + onDOMBeforeInput: true, + onKeyDown: true, + renderElement: true, + renderLeaf: true, + serialize: true, + getFragment: true, + setFragmentData: true, + undo: true, + redo: true, +} satisfies Record); + export function useRegisterExtension(extension: Extension): null { const manager = useExtensionsManager(); - const { - id, - decorate, - deleteBackward, - deleteForward, - deserialize, - isElementEqual, - isInline, - isRichBlock, - isVoid, - insertBreak, - insertData, - insertText, - normalizeNode, - onDOMBeforeInput, - onKeyDown, - renderElement, - renderLeaf, - serialize, - getFragment, - setFragmentData, - undo, - redo, - ...rest - } = extension; + const { id, decorate, onDOMBeforeInput, onKeyDown, renderElement, renderLeaf } = extension; - if (Object.keys(rest).length > 0) { - throw new Error(`Unsupported keys passed: ${Object.keys(rest).join(', ')}.`); - } + const ref = useRef(extension); - // FIXME: [Optimization] Replace callback dependencies with refs + const unsupported = Object.keys(extension).filter((key) => !SUPPORTED_KEYS.includes(key)); - useEffect( - () => manager.register(extension), - [ - manager, - id, + if (unsupported.length > 0) { + throw new Error(`Unsupported keys passed: ${unsupported.join(', ')}.`); + } + + useEffect(() => { + ref.current = extension; + manager.register(id, ref, { decorate, - deleteBackward, - deleteForward, - deserialize, - isElementEqual, - isInline, - isRichBlock, - isVoid, - insertBreak, - insertData, - insertText, - normalizeNode, onDOMBeforeInput, onKeyDown, renderElement, renderLeaf, - serialize, - getFragment, - setFragmentData, - undo, - redo, - ], - ); + }); + }, [id, decorate, onDOMBeforeInput, onKeyDown, renderElement, renderLeaf]); + + useEffect(() => { + // Unregister the extension on unmount + return () => { + manager.unregister(id); + }; + }, [id]); return null; } diff --git a/packages/slate-commons/src/types/Extension.ts b/packages/slate-commons/src/types/Extension.ts index 9475421d1..32ffa1dca 100644 --- a/packages/slate-commons/src/types/Extension.ts +++ b/packages/slate-commons/src/types/Extension.ts @@ -15,8 +15,10 @@ import type { TextInsertionHandler } from './TextInsertionHandler'; /** * Extension hooks that affect component props and thus should be triggering a React re-render. + * + * @internal This is a package-scoped interface. Please do not export it publicly. */ -export interface EditorPropsHooks { +export interface EditorRenderHooks { decorate?: DecorateFactory; onDOMBeforeInput?: OnDOMBeforeInput; onKeyDown?: OnKeyDown; @@ -26,6 +28,8 @@ export interface EditorPropsHooks { /** * Extension hooks that affect the Editor singleton callback-based functionality, and don't require a re-render. + * + * @internal This is a package-scoped interface. Please do not export it publicly. */ export interface EditorMethodsHooks { deleteBackward?: (unit: TextUnit, next: Editor['deleteBackward']) => void; @@ -58,12 +62,12 @@ export interface EditorMethodsHooks { redo?: HistoryHandler; } -export interface Extension extends EditorMethodsHooks, EditorPropsHooks { - id: string; - - deserialize?: DeserializeHtml; - serialize?: Serialize; - +/** + * Additional Editor methods added by the ExtensionsEditor. + * + * @internal This is a package-scoped interface. Please do not export it publicly. + */ +export interface AdditionalEditorMethods { /** * Compare two elements. * `children` arrays can be omitted from the comparison, @@ -72,4 +76,11 @@ export interface Extension extends EditorMethodsHooks, EditorPropsHooks { isElementEqual?: (node: Element, another: Element) => boolean | undefined; isRichBlock?: (node: Node) => boolean; + + serialize?: Serialize; + deserialize?: DeserializeHtml; +} + +export interface Extension extends EditorMethodsHooks, EditorRenderHooks, AdditionalEditorMethods { + id: string; } diff --git a/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx b/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx index 9650da758..be8ff9e13 100644 --- a/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx +++ b/packages/slate-editor/src/modules/editable/EditableWithExtensions.tsx @@ -48,42 +48,40 @@ export function EditableWithExtensions({ renderLeaf, ...props }: Props) { - const extensions = useSlateSelector((editor) => editor.extensions); + const extensions = useSlateSelector((editor) => editor.renderHooks); const combinedDecorate: Decorate = useMemo( function () { - const decorateExtensions = extensions.map((extension) => extension.decorate?.(editor)); - return combineDecorate([decorate, ...decorateExtensions].filter(isNotUndefined)); + const decorateHooks = extensions.map((hook) => hook.decorate?.(editor)); + return combineDecorate([decorate, ...decorateHooks].filter(isNotUndefined)); }, [decorate, extensions], ); const combinedOnDOMBeforeInput = useMemo(() => { - const onDOMBeforeInputExtensions = extensions.map( - (extension) => extension.onDOMBeforeInput, - ); + const onDOMBeforeInputHooks = extensions.map((hook) => hook.onDOMBeforeInput); return combineOnDOMBeforeInput( editor, - [onDOMBeforeInput, ...onDOMBeforeInputExtensions].filter(isNotUndefined), + [onDOMBeforeInput, ...onDOMBeforeInputHooks].filter(isNotUndefined), ); }, [onDOMBeforeInput, extensions]); const combinedOnKeyDown = useMemo(() => { - const onKeyDownExtensions = extensions.map((extension) => extension.onKeyDown); - return combineOnKeyDown(editor, [onKeyDown, ...onKeyDownExtensions].filter(isNotUndefined)); + const onKeyDownHooks = extensions.map((hook) => hook.onKeyDown); + return combineOnKeyDown(editor, [onKeyDown, ...onKeyDownHooks].filter(isNotUndefined)); }, [onKeyDown, extensions]); const combinedRenderElement = useMemo(() => { - const renderElementExtensions = extensions.map((extension) => extension.renderElement); + const renderElementHooks = extensions.map((hook) => hook.renderElement); return combineRenderElement( editor, - [renderElement, ...renderElementExtensions].filter(isNotUndefined), + [renderElement, ...renderElementHooks].filter(isNotUndefined), ); }, [renderElement, extensions]); const combinedRenderLeaf = useMemo(() => { - const renderLeafExtensions = extensions.map((extension) => extension.renderLeaf); - return combineRenderLeaf([renderLeaf, ...renderLeafExtensions].filter(isNotUndefined)); + const renderLeafHooks = extensions.map((hook) => hook.renderLeaf); + return combineRenderLeaf([renderLeaf, ...renderLeafHooks].filter(isNotUndefined)); }, [renderLeaf, extensions]); return (