mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-09 08:46:35 +02:00
update docs for removal of commands
This commit is contained in:
@@ -2,72 +2,73 @@
|
||||
|
||||
While editing richtext content, your users will be doing things like inserting text, deleteing text, splitting paragraphs, adding formatting, etc. These edits are expressed using two concepts: commands and operations.
|
||||
|
||||
Commands are the high-level actions that represent a specific intent of the user. Their interface is simply:
|
||||
Commands are the high-level actions that represent a specific intent of the user. They are represented as helper functions on the `Editor` interface. A handful of helpers are included in core for common richtext behaviors, but you are encouraged to write your own that model your specific domain.
|
||||
|
||||
```ts
|
||||
interface Command {
|
||||
type: string
|
||||
[key: string]: any
|
||||
}
|
||||
```
|
||||
|
||||
Slate defines and recognizes a handful of core commands out of the box for common richtext behaviors, like:
|
||||
For example, here are some of the built-in commands:
|
||||
|
||||
```js
|
||||
editor.exec({
|
||||
type: 'insert_text',
|
||||
text: 'A new string of text to be inserted.',
|
||||
})
|
||||
Editor.insertText(editor, 'A new string of text to be inserted.')
|
||||
|
||||
editor.exec({
|
||||
type: 'delete_backward',
|
||||
unit: 'character',
|
||||
})
|
||||
Editor.deleteBackward(editor, { unit: 'word' })
|
||||
|
||||
editor.exec({
|
||||
type: 'insert_break',
|
||||
})
|
||||
Editor.insertBreak(editor)
|
||||
```
|
||||
|
||||
But you can (and will!) also define your own custom commands that model your domain. For example, you might want to define a `wrap_quote` command, or a `insert_image` command, or a `format_bold` command depending on what types of content you allow.
|
||||
But you can (and will!) also define your own custom commands that model your domain. For example, you might want to define a `formatQuote` command, or an `insertImage` command, or a `toggleBold` command depending on what types of content you allow.
|
||||
|
||||
Commands always describe an action to be taken as if the user themselves was performing the action. For that reason, they never need to define a location to perform the command, because they always act on the user's current selection.
|
||||
Commands always describe an action to be taken as if the **user** themselves was performing the action. For that reason, they never need to define a location to perform the command, because they always act on the user's current selection.
|
||||
|
||||
> 🤖 The concept of commands are loosely based on the DOM's built-in [`execCommand`](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand) APIs. However Slate defines its own simpler (and extendable!) version of the API, because the DOM's version is overly complex and has known versatility issues.
|
||||
> 🤖 The concept of commands are loosely based on the DOM's built-in [`execCommand`](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand) APIs. However Slate defines its own simpler (and extendable!) version of the API, because the DOM's version is too opinionated and inconsistent.
|
||||
|
||||
Under the covers, Slate takes care of converting each command into a set of low-level "operations" that are applied to produce a new value. This is what makes collaborative editing implementations possible. But you don't have to worry about that, because it happens automatically.
|
||||
|
||||
## Custom Commands
|
||||
|
||||
When defining custom commands, you'll always trigger them by calling the `editor.exec` function:
|
||||
When defining custom commands, you can create your own namespace:
|
||||
|
||||
```js
|
||||
// Call your custom "insert_image" command.
|
||||
editor.exec({
|
||||
type: 'insert_image',
|
||||
url: 'https://unsplash.com/photos/m0By_H6ofeE',
|
||||
})
|
||||
```
|
||||
const MyEditor = {
|
||||
...Editor,
|
||||
|
||||
And then you define what their behaviors are by overriding the default `editor.exec` command:
|
||||
|
||||
```js
|
||||
const withImages = editor => {
|
||||
const { exec } = editor
|
||||
|
||||
editor.exec = command => {
|
||||
if (command.type === 'insert_image') {
|
||||
// Define your behaviors here...
|
||||
} else {
|
||||
// Call the existing behavior to handle any other commands.
|
||||
exec(command)
|
||||
}
|
||||
}
|
||||
|
||||
return editor
|
||||
insertParagraph(editor) {
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This makes it easy for you to define a handful of commands specific to your problem domain.
|
||||
When writing your own commands, you'll often make use of the `Transforms` helpers that ship with Slate.
|
||||
|
||||
But as you can see with the `withImages` function above, custom logic can be extracted into simple plugins that can be reused and shared with others. This is one of the powerful aspects of Slate's architecture.
|
||||
## Transforms
|
||||
|
||||
Transforms are a specific set of helpers that allow you to perform a wide variety of specific changes to the document, for example:
|
||||
|
||||
```js
|
||||
// Set a "bold" format on all of the text nodes in a range.
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{ bold: true },
|
||||
{
|
||||
at: range,
|
||||
match: node => Text.isText(node),
|
||||
split: true,
|
||||
}
|
||||
)
|
||||
|
||||
// Wrap the lowest block at a point in the document in a quote block.
|
||||
Transforms.wrapNodes(
|
||||
editor,
|
||||
{ type: 'quote', children: [] },
|
||||
{
|
||||
at: point,
|
||||
match: node => Editor.isBlock(editor, node),
|
||||
mode: 'lowest',
|
||||
}
|
||||
)
|
||||
|
||||
// Insert new text to replace the text in a node at a specific path.
|
||||
Transforms.insertText(editor, 'A new string of text.', { at: path })
|
||||
|
||||
// ...there are many more transforms!
|
||||
```
|
||||
|
||||
The transform helpers are designed to be composed together. So you might use a handful of them for each command.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Operations
|
||||
|
||||
Operations are the granular, low-level actions that occur while invoking commands. A single high-level command could result in many low-level operations being applied to the editor.
|
||||
Operations are the granular, low-level actions that occur while invoking commands and transforms. A single high-level command could result in many low-level operations being applied to the editor.
|
||||
|
||||
Unlike commands, operations aren't extendable. Slate's core defines all of the possible operations that can occur on a richtext document. For example:
|
||||
|
||||
|
@@ -5,16 +5,28 @@ All of the behaviors, content and state of a Slate editor is rollup up into a si
|
||||
```ts
|
||||
interface Editor {
|
||||
children: Node[]
|
||||
operations: Operation[]
|
||||
selection: Range | null
|
||||
operations: Operation[]
|
||||
marks: Record<string, any> | null
|
||||
apply: (operation: Operation) => void
|
||||
exec: (command: Command) => void
|
||||
[key: string]: any
|
||||
|
||||
// Schema-specific node behaviors.
|
||||
isInline: (element: Element) => boolean
|
||||
isVoid: (element: Element) => boolean
|
||||
normalizeNode: (entry: NodeEntry) => void
|
||||
onChange: () => void
|
||||
[key: string]: any
|
||||
|
||||
// Overrideable core actions.
|
||||
addMark: (key: string, value: any) => void
|
||||
apply: (operation: Operation) => void
|
||||
deleteBackward: (unit: 'character' | 'word' | 'line' | 'block') => void
|
||||
deleteForward: (unit: 'character' | 'word' | 'line' | 'block') => void
|
||||
deleteFragment: () => void
|
||||
insertBreak: () => void
|
||||
insertFragment: (fragment: Node[]) => void
|
||||
insertNode: (node: Node) => void
|
||||
insertText: (text: string) => void
|
||||
removeMark: (key: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
@@ -24,7 +36,9 @@ The `children` property contains the document tree of nodes that make up the edi
|
||||
|
||||
The `selection` property contains the user's current selection, if any.
|
||||
|
||||
And the `operations` property contains all of the operations that have been applied since the last "change" was flushed. (Since Slate batches operations up into ticks of the event loop.)
|
||||
The `operations` property contains all of the operations that have been applied since the last "change" was flushed. (Since Slate batches operations up into ticks of the event loop.)
|
||||
|
||||
The `marks` property stores formatting that is attached to the cursor, and that will be applied to the text that is inserted next.
|
||||
|
||||
## Overriding Behaviors
|
||||
|
||||
@@ -40,18 +54,18 @@ editor.isInline = element => {
|
||||
}
|
||||
```
|
||||
|
||||
Or maybe you want to define a custom command:
|
||||
Or maybe you want to override the `insertText` behavior to "linkify" URLs:
|
||||
|
||||
```js
|
||||
const { exec } = editor
|
||||
const { insertText } = editor
|
||||
|
||||
editor.exec = command => {
|
||||
if (command.type === 'insert_link') {
|
||||
const { url } = command
|
||||
editor.insertText = text => {
|
||||
if (isUrl(text) {
|
||||
// ...
|
||||
} else {
|
||||
exec(command)
|
||||
return
|
||||
}
|
||||
|
||||
insertText(text)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -65,6 +79,7 @@ editor.normalizeNode = entry => {
|
||||
|
||||
if (Element.isElement(node) && node.type === 'link') {
|
||||
// ...
|
||||
return
|
||||
}
|
||||
|
||||
normalizeNode(entry)
|
||||
@@ -98,18 +113,3 @@ for (const [point] of Editor.positions(editor)) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Another special group of helper functions exposed on the `Editor` interface are the "transform" helpers. They are the lower-level functions that commands use to define their behaviors. For example:
|
||||
|
||||
```js
|
||||
// Insert an element node at a specific path.
|
||||
Transforms.insertNodes(editor, [element], { at: path })
|
||||
|
||||
// Split the nodes in half at a specific point.
|
||||
Transforms.splitNodes(editor, { at: point })
|
||||
|
||||
// Add a quote format to all the block nodes in the selection.
|
||||
Transforms.setNodes(editor, { type: 'quote' })
|
||||
```
|
||||
|
||||
The editor-specific helpers are the ones you'll use most often when working with Slate editors, so it pays to become very familiar with them.
|
||||
|
@@ -4,21 +4,11 @@ You've already seen how the behaviors of Slate editors can be overriden. These o
|
||||
|
||||
A plugin is simply a function that takes an `Editor` object and returns it after it has augmented it in some way.
|
||||
|
||||
For example, a plugin that handles images:
|
||||
For example, a plugin that marks image nodes as "void":
|
||||
|
||||
```js
|
||||
const withImages = editor => {
|
||||
const { exec, isVoid } = editor
|
||||
|
||||
editor.exec = command => {
|
||||
if (command.type === 'insert_image') {
|
||||
const { url } = command
|
||||
const element = { type: 'image', url, children: [{ text: '' }] }
|
||||
Transforms.insertNodes(editor, element)
|
||||
} else {
|
||||
exec(command)
|
||||
}
|
||||
}
|
||||
const { isVoid } = editor
|
||||
|
||||
editor.isVoid = element => {
|
||||
return element.type === 'image' ? true : isVoid(editor)
|
||||
@@ -34,38 +24,31 @@ And then to use the plugin, simply:
|
||||
import { createEditor } from 'slate'
|
||||
|
||||
const editor = withImages(createEditor())
|
||||
|
||||
// Later, when you want to insert an image...
|
||||
editor.exec({
|
||||
type: 'insert_image',
|
||||
url: 'https://unsplash.com/photos/m0By_H6ofeE',
|
||||
})
|
||||
```
|
||||
|
||||
This plugin composition model makes Slate extremely easy to extend!
|
||||
|
||||
## Helpers Functions
|
||||
## Helper Functions
|
||||
|
||||
In addition to the plugin functions, you might want to expose helper functions that are used alongside your plugins. For example:
|
||||
|
||||
```js
|
||||
const ImageElement = {
|
||||
import { Editor, Element } from 'slate'
|
||||
|
||||
const MyEditor = {
|
||||
...Editor,
|
||||
insertImage(editor, url) {
|
||||
const element = { type: 'image', url, children: [{ text: '' }] }
|
||||
Transforms.insertNodes(editor, element)
|
||||
},
|
||||
}
|
||||
|
||||
const MyElement = {
|
||||
...Element,
|
||||
isImageElement(value) {
|
||||
return Element.isElement(element) && element.type === 'image'
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
That way you can reuse your helpers. Or even mix them with the core Slate helpers to create your own bundle, like:
|
||||
|
||||
```js
|
||||
import { Element } from 'slate'
|
||||
import { ImageElement } from './images'
|
||||
|
||||
export const MyElement = {
|
||||
...Element,
|
||||
...ImageElement,
|
||||
}
|
||||
```
|
||||
|
||||
Then you can use `MyElement` everywhere and have access to all your helpers in one place.
|
||||
Then you can use `MyEditor` and `MyElement` everywhere and have access to all your helpers in one place.
|
||||
|
@@ -36,9 +36,9 @@ In attempt to decrease the maintenance burden, and because the new abstraction a
|
||||
|
||||
### Commands
|
||||
|
||||
A new `Command` concept has been introduced. (The old "commands" are now called "transforms".) This new concept expresses the semantic intent of a user editing the document. And they allow for the right abstraction to tap into user behaviors—for example to change what happens when a user presses enter, or backspace, etc. Instead of using `keydown` events you should likely override command behaviors instead.
|
||||
A new "command" concept has been introduced. (The old "commands" are now called "transforms".) This new concept expresses the semantic intent of a user editing the document. And they allow for the right abstraction to tap into user behaviors—for example to change what happens when a user presses enter, or backspace, etc. Instead of using `keydown` events you should likely override command behaviors instead.
|
||||
|
||||
Commands are triggered by calling the `editor.exec` function. And they travel through a middleware-like stack, but built from composed functions. Any plugin can override the `exec` behaviors to augment an editor.
|
||||
Commands are triggered by calling the `editor.*` core functions. And they travel through a middleware-like stack, but built from composed functions. Any plugin can override the behaviors to augment an editor.
|
||||
|
||||
### Plugins
|
||||
|
||||
|
@@ -76,7 +76,7 @@ const App = () => {
|
||||
|
||||
It has the concept of "code blocks" and "bold formatting". But these things are all defined in one-off cases inside the `onKeyDown` handler. If you wanted to reuse that logic elsewhere you'd need to extract it.
|
||||
|
||||
We can instead implement these domain-specific concepts by extending the `editor` object:
|
||||
We can instead implement these domain-specific concepts by creating custom functions:
|
||||
|
||||
```js
|
||||
// Create a custom editor plugin function that will augment the editor.
|
||||
@@ -154,39 +154,6 @@ Since we haven't yet defined (or overridden) any commands in `withCustom`, nothi
|
||||
However, now we can start extract bits of logic into reusable methods:
|
||||
|
||||
```js
|
||||
const withCustom = editor => {
|
||||
const { exec } = editor
|
||||
|
||||
editor.exec = command => {
|
||||
// Define a command to toggle the bold formatting.
|
||||
if (command.type === 'toggle_bold_mark') {
|
||||
const isActive = CustomEditor.isBoldMarkActive(editor)
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{ bold: isActive ? null : true },
|
||||
{ match: n => Text.isText(n), split: true }
|
||||
)
|
||||
}
|
||||
|
||||
// Define a command to toggle the code block formatting.
|
||||
else if (command.type === 'toggle_code_block') {
|
||||
const isActive = CustomEditor.isCodeBlockActive(editor)
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{ type: isActive ? null : 'code' },
|
||||
{ match: n => Editor.isBlock(editor, n) }
|
||||
)
|
||||
}
|
||||
|
||||
// Otherwise, fall back to the built-in `exec` logic for everything else.
|
||||
else {
|
||||
exec(command)
|
||||
}
|
||||
}
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
// Define our own custom set of helpers for active-checking queries.
|
||||
const CustomEditor = {
|
||||
isBoldMarkActive(editor) {
|
||||
@@ -205,10 +172,28 @@ const CustomEditor = {
|
||||
|
||||
return !!match
|
||||
},
|
||||
|
||||
toggleBoldMark(editor) {
|
||||
const isActive = CustomEditor.isBoldMarkActive(editor)
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{ bold: isActive ? null : true },
|
||||
{ match: n => Text.isText(n), split: true }
|
||||
)
|
||||
},
|
||||
|
||||
toggleCodeBlock(editor) {
|
||||
const isActive = CustomEditor.isCodeBlockActive(editor)
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{ type: isActive ? null : 'code' },
|
||||
{ match: n => Editor.isBlock(editor, n) }
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withCustom(withReact(createEditor())), [])
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -243,13 +228,13 @@ const App = () => {
|
||||
switch (event.key) {
|
||||
case '`': {
|
||||
event.preventDefault()
|
||||
editor.exec({ type: 'toggle_code_block' })
|
||||
CustomEditor.toggleCodeBlock(editor)
|
||||
break
|
||||
}
|
||||
|
||||
case 'b': {
|
||||
event.preventDefault()
|
||||
editor.exec({ type: 'toggle_bold_mark' })
|
||||
CustomEditor.toggleBoldMark(editor)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -264,7 +249,7 @@ Now our commands are clearly defined and you can invoke them from anywhere we ha
|
||||
|
||||
```js
|
||||
const App = () => {
|
||||
const editor = useMemo(() => withCustom(withReact(createEditor())), [])
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const [value, setValue] = useState([
|
||||
{
|
||||
type: 'paragraph',
|
||||
@@ -292,7 +277,7 @@ const App = () => {
|
||||
<button
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
editor.exec({ type: 'toggle_bold_mark' })
|
||||
CustomEditor.toggleBoldMark(editor)
|
||||
}}
|
||||
>
|
||||
Bold
|
||||
@@ -300,7 +285,7 @@ const App = () => {
|
||||
<button
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
editor.exec({ type: 'toggle_code_block' })
|
||||
CustomEditor.toggleCodeBlock(editor)
|
||||
}}
|
||||
>
|
||||
Code Block
|
||||
@@ -318,13 +303,13 @@ const App = () => {
|
||||
switch (event.key) {
|
||||
case '`': {
|
||||
event.preventDefault()
|
||||
editor.exec({ type: 'toggle_code_block' })
|
||||
CustomEditor.toggleCodeBlock(editor)
|
||||
break
|
||||
}
|
||||
|
||||
case 'b': {
|
||||
event.preventDefault()
|
||||
editor.exec({ type: 'toggle_bold_mark' })
|
||||
CustomEditor.toggleBoldMark(editor)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -337,20 +322,4 @@ const App = () => {
|
||||
|
||||
That's the benefit of extracting the logic.
|
||||
|
||||
And you don't necessarily need to define it all in the same plugin. You can use the plugin pattern to add logic and behaviors to an editor from elsewhere.
|
||||
|
||||
For example, you can use the `slate-history` package to add a history stack to your editor, like so:
|
||||
|
||||
```js
|
||||
import { Editor } from 'slate'
|
||||
import { withHistory } from 'slate-history'
|
||||
|
||||
const editor = useMemo(
|
||||
() => withCustom(withHistory(withReact(createEditor()))),
|
||||
[]
|
||||
)
|
||||
```
|
||||
|
||||
And there you have it! We just added a ton of functionality to the editor with very little work. And we can keep all of our command logic tested and isolated in a single place, making the code easier to maintain.
|
||||
|
||||
That's why plugins are awesome. They let you get really expressive while also making your codebase easier to manage. And since Slate is built with plugins as a primary consideration, using them is dead simple!
|
||||
|
Reference in New Issue
Block a user