1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-30 18:39:51 +02:00

Add renderText and leafPosition (#5850)

* feat

* revert

* revert

* docs

* test

* refactor

* test

* revert

* refactor

* doc

* docs

* refactor

* docs

* test

* docs

* docs

* docs

* refactor
This commit is contained in:
Ziad Beyens
2025-04-29 16:30:57 +02:00
committed by GitHub
parent 2c62e01797
commit 22a3dda36d
20 changed files with 390 additions and 117 deletions

View File

@@ -23,6 +23,7 @@ import {
Text,
Transforms,
DecoratedRange,
LeafPosition,
} from 'slate'
import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager'
import useChildren from '../hooks/use-children'
@@ -105,11 +106,31 @@ export interface RenderElementProps {
export interface RenderLeafProps {
children: any
/**
* The leaf node with any applied decorations.
* If no decorations are applied, it will be identical to the `text` property.
*/
leaf: Text
text: Text
attributes: {
'data-slate-leaf': true
}
/**
* The position of the leaf within the Text node, only present when the text node is split by decorations.
*/
leafPosition?: LeafPosition
}
/**
* `RenderTextProps` are passed to the `renderText` handler.
*/
export interface RenderTextProps {
text: Text
children: any
attributes: {
'data-slate-node': 'text'
ref: any
}
}
/**
@@ -125,6 +146,7 @@ export type EditableProps = {
style?: React.CSSProperties
renderElement?: (props: RenderElementProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
renderText?: (props: RenderTextProps) => JSX.Element
renderPlaceholder?: (props: RenderPlaceholderProps) => JSX.Element
scrollSelectionIntoView?: (editor: ReactEditor, domRange: DOMRange) => void
as?: React.ElementType
@@ -149,6 +171,7 @@ export const Editable = forwardRef(
readOnly = false,
renderElement,
renderLeaf,
renderText,
renderPlaceholder = defaultRenderPlaceholder,
scrollSelectionIntoView = defaultScrollSelectionIntoView,
style: userStyle = {},
@@ -1831,6 +1854,7 @@ export const Editable = forwardRef(
renderElement={renderElement}
renderPlaceholder={renderPlaceholder}
renderLeaf={renderLeaf}
renderText={renderText}
selection={editor.selection}
/>
</Component>

View File

@@ -22,6 +22,7 @@ import {
RenderElementProps,
RenderLeafProps,
RenderPlaceholderProps,
RenderTextProps,
} from './editable'
import Text from './text'
@@ -35,6 +36,7 @@ const Element = (props: {
element: SlateElement
renderElement?: (props: RenderElementProps) => JSX.Element
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
renderText?: (props: RenderTextProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
selection: Range | null
}) => {
@@ -44,6 +46,7 @@ const Element = (props: {
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
renderPlaceholder,
renderLeaf,
renderText,
selection,
} = props
const editor = useSlateStatic()
@@ -71,6 +74,7 @@ const Element = (props: {
renderElement,
renderPlaceholder,
renderLeaf,
renderText,
selection,
})
@@ -145,6 +149,7 @@ const MemoizedElement = React.memo(Element, (prev, next) => {
return (
prev.element === next.element &&
prev.renderElement === next.renderElement &&
prev.renderText === next.renderText &&
prev.renderLeaf === next.renderLeaf &&
prev.renderPlaceholder === next.renderPlaceholder &&
isElementDecorationsEqual(prev.decorations, next.decorations) &&

View File

@@ -6,7 +6,7 @@ import React, {
useEffect,
} from 'react'
import { JSX } from 'react'
import { Element, Text } from 'slate'
import { Element, LeafPosition, Text } from 'slate'
import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer'
import String from './string'
import {
@@ -53,6 +53,7 @@ const Leaf = (props: {
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
text: Text
leafPosition?: LeafPosition
}) => {
const {
leaf,
@@ -61,6 +62,7 @@ const Leaf = (props: {
parent,
renderPlaceholder,
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
leafPosition,
} = props
const editor = useSlateStatic()
@@ -157,7 +159,13 @@ const Leaf = (props: {
'data-slate-leaf': true,
}
return renderLeaf({ attributes, children, leaf, text })
return renderLeaf({
attributes,
children,
leaf,
text,
leafPosition,
})
}
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useRef } from 'react'
import { Element, DecoratedRange, Text as SlateText } from 'slate'
import { Element, Text as SlateText, DecoratedRange } from 'slate'
import { ReactEditor, useSlateStatic } from '..'
import { isTextDecorationsEqual } from 'slate-dom'
import {
@@ -7,7 +7,11 @@ import {
ELEMENT_TO_NODE,
NODE_TO_ELEMENT,
} from 'slate-dom'
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
import {
RenderLeafProps,
RenderPlaceholderProps,
RenderTextProps,
} from './editable'
import Leaf from './leaf'
/**
@@ -20,25 +24,34 @@ const Text = (props: {
parent: Element
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
renderText?: (props: RenderTextProps) => JSX.Element
text: SlateText
}) => {
const { decorations, isLast, parent, renderPlaceholder, renderLeaf, text } =
props
const {
decorations,
isLast,
parent,
renderPlaceholder,
renderLeaf,
renderText = (props: RenderTextProps) => <DefaultText {...props} />,
text,
} = props
const editor = useSlateStatic()
const ref = useRef<HTMLSpanElement | null>(null)
const leaves = SlateText.decorations(text, decorations)
const decoratedLeaves = SlateText.decorations(text, decorations)
const key = ReactEditor.findKey(editor, text)
const children = []
for (let i = 0; i < leaves.length; i++) {
const leaf = leaves[i]
for (let i = 0; i < decoratedLeaves.length; i++) {
const { leaf, position } = decoratedLeaves[i]
children.push(
<Leaf
isLast={isLast && i === leaves.length - 1}
isLast={isLast && i === decoratedLeaves.length - 1}
key={`${key.id}-${i}`}
renderPlaceholder={renderPlaceholder}
leaf={leaf}
leafPosition={position}
text={text}
parent={parent}
renderLeaf={renderLeaf}
@@ -65,17 +78,27 @@ const Text = (props: {
},
[ref, editor, key, text]
)
return (
<span data-slate-node="text" ref={callbackRef}>
{children}
</span>
)
const attributes: {
'data-slate-node': 'text'
ref: any
} = {
'data-slate-node': 'text',
ref: callbackRef,
}
return renderText({
text,
children,
attributes,
})
}
const MemoizedText = React.memo(Text, (prev, next) => {
return (
next.parent === prev.parent &&
next.isLast === prev.isLast &&
next.renderText === prev.renderText &&
next.renderLeaf === prev.renderLeaf &&
next.renderPlaceholder === prev.renderPlaceholder &&
next.text === prev.text &&
@@ -83,4 +106,9 @@ const MemoizedText = React.memo(Text, (prev, next) => {
)
})
export const DefaultText = (props: RenderTextProps) => {
const { attributes, children } = props
return <span {...attributes}>{children}</span>
}
export default MemoizedText

View File

@@ -11,6 +11,7 @@ import {
RenderElementProps,
RenderLeafProps,
RenderPlaceholderProps,
RenderTextProps,
} from '../components/editable'
import ElementComponent from '../components/element'
@@ -30,6 +31,7 @@ const useChildren = (props: {
node: Ancestor
renderElement?: (props: RenderElementProps) => JSX.Element
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
renderText?: (props: RenderTextProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
selection: Range | null
}) => {

View File

@@ -8,6 +8,7 @@ export {
} from './components/editable'
export { DefaultElement } from './components/element'
export { DefaultText } from './components/text'
export { DefaultLeaf } from './components/leaf'
export { Slate } from './components/slate'

View File

@@ -1,5 +1,5 @@
import { isPlainObject } from 'is-plain-object'
import { Range } from '..'
import { Path, Range } from '..'
import { ExtendedType } from '../types/custom-types'
import { isDeepEqual } from '../utils/deep-equal'
@@ -15,6 +15,13 @@ export interface BaseText {
export type Text = ExtendedType<'Text', BaseText>
export interface LeafPosition {
start: number
end: number
isFirst?: true
isLast?: true
}
export interface TextEqualsOptions {
loose?: boolean
}
@@ -62,7 +69,10 @@ export interface TextInterface {
/**
* Get the leaves for a text node given decorations.
*/
decorations: (node: Text, decorations: DecoratedRange[]) => Text[]
decorations: (
node: Text,
decorations: DecoratedRange[]
) => { leaf: Text; position?: LeafPosition }[]
}
// eslint-disable-next-line no-redeclare
@@ -111,8 +121,13 @@ export const Text: TextInterface = {
return true
},
decorations(node: Text, decorations: DecoratedRange[]): Text[] {
let leaves: Text[] = [{ ...node }]
decorations(
node: Text,
decorations: DecoratedRange[]
): { leaf: Text; position?: LeafPosition }[] {
let leaves: { leaf: Text; position?: LeafPosition }[] = [
{ leaf: { ...node } },
]
for (const dec of decorations) {
const { anchor, focus, merge: mergeDecoration, ...rest } = dec
@@ -123,7 +138,7 @@ export const Text: TextInterface = {
const decorationEnd = end.offset
const merge = mergeDecoration ?? Object.assign
for (const leaf of leaves) {
for (const { leaf } of leaves) {
const { length } = leaf.text
const leafStart = leafEnd
leafEnd += length
@@ -131,7 +146,7 @@ export const Text: TextInterface = {
// If the range encompasses the entire leaf, add the range.
if (decorationStart <= leafStart && leafEnd <= decorationEnd) {
merge(leaf, rest)
next.push(leaf)
next.push({ leaf })
continue
}
@@ -143,7 +158,7 @@ export const Text: TextInterface = {
decorationEnd < leafStart ||
(decorationEnd === leafStart && leafStart !== 0)
) {
next.push(leaf)
next.push({ leaf })
continue
}
@@ -156,13 +171,13 @@ export const Text: TextInterface = {
if (decorationEnd < leafEnd) {
const off = decorationEnd - leafStart
after = { ...middle, text: middle.text.slice(off) }
after = { leaf: { ...middle, text: middle.text.slice(off) } }
middle = { ...middle, text: middle.text.slice(0, off) }
}
if (decorationStart > leafStart) {
const off = decorationStart - leafStart
before = { ...middle, text: middle.text.slice(0, off) }
before = { leaf: { ...middle, text: middle.text.slice(0, off) } }
middle = { ...middle, text: middle.text.slice(off) }
}
@@ -172,7 +187,7 @@ export const Text: TextInterface = {
next.push(before)
}
next.push(middle)
next.push({ leaf: middle })
if (after) {
next.push(after)
@@ -182,6 +197,21 @@ export const Text: TextInterface = {
leaves = next
}
if (leaves.length > 1) {
let currentOffset = 0
for (const [index, item] of leaves.entries()) {
const start = currentOffset
const end = start + item.leaf.text.length
const position: LeafPosition = { start, end }
if (index === 0) position.isFirst = true
if (index === leaves.length - 1) position.isLast = true
item.position = position
currentOffset = end
}
}
return leaves
},
}

View File

@@ -31,21 +31,33 @@ export const test = decorations => {
export const output = [
{
text: 'a',
mark: 'mark',
leaf: {
text: 'a',
mark: 'mark',
},
position: { start: 0, end: 1, isFirst: true },
},
{
text: 'b',
mark: 'mark',
decoration1: 'decoration1',
leaf: {
text: 'b',
mark: 'mark',
decoration1: 'decoration1',
},
position: { start: 1, end: 2 },
},
{
text: 'c',
mark: 'mark',
decoration2: 'decoration2',
leaf: {
text: 'c',
mark: 'mark',
decoration2: 'decoration2',
},
position: { start: 2, end: 3 },
},
{
text: 'd',
mark: 'mark',
leaf: {
text: 'd',
mark: 'mark',
},
position: { start: 3, end: 4, isLast: true },
},
]

View File

@@ -53,33 +53,51 @@ export const test = decorations => {
export const output = [
{
text: 'a',
mark: 'mark',
leaf: {
text: 'a',
mark: 'mark',
},
position: { start: 0, end: 1, isFirst: true },
},
{
text: 'b',
mark: 'mark',
decoration1: 'decoration1',
leaf: {
text: 'b',
mark: 'mark',
decoration1: 'decoration1',
},
position: { start: 1, end: 2 },
},
{
text: '',
mark: 'mark',
decoration1: 'decoration1',
decoration2: 'decoration2',
decoration3: 'decoration3',
leaf: {
text: '',
mark: 'mark',
decoration1: 'decoration1',
decoration2: 'decoration2',
decoration3: 'decoration3',
},
position: { start: 2, end: 2 },
},
{
text: 'c',
mark: 'mark',
decoration3: 'decoration3',
leaf: {
text: 'c',
mark: 'mark',
decoration3: 'decoration3',
},
position: { start: 2, end: 3 },
},
{
text: 'd',
mark: 'mark',
leaf: {
text: 'd',
mark: 'mark',
},
position: { start: 3, end: 4 },
},
{
text: '',
mark: 'mark',
decoration4: 'decoration4',
leaf: {
text: '',
mark: 'mark',
decoration4: 'decoration4',
},
position: { start: 4, end: 4, isLast: true },
},
]

View File

@@ -18,12 +18,18 @@ export const test = decorations => {
}
export const output = [
{
text: 'ab',
mark: 'mark',
leaf: {
text: 'ab',
mark: 'mark',
},
position: { start: 0, end: 2, isFirst: true },
},
{
text: 'c',
mark: 'mark',
decoration: 'decoration',
leaf: {
text: 'c',
mark: 'mark',
decoration: 'decoration',
},
position: { start: 2, end: 3, isLast: true },
},
]

View File

@@ -53,43 +53,64 @@ export const test = decorations => {
export const output = [
{
text: 'a',
mark: 'mark',
leaf: {
text: 'a',
mark: 'mark',
},
position: { start: 0, end: 1, isFirst: true },
},
{
text: 'b',
mark: 'mark',
decoration1: 'decoration1',
decoration2: 'decoration2',
leaf: {
text: 'b',
mark: 'mark',
decoration1: 'decoration1',
decoration2: 'decoration2',
},
position: { start: 1, end: 2 },
},
{
text: '',
mark: 'mark',
decoration1: 'decoration1',
decoration2: 'decoration2',
decoration3: 'decoration3',
decoration4: 'decoration4',
leaf: {
text: '',
mark: 'mark',
decoration1: 'decoration1',
decoration2: 'decoration2',
decoration3: 'decoration3',
decoration4: 'decoration4',
},
position: { start: 2, end: 2 },
},
{
text: 'c',
mark: 'mark',
decoration1: 'decoration1',
decoration2: 'decoration2',
decoration4: 'decoration4',
leaf: {
text: 'c',
mark: 'mark',
decoration1: 'decoration1',
decoration2: 'decoration2',
decoration4: 'decoration4',
},
position: { start: 2, end: 3 },
},
{
text: 'd',
mark: 'mark',
decoration1: 'decoration1',
decoration4: 'decoration4',
leaf: {
text: 'd',
mark: 'mark',
decoration1: 'decoration1',
decoration4: 'decoration4',
},
position: { start: 3, end: 4 },
},
{
text: 'e',
mark: 'mark',
decoration1: 'decoration1',
leaf: {
text: 'e',
mark: 'mark',
decoration1: 'decoration1',
},
position: { start: 4, end: 5 },
},
{
text: 'f',
mark: 'mark',
leaf: {
text: 'f',
mark: 'mark',
},
position: { start: 5, end: 6, isLast: true },
},
]

View File

@@ -37,18 +37,27 @@ export const test = decorations => {
}
export const output = [
{
text: 'a',
mark: 'mark',
decoration: [1, 2, 3],
leaf: {
text: 'a',
mark: 'mark',
decoration: [1, 2, 3],
},
position: { start: 0, end: 1, isFirst: true },
},
{
text: 'b',
mark: 'mark',
decoration: [1, 2, 3, 4, 5, 6],
leaf: {
text: 'b',
mark: 'mark',
decoration: [1, 2, 3, 4, 5, 6],
},
position: { start: 1, end: 2 },
},
{
text: 'c',
mark: 'mark',
decoration: [4, 5, 6],
leaf: {
text: 'c',
mark: 'mark',
decoration: [4, 5, 6],
},
position: { start: 2, end: 3, isLast: true },
},
]

View File

@@ -18,16 +18,25 @@ export const test = decorations => {
}
export const output = [
{
text: 'a',
mark: 'mark',
leaf: {
text: 'a',
mark: 'mark',
},
position: { start: 0, end: 1, isFirst: true },
},
{
text: 'b',
mark: 'mark',
decoration: 'decoration',
leaf: {
text: 'b',
mark: 'mark',
decoration: 'decoration',
},
position: { start: 1, end: 2 },
},
{
text: 'c',
mark: 'mark',
leaf: {
text: 'c',
mark: 'mark',
},
position: { start: 2, end: 3, isLast: true },
},
]

View File

@@ -29,19 +29,28 @@ export const test = decorations => {
}
export const output = [
{
text: 'a',
mark: 'mark',
decoration2: 'decoration2',
leaf: {
text: 'a',
mark: 'mark',
decoration2: 'decoration2',
},
position: { start: 0, end: 1, isFirst: true },
},
{
text: 'b',
mark: 'mark',
decoration1: 'decoration1',
decoration2: 'decoration2',
leaf: {
text: 'b',
mark: 'mark',
decoration1: 'decoration1',
decoration2: 'decoration2',
},
position: { start: 1, end: 2 },
},
{
text: 'c',
mark: 'mark',
decoration2: 'decoration2',
leaf: {
text: 'c',
mark: 'mark',
decoration2: 'decoration2',
},
position: { start: 2, end: 3, isLast: true },
},
]

View File

@@ -18,12 +18,18 @@ export const test = decorations => {
}
export const output = [
{
text: 'a',
mark: 'mark',
decoration: 'decoration',
leaf: {
text: 'a',
mark: 'mark',
decoration: 'decoration',
},
position: { start: 0, end: 1, isFirst: true },
},
{
text: 'bc',
mark: 'mark',
leaf: {
text: 'bc',
mark: 'mark',
},
position: { start: 1, end: 3, isLast: true },
},
]