From f183bde599133e1e6ce3549e1f3055e936246b8e Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Wed, 7 Apr 2021 18:03:57 -0400 Subject: [PATCH] add transforms concept docs (#4179) --- docs/Summary.md | 15 +- docs/concepts/04-transforms.md | 190 ++++++++++++++++++ docs/concepts/05-operations.md | 6 +- .../{04-commands.md => 06-commands.md} | 2 +- docs/concepts/{06-editor.md => 07-editor.md} | 0 .../concepts/{07-plugins.md => 08-plugins.md} | 0 .../{08-rendering.md => 09-rendering.md} | 0 .../{09-serializing.md => 10-serializing.md} | 0 .../{10-normalizing.md => 11-normalizing.md} | 0 .../{11-typescript.md => 12-typescript.md} | 0 10 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 docs/concepts/04-transforms.md rename docs/concepts/{04-commands.md => 06-commands.md} (95%) rename docs/concepts/{06-editor.md => 07-editor.md} (100%) rename docs/concepts/{07-plugins.md => 08-plugins.md} (100%) rename docs/concepts/{08-rendering.md => 09-rendering.md} (100%) rename docs/concepts/{09-serializing.md => 10-serializing.md} (100%) rename docs/concepts/{10-normalizing.md => 11-normalizing.md} (100%) rename docs/concepts/{11-typescript.md => 12-typescript.md} (100%) diff --git a/docs/Summary.md b/docs/Summary.md index 07ef06a04..1dc25ef1d 100644 --- a/docs/Summary.md +++ b/docs/Summary.md @@ -17,14 +17,15 @@ - [Interfaces](concepts/01-interfaces.md) - [Nodes](concepts/02-nodes.md) - [Locations](concepts/03-locations.md) -- [Commands](concepts/04-commands.md) +- [Transforms](concepts/04-transforms.md) - [Operations](concepts/05-operations.md) -- [Editor](concepts/06-editor.md) -- [Plugins](concepts/07-plugins.md) -- [Rendering](concepts/08-rendering.md) -- [Serializing](concepts/09-serializing.md) -- [Normalizing](concepts/10-normalizing.md) -- [TypeScript](concepts/11-typescript.md) +- [Commands](concepts/06-commands.md) +- [Editor](concepts/07-editor.md) +- [Plugins](concepts/08-plugins.md) +- [Rendering](concepts/09-rendering.md) +- [Serializing](concepts/10-serializing.md) +- [Normalizing](concepts/11-normalizing.md) +- [TypeScript](concepts/12-typescript.md) - [Migrating](concepts/xx-migrating.md) ## API diff --git a/docs/concepts/04-transforms.md b/docs/concepts/04-transforms.md new file mode 100644 index 000000000..bd2657b9a --- /dev/null +++ b/docs/concepts/04-transforms.md @@ -0,0 +1,190 @@ +# Transforms + +Slate's data structure is immutable, so you can't modify or delete nodes directly. Instead, Slate comes with a collection of "transform" functions that let you change your editor's value. + +Slate's transform functions are designed to be very flexible, to make it possible to represent all kinds of changes you might need to make to your editor. However, that flexibility can be hard to understand at first. + +## Selection Transforms + +Selection-related transforms are some of the simpler ones. For example, here's how you set the selection to a new range: + +```js +Transforms.select(editor, { + anchor: { path: [0, 0], offset: 0 }, + focus: { path: [1, 0], offset: 2 }, +}) +``` + +But they can be more complex too. + +For example, it's common to need to move a cursor forwards or backwards by varying distances—by character, by word, by line. Here's how you'd move the cursor backwards by three words: + +```js +Transforms.move(editor, { + distance: 3, + unit: 'word', + reverse: true, +}) +``` + +> 🤖 + +## Text Transforms + +Text transforms act on the text content of the editor. For example, here's how you'd insert a string of text as a specific point: + +```js +Transforms.insertText(editor, 'some words', { + at: { path: [0, 0], offset: 3 }, +}) +``` + +Or you could delete all of the content in an entire range from the editor: + +```js +Transforms.delete(editor, { + at: { + anchor: { path: [0, 0], offset: 0 }, + focus: { path: [1, 0], offset: 2 }, + }, +}) +``` + +> 🤖 + +## Node Transforms + +Node transforms act on the individual element and text nodes that make up the editor's value. For example you could insert a new text node at a specific path: + +```js +Transforms.insertNodes( + editor, + { + text: 'A new string of text.', + }, + { + at: [0, 1], + } +) +``` + +Or you could move nodes from one path to another: + +```js +Transforms.moveNodes(editor, { + at: [0, 0], + to: [0, 1], +}) +``` + +> 🤖 + +## The `at` Option + +Many transforms act on a specific location in the document. By default, they will use the user's current selection. But this can be overridden with the `at` option. + +For example when inserting text, this would insert the string at the user's current cursor: + +```js +Transforms.insertText(editor, 'some words') +``` + +Whereas this would insert it at a specific point: + +```js +Transforms.insertText(editor, 'some words', { + at: { path: [0, 0], offset: 3 }, +}) +``` + +The `at` option is very versatile, and can be used to implement more complex transforms very easily. Since it is a `Location` it can always be either a `Path`, `Point`, or `Range`. And each of those types of locations will result in slightly different transformations. + +For example, in the case of inserting text, if you specify a `Range` location, the range will first be deleted, collapsing to a single point where your text is then inserted. + +So to replace a range of text with a new string you can do: + +```js +Transforms.insertText(editor, 'some words', { + at: { + anchor: { path: [0, 0], offset: 0 }, + focus: { path: [0, 0], offset: 3 }, + }, +}) +``` + +Or, if you specify a `Path` location, it will expand to a range that covers the entire node at that path. Then, using the range-based behavior it will delete all of the content of the node, and replace it with your text. + +So to replace the text of an entire node with a new string you can do: + +```js +Transforms.insertText(editor, 'some words', { + at: [0, 0], +}) +``` + +These location-based behaviors work for all the transforms that take an `at` option. It can be hard to wrap your head around at first, but it makes the API very powerful and capable of expressing many subtly different transforms. + +## The `match` Option + +Many of the node-based transforms take a `match` function option, which restricts the transform to only apply to nodes for which the function returns `true`. When combined with `at`, `match` can also be very powerful. + +For example, consider a basic transform that moves a node from one path to another: + +```js +Transforms.moveNodes(editor, { + at: [2], + to: [5], +}) +``` + +Although it looks like it simply takes a path and moves it to another place. Under the hood two things are happening: + +1. The `at` option is expanded to be a range representing all of the content inside the node at `[2]`. Which might look something like: + +```js +at: { + anchor: { path: [2, 0], offset: 0 }, + focus: { path: [2, 2], offset: 19 } +} +``` + +2. The `match` option is defaulted to a function that only matches the specific path, in this case `[2]`: + +```js +match: (node, path) => Path.equals(path, [2]) +``` + +Then Slate iterates over the range and moves any nodes that pass the matcher function to the new location. In this case, since `match` is defaulted to only match the exact `[2]` path, that node is moved. + +But what if you wanted to move the children of the node at `[2]` instead? + +You might consider looping over the node's children and moving them one at a time, but this gets very complex to manage because as you move the nodes the paths you're referring to become outdated. + +Instead, you can take advantage of the `at` and `match` options to match all of the children: + +```js +Transforms.moveNodes(editor, { + // This will again be expanded to a range of the entire node at `[2]`. + at: [2], + // Nodes with a path of 1 longer are the children. + match: (node, path) => path.length === 2, +}) +``` + +Here we're using the same `at` path (which is expanded to a range), but instead of letting it match just that path by default, we're supplying our own `match` function which happens to match only the children of the node. + +Using `match` can make representing complex logic a lot simpler. + +For example, consider wanting to add a bold mark to any text nodes that aren't already italic: + +```js +Transform.setNodes(editor, { + // This path references the editor, and is expanded to a range that range + // will encompass all the content of the editor. + at: [], + // This only matches text nodes that are not already italic. + match: (node, path) => Text.isText(node) && node.italic !== true, +}) +``` + +When performing transforms, if you're ever looping over nodes and transforming them one at a time, consider seeing if `match` can solve your use case, and offload the complexity of managing loops to Slate instead. diff --git a/docs/concepts/05-operations.md b/docs/concepts/05-operations.md index 59c1308a9..192870609 100644 --- a/docs/concepts/05-operations.md +++ b/docs/concepts/05-operations.md @@ -1,8 +1,8 @@ # Operations -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. +Operations are the granular, low-level actions that occur while invoking transforms. A single transform 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: +Slate's core defines all of the possible operations that can occur on a richtext document. For example: ```javascript editor.apply({ @@ -31,6 +31,6 @@ editor.apply({ }) ``` -Under the covers Slate converts complex commands into the low-level operations and applies them to the editor automatically, so you rarely have to think about them. +Under the covers Slate converts complex transforms into the low-level operations and applies them to the editor automatically. So you rarely have to think about operations unless you're implementing collaborative editing. > 🤖 Slate's editing behaviors being defined as operations is what makes things like collaborative editing possible, because each change is easily define-able, apply-able, compose-able and even undo-able! diff --git a/docs/concepts/04-commands.md b/docs/concepts/06-commands.md similarity index 95% rename from docs/concepts/04-commands.md rename to docs/concepts/06-commands.md index b2372e5a2..0caed05f6 100644 --- a/docs/concepts/04-commands.md +++ b/docs/concepts/06-commands.md @@ -1,6 +1,6 @@ # Commands -While editing richtext content, your users will be doing things like inserting text, deleting text, splitting paragraphs, adding formatting, etc. These edits are expressed using two concepts: commands and operations. +While editing richtext content, your users will be doing things like inserting text, deleting text, splitting paragraphs, adding formatting, etc. Under the cover these edits are expressed using transforms and operations. But at a high level we talk about them as "commands". 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. diff --git a/docs/concepts/06-editor.md b/docs/concepts/07-editor.md similarity index 100% rename from docs/concepts/06-editor.md rename to docs/concepts/07-editor.md diff --git a/docs/concepts/07-plugins.md b/docs/concepts/08-plugins.md similarity index 100% rename from docs/concepts/07-plugins.md rename to docs/concepts/08-plugins.md diff --git a/docs/concepts/08-rendering.md b/docs/concepts/09-rendering.md similarity index 100% rename from docs/concepts/08-rendering.md rename to docs/concepts/09-rendering.md diff --git a/docs/concepts/09-serializing.md b/docs/concepts/10-serializing.md similarity index 100% rename from docs/concepts/09-serializing.md rename to docs/concepts/10-serializing.md diff --git a/docs/concepts/10-normalizing.md b/docs/concepts/11-normalizing.md similarity index 100% rename from docs/concepts/10-normalizing.md rename to docs/concepts/11-normalizing.md diff --git a/docs/concepts/11-typescript.md b/docs/concepts/12-typescript.md similarity index 100% rename from docs/concepts/11-typescript.md rename to docs/concepts/12-typescript.md