WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 22a3dda

Browse files
authored
Add renderText and leafPosition (#5850)
* feat * revert * revert * docs * test * refactor * test * revert * refactor * doc * docs * refactor * docs * test * docs * docs * docs * refactor
1 parent 2c62e01 commit 22a3dda

File tree

20 files changed

+390
-117
lines changed

20 files changed

+390
-117
lines changed

.changeset/slate-react.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'slate-react': minor
3+
---
4+
5+
- Update `RenderLeafProps` interface to add `leafPosition` property containing `start`, `end`, `isFirst`, and `isLast` when a text node is split by decorations.
6+
- Add optional `renderText` prop to `<Editable />` component for customizing text node rendering.

.changeset/slate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'slate': minor
3+
---
4+
5+
- Update `Text.decorations` to return the positions in addition to the leaf nodes: `{ leaf: Text, position?: { start: number, end: number, isFirst: boolean, isLast: boolean } }[]`.

docs/api/nodes/text.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ If a `props.text` property is passed in, it will be ignored.
2626

2727
If there are properties in `text` that are not in `props`, those will be ignored when it comes to testing for a match.
2828

29-
#### `Text.decorations(node: Text, decorations: DecoratedRange[]) => Text[]`
29+
#### `Text.decorations(node: Text, decorations: DecoratedRange[]) => { leaf: Text; position?: LeafPosition }[]`
3030

31-
Get the leaves for a text node, given `decorations`.
31+
Get the leaves and positions for a text node, given `decorations`.
3232

3333
### Check methods
3434

docs/concepts/09-rendering.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ Notice though how we've handled it slightly differently than `renderElement`. Si
8080

8181
> 🤖 As with the Element renderer, be sure to mix in `props.attributes` and render `props.children` in your leaf renderer! The attributes must be added to the top-level DOM element inside the component, as they are required for Slate's DOM helper functions to work. And the children are the actual text content of your document which Slate manages for you automatically.
8282
83+
When decorations split a single text node, the `renderLeaf` function will receive an additional `leafPosition` property. This object contains the `start` and `end` offsets of the leaf within the original text node, along with optional `isFirst` and `isLast` booleans. This `leafPosition` property is only added when a text node is actually split by decorations.
84+
8385
One disadvantage of text-level formatting is that you cannot guarantee that any given format is "contiguous"—meaning that it stays as a single leaf. This limitation with respect to leaves is similar to the DOM, where this is invalid:
8486

8587
```markup
@@ -99,6 +101,37 @@ Of course, this leaf stuff sounds pretty complex. But, you do not have to think
99101
- Text properties are for **non-contiguous**, character-level formatting.
100102
- Element properties are for **contiguous**, semantic elements in the document.
101103

104+
## Texts
105+
106+
While `renderLeaf` allows you to customize the rendering of individual leaves based on their formatting (marks and decorations), sometimes you need to customize the rendering for an entire text node, regardless of how decorations might split it into multiple leaves.
107+
108+
This is where the `renderText` prop comes in. It allows you to render a component that wraps all the leaves generated for a single `Text` node.
109+
110+
```jsx
111+
const renderText = useCallback(({ attributes, children, text }) => {
112+
return (
113+
<span {...attributes} className="custom-text">
114+
{children}
115+
{/* Render anything you want here */}
116+
</span>
117+
)
118+
}, [])
119+
120+
// In your editor component:
121+
<Editable
122+
renderText={renderText}
123+
renderLeaf={renderLeaf}
124+
/>
125+
```
126+
127+
**When to use `renderLeaf` vs `renderText`:**
128+
129+
- **`renderLeaf`**: Use this when you need to apply styles or rendering logic based on the specific properties of each individual leaf (e.g., applying bold style if `leaf.bold` is true, or highlighting based on a decoration). This function might be called multiple times for a single text node if decorations split it. You can use the optional `leafPosition` prop (available when a text node is split) to conditionally render something based on the position of the leaf within the text node.
130+
131+
- **`renderText`**: Use this when you need to render something exactly once for a given text node, regardless of how many leaves it's split into. It's ideal for wrapping the entire text node's content or adding elements associated with the text node as a whole without worrying about duplication caused by decorations.
132+
133+
You can use both `renderText` and `renderLeaf` together. `renderLeaf` renders the individual marks and decorations within a text node (leaves), and `renderText` renders the container of those leaves.
134+
102135
## Decorations
103136

104137
Decorations are another type of text-level formatting. They are similar to regular old custom properties, except each one applies to a `Range` of the document instead of being associated with a given text node.

docs/libraries/slate-react/editable.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,15 @@ export interface RenderLeafProps {
122122
attributes: {
123123
'data-slate-leaf': true
124124
}
125+
/**
126+
* The position of the leaf within the Text node, only present when the text node is split by decorations.
127+
*/
128+
leafPosition?: {
129+
start: number
130+
end: number
131+
isFirst?: true
132+
isLast?: true
133+
}
125134
}
126135
```
127136

@@ -142,6 +151,38 @@ Example usage:
142151
/>
143152
```
144153

154+
#### `renderText?: (props: RenderTextProps) => JSX.Element`
155+
156+
The `renderText` prop allows you to customize the rendering of the container element for a Text node in the Slate editor. This is useful when you need to wrap the entire text node content or add elements associated with the text node as a whole, regardless of how decorations might split the text into multiple leaves.
157+
158+
The `renderText` function receives an object of type `RenderTextProps` as its argument:
159+
160+
```typescript
161+
export interface RenderTextProps {
162+
text: Text
163+
children: any
164+
attributes: {
165+
'data-slate-node': 'text'
166+
ref: any
167+
}
168+
}
169+
```
170+
171+
Example usage:
172+
173+
```jsx
174+
<Editable
175+
renderText={({ attributes, children, text }) => {
176+
return (
177+
<span {...attributes} className="custom-text">
178+
{children}
179+
{text.tooltipContent && <Tooltip content={text.tooltipContent} />}
180+
</span>
181+
)
182+
}}
183+
/>
184+
```
185+
145186
#### `renderPlaceholder?: (props: RenderPlaceholderProps) => JSX.Element`
146187

147188
The `renderPlaceholder` prop allows you to customize how the placeholder of the Slate.js `Editable` component is rendered when the editor is empty. The placeholder will only be shown when the editor's content is empty.

packages/slate-react/src/components/editable.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
Text,
2424
Transforms,
2525
DecoratedRange,
26+
LeafPosition,
2627
} from 'slate'
2728
import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager'
2829
import useChildren from '../hooks/use-children'
@@ -105,11 +106,31 @@ export interface RenderElementProps {
105106

106107
export interface RenderLeafProps {
107108
children: any
109+
/**
110+
* The leaf node with any applied decorations.
111+
* If no decorations are applied, it will be identical to the `text` property.
112+
*/
108113
leaf: Text
109114
text: Text
110115
attributes: {
111116
'data-slate-leaf': true
112117
}
118+
/**
119+
* The position of the leaf within the Text node, only present when the text node is split by decorations.
120+
*/
121+
leafPosition?: LeafPosition
122+
}
123+
124+
/**
125+
* `RenderTextProps` are passed to the `renderText` handler.
126+
*/
127+
export interface RenderTextProps {
128+
text: Text
129+
children: any
130+
attributes: {
131+
'data-slate-node': 'text'
132+
ref: any
133+
}
113134
}
114135

115136
/**
@@ -125,6 +146,7 @@ export type EditableProps = {
125146
style?: React.CSSProperties
126147
renderElement?: (props: RenderElementProps) => JSX.Element
127148
renderLeaf?: (props: RenderLeafProps) => JSX.Element
149+
renderText?: (props: RenderTextProps) => JSX.Element
128150
renderPlaceholder?: (props: RenderPlaceholderProps) => JSX.Element
129151
scrollSelectionIntoView?: (editor: ReactEditor, domRange: DOMRange) => void
130152
as?: React.ElementType
@@ -149,6 +171,7 @@ export const Editable = forwardRef(
149171
readOnly = false,
150172
renderElement,
151173
renderLeaf,
174+
renderText,
152175
renderPlaceholder = defaultRenderPlaceholder,
153176
scrollSelectionIntoView = defaultScrollSelectionIntoView,
154177
style: userStyle = {},
@@ -1831,6 +1854,7 @@ export const Editable = forwardRef(
18311854
renderElement={renderElement}
18321855
renderPlaceholder={renderPlaceholder}
18331856
renderLeaf={renderLeaf}
1857+
renderText={renderText}
18341858
selection={editor.selection}
18351859
/>
18361860
</Component>

packages/slate-react/src/components/element.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
RenderElementProps,
2323
RenderLeafProps,
2424
RenderPlaceholderProps,
25+
RenderTextProps,
2526
} from './editable'
2627

2728
import Text from './text'
@@ -35,6 +36,7 @@ const Element = (props: {
3536
element: SlateElement
3637
renderElement?: (props: RenderElementProps) => JSX.Element
3738
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
39+
renderText?: (props: RenderTextProps) => JSX.Element
3840
renderLeaf?: (props: RenderLeafProps) => JSX.Element
3941
selection: Range | null
4042
}) => {
@@ -44,6 +46,7 @@ const Element = (props: {
4446
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
4547
renderPlaceholder,
4648
renderLeaf,
49+
renderText,
4750
selection,
4851
} = props
4952
const editor = useSlateStatic()
@@ -71,6 +74,7 @@ const Element = (props: {
7174
renderElement,
7275
renderPlaceholder,
7376
renderLeaf,
77+
renderText,
7478
selection,
7579
})
7680

@@ -145,6 +149,7 @@ const MemoizedElement = React.memo(Element, (prev, next) => {
145149
return (
146150
prev.element === next.element &&
147151
prev.renderElement === next.renderElement &&
152+
prev.renderText === next.renderText &&
148153
prev.renderLeaf === next.renderLeaf &&
149154
prev.renderPlaceholder === next.renderPlaceholder &&
150155
isElementDecorationsEqual(prev.decorations, next.decorations) &&

packages/slate-react/src/components/leaf.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import React, {
66
useEffect,
77
} from 'react'
88
import { JSX } from 'react'
9-
import { Element, Text } from 'slate'
9+
import { Element, LeafPosition, Text } from 'slate'
1010
import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer'
1111
import String from './string'
1212
import {
@@ -53,6 +53,7 @@ const Leaf = (props: {
5353
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
5454
renderLeaf?: (props: RenderLeafProps) => JSX.Element
5555
text: Text
56+
leafPosition?: LeafPosition
5657
}) => {
5758
const {
5859
leaf,
@@ -61,6 +62,7 @@ const Leaf = (props: {
6162
parent,
6263
renderPlaceholder,
6364
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
65+
leafPosition,
6466
} = props
6567

6668
const editor = useSlateStatic()
@@ -157,7 +159,13 @@ const Leaf = (props: {
157159
'data-slate-leaf': true,
158160
}
159161

160-
return renderLeaf({ attributes, children, leaf, text })
162+
return renderLeaf({
163+
attributes,
164+
children,
165+
leaf,
166+
text,
167+
leafPosition,
168+
})
161169
}
162170

163171
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {

packages/slate-react/src/components/text.tsx

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import React, { useCallback, useRef } from 'react'
2-
import { Element, DecoratedRange, Text as SlateText } from 'slate'
2+
import { Element, Text as SlateText, DecoratedRange } from 'slate'
33
import { ReactEditor, useSlateStatic } from '..'
44
import { isTextDecorationsEqual } from 'slate-dom'
55
import {
66
EDITOR_TO_KEY_TO_ELEMENT,
77
ELEMENT_TO_NODE,
88
NODE_TO_ELEMENT,
99
} from 'slate-dom'
10-
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
10+
import {
11+
RenderLeafProps,
12+
RenderPlaceholderProps,
13+
RenderTextProps,
14+
} from './editable'
1115
import Leaf from './leaf'
1216

1317
/**
@@ -20,25 +24,34 @@ const Text = (props: {
2024
parent: Element
2125
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
2226
renderLeaf?: (props: RenderLeafProps) => JSX.Element
27+
renderText?: (props: RenderTextProps) => JSX.Element
2328
text: SlateText
2429
}) => {
25-
const { decorations, isLast, parent, renderPlaceholder, renderLeaf, text } =
26-
props
30+
const {
31+
decorations,
32+
isLast,
33+
parent,
34+
renderPlaceholder,
35+
renderLeaf,
36+
renderText = (props: RenderTextProps) => <DefaultText {...props} />,
37+
text,
38+
} = props
2739
const editor = useSlateStatic()
2840
const ref = useRef<HTMLSpanElement | null>(null)
29-
const leaves = SlateText.decorations(text, decorations)
41+
const decoratedLeaves = SlateText.decorations(text, decorations)
3042
const key = ReactEditor.findKey(editor, text)
3143
const children = []
3244

33-
for (let i = 0; i < leaves.length; i++) {
34-
const leaf = leaves[i]
45+
for (let i = 0; i < decoratedLeaves.length; i++) {
46+
const { leaf, position } = decoratedLeaves[i]
3547

3648
children.push(
3749
<Leaf
38-
isLast={isLast && i === leaves.length - 1}
50+
isLast={isLast && i === decoratedLeaves.length - 1}
3951
key={`${key.id}-${i}`}
4052
renderPlaceholder={renderPlaceholder}
4153
leaf={leaf}
54+
leafPosition={position}
4255
text={text}
4356
parent={parent}
4457
renderLeaf={renderLeaf}
@@ -65,22 +78,37 @@ const Text = (props: {
6578
},
6679
[ref, editor, key, text]
6780
)
68-
return (
69-
<span data-slate-node="text" ref={callbackRef}>
70-
{children}
71-
</span>
72-
)
81+
82+
const attributes: {
83+
'data-slate-node': 'text'
84+
ref: any
85+
} = {
86+
'data-slate-node': 'text',
87+
ref: callbackRef,
88+
}
89+
90+
return renderText({
91+
text,
92+
children,
93+
attributes,
94+
})
7395
}
7496

7597
const MemoizedText = React.memo(Text, (prev, next) => {
7698
return (
7799
next.parent === prev.parent &&
78100
next.isLast === prev.isLast &&
101+
next.renderText === prev.renderText &&
79102
next.renderLeaf === prev.renderLeaf &&
80103
next.renderPlaceholder === prev.renderPlaceholder &&
81104
next.text === prev.text &&
82105
isTextDecorationsEqual(next.decorations, prev.decorations)
83106
)
84107
})
85108

109+
export const DefaultText = (props: RenderTextProps) => {
110+
const { attributes, children } = props
111+
return <span {...attributes}>{children}</span>
112+
}
113+
86114
export default MemoizedText

packages/slate-react/src/hooks/use-children.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
RenderElementProps,
1212
RenderLeafProps,
1313
RenderPlaceholderProps,
14+
RenderTextProps,
1415
} from '../components/editable'
1516

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

0 commit comments

Comments
 (0)