mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-11 17:53:59 +02:00
feat: add optional method to decoration object (#5776)
This commit is contained in:
5
.changeset/tiny-cheetahs-ring.md
Normal file
5
.changeset/tiny-cheetahs-ring.md
Normal file
@@ -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"
|
@@ -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.
|
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`.
|
Get the leaves for a text node, given `decorations`.
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ import {
|
|||||||
Range,
|
Range,
|
||||||
Text,
|
Text,
|
||||||
Transforms,
|
Transforms,
|
||||||
|
DecoratedRange,
|
||||||
} from 'slate'
|
} from 'slate'
|
||||||
import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager'
|
import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager'
|
||||||
import useChildren from '../hooks/use-children'
|
import useChildren from '../hooks/use-children'
|
||||||
@@ -116,7 +117,7 @@ export interface RenderLeafProps {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type EditableProps = {
|
export type EditableProps = {
|
||||||
decorate?: (entry: NodeEntry) => Range[]
|
decorate?: (entry: NodeEntry) => DecoratedRange[]
|
||||||
onDOMBeforeInput?: (event: InputEvent) => void
|
onDOMBeforeInput?: (event: InputEvent) => void
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
@@ -1876,7 +1877,7 @@ export const DefaultPlaceholder = ({
|
|||||||
* A default memoized decorate function.
|
* 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.
|
* A default implement to scroll dom range into view.
|
||||||
|
@@ -1,7 +1,13 @@
|
|||||||
import getDirection from 'direction'
|
import getDirection from 'direction'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { JSX } 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 { ReactEditor, useReadOnly, useSlateStatic } from '..'
|
||||||
import useChildren from '../hooks/use-children'
|
import useChildren from '../hooks/use-children'
|
||||||
import { isElementDecorationsEqual } from 'slate-dom'
|
import { isElementDecorationsEqual } from 'slate-dom'
|
||||||
@@ -25,7 +31,7 @@ import Text from './text'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const Element = (props: {
|
const Element = (props: {
|
||||||
decorations: Range[]
|
decorations: DecoratedRange[]
|
||||||
element: SlateElement
|
element: SlateElement
|
||||||
renderElement?: (props: RenderElementProps) => JSX.Element
|
renderElement?: (props: RenderElementProps) => JSX.Element
|
||||||
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
|
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useRef } from 'react'
|
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 { ReactEditor, useSlateStatic } from '..'
|
||||||
import { isTextDecorationsEqual } from 'slate-dom'
|
import { isTextDecorationsEqual } from 'slate-dom'
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +15,7 @@ import Leaf from './leaf'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const Text = (props: {
|
const Text = (props: {
|
||||||
decorations: Range[]
|
decorations: DecoratedRange[]
|
||||||
isLast: boolean
|
isLast: boolean
|
||||||
parent: Element
|
parent: Element
|
||||||
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
|
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Ancestor, Descendant, Editor, Element, Range } from 'slate'
|
import {
|
||||||
|
Ancestor,
|
||||||
|
Descendant,
|
||||||
|
Editor,
|
||||||
|
Element,
|
||||||
|
Range,
|
||||||
|
DecoratedRange,
|
||||||
|
} from 'slate'
|
||||||
import {
|
import {
|
||||||
RenderElementProps,
|
RenderElementProps,
|
||||||
RenderLeafProps,
|
RenderLeafProps,
|
||||||
@@ -19,7 +26,7 @@ import { useSlateStatic } from './use-slate-static'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const useChildren = (props: {
|
const useChildren = (props: {
|
||||||
decorations: Range[]
|
decorations: DecoratedRange[]
|
||||||
node: Ancestor
|
node: Ancestor
|
||||||
renderElement?: (props: RenderElementProps) => JSX.Element
|
renderElement?: (props: RenderElementProps) => JSX.Element
|
||||||
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
|
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
import { createContext, useContext } from 'react'
|
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.
|
* 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.
|
* Get the current `decorate` prop of the editable.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const useDecorate = (): ((entry: NodeEntry) => Range[]) => {
|
export const useDecorate = (): ((entry: NodeEntry) => DecoratedRange[]) => {
|
||||||
return useContext(DecorateContext)
|
return useContext(DecorateContext)
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,14 @@ export interface TextEqualsOptions {
|
|||||||
loose?: boolean
|
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 {
|
export interface TextInterface {
|
||||||
/**
|
/**
|
||||||
* Check if two text nodes are equal.
|
* Check if two text nodes are equal.
|
||||||
@@ -54,7 +62,7 @@ export interface TextInterface {
|
|||||||
/**
|
/**
|
||||||
* Get the leaves for a text node given decorations.
|
* 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
|
// eslint-disable-next-line no-redeclare
|
||||||
@@ -103,16 +111,17 @@ export const Text: TextInterface = {
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
decorations(node: Text, decorations: Range[]): Text[] {
|
decorations(node: Text, decorations: DecoratedRange[]): Text[] {
|
||||||
let leaves: Text[] = [{ ...node }]
|
let leaves: Text[] = [{ ...node }]
|
||||||
|
|
||||||
for (const dec of decorations) {
|
for (const dec of decorations) {
|
||||||
const { anchor, focus, ...rest } = dec
|
const { anchor, focus, merge: mergeDecoration, ...rest } = dec
|
||||||
const [start, end] = Range.edges(dec)
|
const [start, end] = Range.edges(dec)
|
||||||
const next = []
|
const next = []
|
||||||
let leafEnd = 0
|
let leafEnd = 0
|
||||||
const decorationStart = start.offset
|
const decorationStart = start.offset
|
||||||
const decorationEnd = end.offset
|
const decorationEnd = end.offset
|
||||||
|
const merge = mergeDecoration ?? Object.assign
|
||||||
|
|
||||||
for (const leaf of leaves) {
|
for (const leaf of leaves) {
|
||||||
const { length } = leaf.text
|
const { length } = leaf.text
|
||||||
@@ -121,7 +130,7 @@ export const Text: TextInterface = {
|
|||||||
|
|
||||||
// If the range encompasses the entire leaf, add the range.
|
// If the range encompasses the entire leaf, add the range.
|
||||||
if (decorationStart <= leafStart && leafEnd <= decorationEnd) {
|
if (decorationStart <= leafStart && leafEnd <= decorationEnd) {
|
||||||
Object.assign(leaf, rest)
|
merge(leaf, rest)
|
||||||
next.push(leaf)
|
next.push(leaf)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -157,7 +166,7 @@ export const Text: TextInterface = {
|
|||||||
middle = { ...middle, text: middle.text.slice(off) }
|
middle = { ...middle, text: middle.text.slice(off) }
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(middle, rest)
|
merge(middle, rest)
|
||||||
|
|
||||||
if (before) {
|
if (before) {
|
||||||
next.push(before)
|
next.push(before)
|
||||||
|
54
packages/slate/test/interfaces/Text/decorations/merge.ts
Normal file
54
packages/slate/test/interfaces/Text/decorations/merge.ts
Normal file
@@ -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],
|
||||||
|
},
|
||||||
|
]
|
Reference in New Issue
Block a user