mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-02-24 09:13:24 +01:00
224 lines
6.6 KiB
Markdown
224 lines
6.6 KiB
Markdown
# Defining Custom Elements
|
|
|
|
In our previous example, we started with a paragraph, but we never actually told Slate anything about the `paragraph` block type. We just let it use its internal default renderer, which uses a plain old `<div>`.
|
|
|
|
But that's not all you can do. Slate lets you define any type of custom blocks you want, like block quotes, code blocks, list items, etc.
|
|
|
|
We'll show you how. Let's start with our app from earlier:
|
|
|
|
```jsx
|
|
const App = () => {
|
|
const editor = useMemo(() => withReact(createEditor()), [])
|
|
const [value, setValue] = useState([
|
|
{
|
|
type: 'paragraph',
|
|
children: [{ text: 'A line of text in a paragraph.' }],
|
|
},
|
|
])
|
|
|
|
return (
|
|
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
|
<Editable
|
|
onKeyDown={event => {
|
|
if (event.key === '&') {
|
|
event.preventDefault()
|
|
editor.insertText('and')
|
|
}
|
|
}}
|
|
/>
|
|
</Slate>
|
|
)
|
|
}
|
|
```
|
|
|
|
Now let's add "code blocks" to our editor.
|
|
|
|
The problem is, code blocks won't just be rendered as a plain paragraph, they'll need to be rendered differently. To make that happen, we need to define a "renderer" for `code` element nodes.
|
|
|
|
Element renderers are just simple React components, like so:
|
|
|
|
```jsx
|
|
// Define a React component renderer for our code blocks.
|
|
const CodeElement = props => {
|
|
return (
|
|
<pre {...props.attributes}>
|
|
<code>{props.children}</code>
|
|
</pre>
|
|
)
|
|
}
|
|
```
|
|
|
|
Easy enough.
|
|
|
|
See the `props.attributes` reference? Slate passes attributes that should be rendered on the top-most element of your blocks, so that you don't have to build them up yourself. You **must** mix the attributes into your component.
|
|
|
|
And see that `props.children` reference? Slate will automatically render all of the children of a block for you, and then pass them to you just like any other React component would, via `props.children`. That way you don't have to muck around with rendering the proper text nodes or anything like that. You **must** render the children as the lowest leaf in your component.
|
|
|
|
And here's a component for the "default" elements:
|
|
|
|
```jsx
|
|
const DefaultElement = props => {
|
|
return <p {...props.attributes}>{props.children}</p>
|
|
}
|
|
```
|
|
|
|
Now, let's add that renderer to our `Editor`:
|
|
|
|
```jsx
|
|
const App = () => {
|
|
const editor = useMemo(() => withReact(createEditor()), [])
|
|
const [value, setValue] = useState([
|
|
{
|
|
type: 'paragraph',
|
|
children: [{ text: 'A line of text in a paragraph.' }],
|
|
},
|
|
])
|
|
|
|
// Define a rendering function based on the element passed to `props`. We use
|
|
// `useCallback` here to memoize the function for subsequent renders.
|
|
const renderElement = useCallback(props => {
|
|
switch (props.element.type) {
|
|
case 'code':
|
|
return <CodeElement {...props} />
|
|
default:
|
|
return <DefaultElement {...props} />
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
|
<Editable
|
|
// Pass in the `renderElement` function.
|
|
renderElement={renderElement}
|
|
onKeyDown={event => {
|
|
if (event.key === '&') {
|
|
event.preventDefault()
|
|
editor.insertText('and')
|
|
}
|
|
}}
|
|
/>
|
|
</Slate>
|
|
)
|
|
}
|
|
|
|
const CodeElement = props => {
|
|
return (
|
|
<pre {...props.attributes}>
|
|
<code>{props.children}</code>
|
|
</pre>
|
|
)
|
|
}
|
|
|
|
const DefaultElement = props => {
|
|
return <p {...props.attributes}>{props.children}</p>
|
|
}
|
|
```
|
|
|
|
Okay, but now we'll need a way for the user to actually turn a block into a code block. So let's change our `onKeyDown` function to add a ``Ctrl-``` shortcut that does just that:
|
|
|
|
```jsx
|
|
// Import the `Editor` and `Transforms` helpers from Slate.
|
|
import { Editor, Transforms } from 'slate'
|
|
|
|
const App = () => {
|
|
const editor = useMemo(() => withReact(createEditor()), [])
|
|
const [value, setValue] = useState([
|
|
{
|
|
type: 'paragraph',
|
|
children: [{ text: 'A line of text in a paragraph.' }],
|
|
},
|
|
])
|
|
|
|
const renderElement = useCallback(props => {
|
|
switch (props.element.type) {
|
|
case 'code':
|
|
return <CodeElement {...props} />
|
|
default:
|
|
return <DefaultElement {...props} />
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
|
<Editable
|
|
renderElement={renderElement}
|
|
onKeyDown={event => {
|
|
if (event.key === '`' && event.ctrlKey) {
|
|
// Prevent the "`" from being inserted by default.
|
|
event.preventDefault()
|
|
// Otherwise, set the currently selected blocks type to "code".
|
|
Transforms.setNodes(
|
|
editor,
|
|
{ type: 'code' },
|
|
{ match: n => Editor.isBlock(editor, n) }
|
|
)
|
|
}
|
|
}}
|
|
/>
|
|
</Slate>
|
|
)
|
|
}
|
|
|
|
const CodeElement = props => {
|
|
return (
|
|
<pre {...props.attributes}>
|
|
<code>{props.children}</code>
|
|
</pre>
|
|
)
|
|
}
|
|
|
|
const DefaultElement = props => {
|
|
return <p {...props.attributes}>{props.children}</p>
|
|
}
|
|
```
|
|
|
|
Now, if you press ``Ctrl-``` the block your cursor is in should turn into a code block! Magic!
|
|
|
|
But we forgot one thing. When you hit ``Ctrl-``` again, it should change the code block back into a paragraph. To do that, we'll need to add a bit of logic to change the type we set based on whether any of the currently selected blocks are already a code block:
|
|
|
|
```jsx
|
|
const App = () => {
|
|
const editor = useMemo(() => withReact(createEditor()), [])
|
|
const [value, setValue] = useState([
|
|
{
|
|
type: 'paragraph',
|
|
children: [{ text: 'A line of text in a paragraph.' }],
|
|
},
|
|
])
|
|
|
|
const renderElement = useCallback(props => {
|
|
switch (props.element.type) {
|
|
case 'code':
|
|
return <CodeElement {...props} />
|
|
default:
|
|
return <DefaultElement {...props} />
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
|
|
<Editable
|
|
renderElement={renderElement}
|
|
onKeyDown={event => {
|
|
if (event.key === '`' && event.ctrlKey) {
|
|
event.preventDefault()
|
|
// Determine whether any of the currently selected blocks are code blocks.
|
|
const [match] = Editor.nodes(editor, {
|
|
match: n => n.type === 'code',
|
|
})
|
|
// Toggle the block type depending on whether there's already a match.
|
|
Transforms.setNodes(
|
|
editor,
|
|
{ type: match ? 'paragraph' : 'code' },
|
|
{ match: n => Editor.isBlock(editor, n) }
|
|
)
|
|
}
|
|
}}
|
|
/>
|
|
</Slate>
|
|
)
|
|
}
|
|
```
|
|
|
|
And there you have it! If you press ``Ctrl-``` while inside a code block, it should turn back into a paragraph!
|