From 5a1c728c62bcc6bafe60b9598946bb3adde21cdf Mon Sep 17 00:00:00 2001 From: yf-yang <36890796+yf-yang@users.noreply.github.com> Date: Sat, 7 Dec 2024 15:19:30 +0800 Subject: [PATCH] feat: add optional method to decoration object (#5776) --- .changeset/tiny-cheetahs-ring.md | 5 ++ docs/api/nodes/text.md | 2 +- .../slate-react/src/components/editable.tsx | 5 +- .../slate-react/src/components/element.tsx | 10 +++- packages/slate-react/src/components/text.tsx | 4 +- .../slate-react/src/hooks/use-children.tsx | 11 +++- .../slate-react/src/hooks/use-decorate.ts | 10 ++-- packages/slate/src/interfaces/text.ts | 19 +++++-- .../test/interfaces/Text/decorations/merge.ts | 54 +++++++++++++++++++ 9 files changed, 101 insertions(+), 19 deletions(-) create mode 100644 .changeset/tiny-cheetahs-ring.md create mode 100644 packages/slate/test/interfaces/Text/decorations/merge.ts diff --git a/.changeset/tiny-cheetahs-ring.md b/.changeset/tiny-cheetahs-ring.md new file mode 100644 index 000000000..25855593d --- /dev/null +++ b/.changeset/tiny-cheetahs-ring.md @@ -0,0 +1,5 @@ +--- +'slate': minor +--- + +Add `merge` optional function to decorations and change related type signatures to `DecoratedRange`. Now developers can specify how two decoration object with the same key but different value are merged together if they overlap" diff --git a/docs/api/nodes/text.md b/docs/api/nodes/text.md index cf4e9e017..1c732c3fa 100644 --- a/docs/api/nodes/text.md +++ b/docs/api/nodes/text.md @@ -26,7 +26,7 @@ If a `props.text` property is passed in, it will be ignored. If there are properties in `text` that are not in `props`, those will be ignored when it comes to testing for a match. -#### `Text.decorations(node: Text, decorations: Range[]) => Text[]` +#### `Text.decorations(node: Text, decorations: DecoratedRange[]) => Text[]` Get the leaves for a text node, given `decorations`. diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 701b7c7b8..24a426a6a 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -22,6 +22,7 @@ import { Range, Text, Transforms, + DecoratedRange, } from 'slate' import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager' import useChildren from '../hooks/use-children' @@ -116,7 +117,7 @@ export interface RenderLeafProps { */ export type EditableProps = { - decorate?: (entry: NodeEntry) => Range[] + decorate?: (entry: NodeEntry) => DecoratedRange[] onDOMBeforeInput?: (event: InputEvent) => void placeholder?: string readOnly?: boolean @@ -1876,7 +1877,7 @@ export const DefaultPlaceholder = ({ * A default memoized decorate function. */ -export const defaultDecorate: (entry: NodeEntry) => Range[] = () => [] +export const defaultDecorate: (entry: NodeEntry) => DecoratedRange[] = () => [] /** * A default implement to scroll dom range into view. diff --git a/packages/slate-react/src/components/element.tsx b/packages/slate-react/src/components/element.tsx index 37c719600..04b81100b 100644 --- a/packages/slate-react/src/components/element.tsx +++ b/packages/slate-react/src/components/element.tsx @@ -1,7 +1,13 @@ import getDirection from 'direction' import React, { useCallback } from 'react' import { JSX } from 'react' -import { Editor, Element as SlateElement, Node, Range } from 'slate' +import { + Editor, + Element as SlateElement, + Node, + Range, + DecoratedRange, +} from 'slate' import { ReactEditor, useReadOnly, useSlateStatic } from '..' import useChildren from '../hooks/use-children' import { isElementDecorationsEqual } from 'slate-dom' @@ -25,7 +31,7 @@ import Text from './text' */ const Element = (props: { - decorations: Range[] + decorations: DecoratedRange[] element: SlateElement renderElement?: (props: RenderElementProps) => JSX.Element renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element diff --git a/packages/slate-react/src/components/text.tsx b/packages/slate-react/src/components/text.tsx index a162dcad9..e0bdd52ae 100644 --- a/packages/slate-react/src/components/text.tsx +++ b/packages/slate-react/src/components/text.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useRef } from 'react' -import { Element, Range, Text as SlateText } from 'slate' +import { Element, DecoratedRange, Text as SlateText } from 'slate' import { ReactEditor, useSlateStatic } from '..' import { isTextDecorationsEqual } from 'slate-dom' import { @@ -15,7 +15,7 @@ import Leaf from './leaf' */ const Text = (props: { - decorations: Range[] + decorations: DecoratedRange[] isLast: boolean parent: Element renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element diff --git a/packages/slate-react/src/hooks/use-children.tsx b/packages/slate-react/src/hooks/use-children.tsx index 3d30f55fb..322c7ad60 100644 --- a/packages/slate-react/src/hooks/use-children.tsx +++ b/packages/slate-react/src/hooks/use-children.tsx @@ -1,5 +1,12 @@ import React from 'react' -import { Ancestor, Descendant, Editor, Element, Range } from 'slate' +import { + Ancestor, + Descendant, + Editor, + Element, + Range, + DecoratedRange, +} from 'slate' import { RenderElementProps, RenderLeafProps, @@ -19,7 +26,7 @@ import { useSlateStatic } from './use-slate-static' */ const useChildren = (props: { - decorations: Range[] + decorations: DecoratedRange[] node: Ancestor renderElement?: (props: RenderElementProps) => JSX.Element renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element diff --git a/packages/slate-react/src/hooks/use-decorate.ts b/packages/slate-react/src/hooks/use-decorate.ts index a7eb43a8e..7155efe87 100644 --- a/packages/slate-react/src/hooks/use-decorate.ts +++ b/packages/slate-react/src/hooks/use-decorate.ts @@ -1,18 +1,18 @@ import { createContext, useContext } from 'react' -import { Range, NodeEntry } from 'slate' +import { DecoratedRange, NodeEntry } from 'slate' /** * A React context for sharing the `decorate` prop of the editable. */ -export const DecorateContext = createContext<(entry: NodeEntry) => Range[]>( - () => [] -) +export const DecorateContext = createContext< + (entry: NodeEntry) => DecoratedRange[] +>(() => []) /** * Get the current `decorate` prop of the editable. */ -export const useDecorate = (): ((entry: NodeEntry) => Range[]) => { +export const useDecorate = (): ((entry: NodeEntry) => DecoratedRange[]) => { return useContext(DecorateContext) } diff --git a/packages/slate/src/interfaces/text.ts b/packages/slate/src/interfaces/text.ts index 19a435dca..29dc0a5ff 100644 --- a/packages/slate/src/interfaces/text.ts +++ b/packages/slate/src/interfaces/text.ts @@ -19,6 +19,14 @@ export interface TextEqualsOptions { loose?: boolean } +export type DecoratedRange = Range & { + /** + * Customize how another decoration is merged into a text node. If not specified, `Object.assign` would be used. + * It is useful for overlapping decorations with the same key but different values. + */ + merge?: (leaf: Text, decoration: object) => void +} + export interface TextInterface { /** * Check if two text nodes are equal. @@ -54,7 +62,7 @@ export interface TextInterface { /** * Get the leaves for a text node given decorations. */ - decorations: (node: Text, decorations: Range[]) => Text[] + decorations: (node: Text, decorations: DecoratedRange[]) => Text[] } // eslint-disable-next-line no-redeclare @@ -103,16 +111,17 @@ export const Text: TextInterface = { return true }, - decorations(node: Text, decorations: Range[]): Text[] { + decorations(node: Text, decorations: DecoratedRange[]): Text[] { let leaves: Text[] = [{ ...node }] for (const dec of decorations) { - const { anchor, focus, ...rest } = dec + const { anchor, focus, merge: mergeDecoration, ...rest } = dec const [start, end] = Range.edges(dec) const next = [] let leafEnd = 0 const decorationStart = start.offset const decorationEnd = end.offset + const merge = mergeDecoration ?? Object.assign for (const leaf of leaves) { const { length } = leaf.text @@ -121,7 +130,7 @@ export const Text: TextInterface = { // If the range encompasses the entire leaf, add the range. if (decorationStart <= leafStart && leafEnd <= decorationEnd) { - Object.assign(leaf, rest) + merge(leaf, rest) next.push(leaf) continue } @@ -157,7 +166,7 @@ export const Text: TextInterface = { middle = { ...middle, text: middle.text.slice(off) } } - Object.assign(middle, rest) + merge(middle, rest) if (before) { next.push(before) diff --git a/packages/slate/test/interfaces/Text/decorations/merge.ts b/packages/slate/test/interfaces/Text/decorations/merge.ts new file mode 100644 index 000000000..ee6f9bd79 --- /dev/null +++ b/packages/slate/test/interfaces/Text/decorations/merge.ts @@ -0,0 +1,54 @@ +import { Text } from 'slate' + +const merge = (leaf: Text, dec: { decoration: number[] }) => { + const { decoration, ...rest } = dec + leaf.decoration = [...(leaf.decoration ?? []), ...decoration] + Object.assign(leaf, rest) +} + +export const input = [ + { + anchor: { + path: [0], + offset: 0, + }, + focus: { + path: [0], + offset: 2, + }, + merge, + decoration: [1, 2, 3], + }, + { + anchor: { + path: [0], + offset: 1, + }, + focus: { + path: [0], + offset: 3, + }, + merge, + decoration: [4, 5, 6], + }, +] +export const test = decorations => { + return Text.decorations({ text: 'abc', mark: 'mark' }, decorations) +} +export const output = [ + { + text: 'a', + mark: 'mark', + decoration: [1, 2, 3], + }, + { + text: 'b', + mark: 'mark', + decoration: [1, 2, 3, 4, 5, 6], + }, + { + text: 'c', + mark: 'mark', + decoration: [4, 5, 6], + }, +]