Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/slate-react.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'slate-react': minor
---

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

- Update `Text.decorations` to return the positions in addition to the leaf nodes: `{ leaf: Text, position?: { start: number, end: number, isFirst: boolean, isLast: boolean } }[]`.
4 changes: 2 additions & 2 deletions docs/api/nodes/text.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ 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: DecoratedRange[]) => Text[]`
#### `Text.decorations(node: Text, decorations: DecoratedRange[]) => { leaf: Text; position?: LeafPosition }[]`

Get the leaves for a text node, given `decorations`.
Get the leaves and positions for a text node, given `decorations`.

### Check methods

Expand Down
33 changes: 33 additions & 0 deletions docs/concepts/09-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ Notice though how we've handled it slightly differently than `renderElement`. Si

> 🤖 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.

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.

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:

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

## Texts

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.

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.

```jsx
const renderText = useCallback(({ attributes, children, text }) => {
return (
<span {...attributes} className="custom-text">
{children}
{/* Render anything you want here */}
</span>
)
}, [])

// In your editor component:
<Editable
renderText={renderText}
renderLeaf={renderLeaf}
/>
```

**When to use `renderLeaf` vs `renderText`:**

- **`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.

- **`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.

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.

## Decorations

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.
Expand Down
41 changes: 41 additions & 0 deletions docs/libraries/slate-react/editable.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ export interface RenderLeafProps {
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?: {
start: number
end: number
isFirst?: true
isLast?: true
}
}
```

Expand All @@ -142,6 +151,38 @@ Example usage:
/>
```

#### `renderText?: (props: RenderTextProps) => JSX.Element`

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.

The `renderText` function receives an object of type `RenderTextProps` as its argument:

```typescript
export interface RenderTextProps {
text: Text
children: any
attributes: {
'data-slate-node': 'text'
ref: any
}
}
```

Example usage:

```jsx
<Editable
renderText={({ attributes, children, text }) => {
return (
<span {...attributes} className="custom-text">
{children}
{text.tooltipContent && <Tooltip content={text.tooltipContent} />}
</span>
)
}}
/>
```

#### `renderPlaceholder?: (props: RenderPlaceholderProps) => JSX.Element`

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.
Expand Down
24 changes: 24 additions & 0 deletions packages/slate-react/src/components/editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}
}

/**
Expand All @@ -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
Expand All @@ -149,6 +171,7 @@ export const Editable = forwardRef(
readOnly = false,
renderElement,
renderLeaf,
renderText,
renderPlaceholder = defaultRenderPlaceholder,
scrollSelectionIntoView = defaultScrollSelectionIntoView,
style: userStyle = {},
Expand Down Expand Up @@ -1831,6 +1854,7 @@ export const Editable = forwardRef(
renderElement={renderElement}
renderPlaceholder={renderPlaceholder}
renderLeaf={renderLeaf}
renderText={renderText}
selection={editor.selection}
/>
</Component>
Expand Down
5 changes: 5 additions & 0 deletions packages/slate-react/src/components/element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
RenderElementProps,
RenderLeafProps,
RenderPlaceholderProps,
RenderTextProps,
} from './editable'

import Text from './text'
Expand All @@ -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
}) => {
Expand All @@ -44,6 +46,7 @@ const Element = (props: {
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
renderPlaceholder,
renderLeaf,
renderText,
selection,
} = props
const editor = useSlateStatic()
Expand Down Expand Up @@ -71,6 +74,7 @@ const Element = (props: {
renderElement,
renderPlaceholder,
renderLeaf,
renderText,
selection,
})

Expand Down Expand Up @@ -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) &&
Expand Down
12 changes: 10 additions & 2 deletions packages/slate-react/src/components/leaf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -53,6 +53,7 @@ const Leaf = (props: {
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
text: Text
leafPosition?: LeafPosition
}) => {
const {
leaf,
Expand All @@ -61,6 +62,7 @@ const Leaf = (props: {
parent,
renderPlaceholder,
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
leafPosition,
} = props

const editor = useSlateStatic()
Expand Down Expand Up @@ -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) => {
Expand Down
54 changes: 41 additions & 13 deletions packages/slate-react/src/components/text.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
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 {
EDITOR_TO_KEY_TO_ELEMENT,
ELEMENT_TO_NODE,
NODE_TO_ELEMENT,
} from 'slate-dom'
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
import {
RenderLeafProps,
RenderPlaceholderProps,
RenderTextProps,
} from './editable'
import Leaf from './leaf'

/**
Expand All @@ -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}
Expand All @@ -65,22 +78,37 @@ 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 &&
isTextDecorationsEqual(next.decorations, prev.decorations)
)
})

export const DefaultText = (props: RenderTextProps) => {
const { attributes, children } = props
return <span {...attributes}>{children}</span>
}

export default MemoizedText
2 changes: 2 additions & 0 deletions packages/slate-react/src/hooks/use-children.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
RenderElementProps,
RenderLeafProps,
RenderPlaceholderProps,
RenderTextProps,
} from '../components/editable'

import ElementComponent from '../components/element'
Expand All @@ -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
}) => {
Expand Down
1 change: 1 addition & 0 deletions packages/slate-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Loading
Loading