diff --git a/docs/concepts/04-commands.md b/docs/concepts/04-commands.md index 02d9b4688..806431025 100644 --- a/docs/concepts/04-commands.md +++ b/docs/concepts/04-commands.md @@ -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. diff --git a/docs/concepts/05-operations.md b/docs/concepts/05-operations.md index 3219bbbca..7ebbde9c9 100644 --- a/docs/concepts/05-operations.md +++ b/docs/concepts/05-operations.md @@ -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: diff --git a/docs/concepts/06-editor.md b/docs/concepts/06-editor.md index aecc237f2..1bf401416 100644 --- a/docs/concepts/06-editor.md +++ b/docs/concepts/06-editor.md @@ -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 | 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. diff --git a/docs/concepts/07-plugins.md b/docs/concepts/07-plugins.md index 54cc42c72..1031e5f42 100644 --- a/docs/concepts/07-plugins.md +++ b/docs/concepts/07-plugins.md @@ -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. diff --git a/docs/concepts/XX-migrating.md b/docs/concepts/XX-migrating.md index 94df55c5c..2c340a808 100644 --- a/docs/concepts/XX-migrating.md +++ b/docs/concepts/XX-migrating.md @@ -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 diff --git a/docs/walkthroughs/05-executing-commands.md b/docs/walkthroughs/05-executing-commands.md index 231147e3d..1a9d5b7fe 100644 --- a/docs/walkthroughs/05-executing-commands.md +++ b/docs/walkthroughs/05-executing-commands.md @@ -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 = () => {