mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-01-19 14:27:07 +01:00
add data model guide, update docs
This commit is contained in:
parent
5b2e53039a
commit
c22839c6eb
@ -17,6 +17,7 @@
|
||||
|
||||
- [Changes](./guides/changes.md)
|
||||
- [Plugins](./guides/plugins.md)
|
||||
- [Data Model](./guides/data-model.md)
|
||||
|
||||
|
||||
## General
|
||||
|
@ -1,20 +0,0 @@
|
||||
|
||||
# Plugins
|
||||
|
||||
In Slate, all custom logic added to the editor is done via plugins. Plugins have complete control over the schema, the behaviors, and the rendering of the components of the editor. Slate encourages you to break up code into small, reusable modules that can be shared with others, and easily reasoned about.
|
||||
|
||||
_To learn more, check out the [Using Plugins guide](../walkthroughs/using-plugins.md), or the [Plugins reference](../reference/slate-react/plugins.md)._
|
||||
|
||||
|
||||
### The Core Plugin
|
||||
|
||||
The "core" plugin that ships with Slate is where the default editing behavior is kept. It performs actions like splitting the current block when `enter` is pressed, or inserting a string of text when the user pastes from their clipboard. But it stops short of anything opinionated, leaving that for userland plugins to add.
|
||||
|
||||
_To learn more, check out the [Core Plugin reference](../reference/slate-react/core-plugin.md)._
|
||||
|
||||
|
||||
### The Editor Plugin
|
||||
|
||||
Plugins are so central to Slate's architecture, that the properties of the [`<Editor>`](../reference/slate-react/editor.md) that allow you to add custom functionality (eg. `onKeyDown` or `onPaste`) are actually implemented as a plugin too. All of those properties actually just create an implicitly, top-priority plugin that gets added at the beginning of the editor's plugin stack. But you'd never even know it!
|
||||
|
||||
_To learn more, check out the [`<Editor>` component reference](../reference/slate-react/editor.md)._
|
@ -1,24 +0,0 @@
|
||||
|
||||
# Statelessness & Immutability
|
||||
|
||||
All of the data in Slate is immutable, thanks to [Immutable.js](https://facebook.github.io/immutable-js/). This makes it much easier to reason about complex editing logic, and it makes maintaining a history of changes for undo/redo much simpler.
|
||||
|
||||
_To learn more, check out the [`State` model reference](../reference/slate/state.md)._
|
||||
|
||||
|
||||
### The `onChange` Handler
|
||||
|
||||
Because of Slate's immutability, you don't actually "set" itself a new state when something changes.
|
||||
|
||||
Instead, the new state is propagated to the Slate editor's parent component, who decides what to do with it. Usually, you'll simply give the new state right back to the editor via React's `this.setState()` method, similarly to other internal component state. But that's up to you!
|
||||
|
||||
_To learn more, check out the [`<Editor>` component reference](../reference/slate-react/editor.md)._
|
||||
|
||||
|
||||
### Changes
|
||||
|
||||
All of the changes in Slate are applied via [`Changes`](../reference/slate/change.md). This makes it possible to enforce some of the constraints that Slate needs to enforce, like requiring that [all leaf nodes be text nodes](./the-document-model.md#leaf-text-nodes). This also makes it possible to implement collaborative editing, where information about changes must be serialized and sent over the network to other editors.
|
||||
|
||||
You should never update the `selection` or `document` of an editor other than by using the [`change()`](../reference/slate/state.md#change) method of a `State`.
|
||||
|
||||
_To learn more, check out the [`Change` model reference](../reference/slate/change.md)._
|
@ -1,34 +0,0 @@
|
||||
|
||||
# The Document Model
|
||||
|
||||
A big difference between Slate and other rich-text editors is that Slate is built on top of a nested, recursive document model—much like the DOM itself. This means you can build complex components like tables or nested block quotes, just like you can with the regular DOM.
|
||||
|
||||
|
||||
### Node Hierarchy
|
||||
|
||||
Each Slate document is made up of [`Document`](../reference/slate/document.md), [`Block`](../reference/slate/block.md), [`Inline`](../reference/slate/inline.md) and [`Text`](../reference/slate/text.md) nodes—again, very similar to the DOM.
|
||||
|
||||
The top-level node of a Slate document is the `Document` node. The `Document` then has child `Block` node children.
|
||||
|
||||
After that, nesting can occur to any depth. `Block` nodes can contain other `Block` nodes, or they can contain `Inline` or `Text` nodes. And `Inline` nodes can contain other `Inline` nodes or simply `Text` nodes.
|
||||
|
||||
Each `Document`, `Block` and `Inline` node implements a [`Node`](../reference/slate/node.md) interface, to make working with the nested tree easier.
|
||||
|
||||
|
||||
### Characters & Marks
|
||||
|
||||
As the leaves of the tree, `Text` nodes contain both the text content of the document as well as all of the text-level formatting, like **bold** and _italic_. In Slate, that formatting is referred to as [`Marks`](../reference/slate/mark.md), and `Marks` are applied to individual [`Characters`](../reference/slate/character.md).
|
||||
|
||||
|
||||
### Void Nodes
|
||||
|
||||
Just like the DOM, Slate's `Block` and `Inline` nodes can be "void" nodes, meaning that they have no content inside of them. When the `isVoid` flag of a node is enabled, it will be specially rendered, to preserve the editing experience that a user expects, without any extra work.
|
||||
|
||||
Note that even though the node is "void", it will still have an empty text node inside of it. Because of...
|
||||
|
||||
|
||||
### Leaf Text Nodes
|
||||
|
||||
One constraint of Slate documents is that the leaf nodes are always `Text` nodes. No `Block` or `Inline` node will ever have no children. It will always have at least an empty text node. (However, you can _render_ text-less nodes, see the [Void Nodes](#void-nodes) section above!)
|
||||
|
||||
This constraint means that [`Ranges`](../reference/slate/range.md) can always refer to text nodes, and many text-node-level operations are always "safe" regardless of the tree's structure.
|
@ -1,25 +0,0 @@
|
||||
|
||||
# The Selection Model
|
||||
|
||||
Slate keeps track of the user's selection in the editor in an immutable data store called a [`Range`](../reference/slate/range.md). By doing this, it lets Slate manipulate the selection with changes, but still update it in the DOM on `render`.
|
||||
|
||||
|
||||
### Always References Text
|
||||
|
||||
One of the constraints of the Slate document model is that [leaf nodes are always text nodes](./the-document-model.md#leaf-text-nodes). This constraint exists to make selection logic simpler.
|
||||
|
||||
A selection always refers to text nodes. Such that even if you set a selection relative to a non-text node, Slate will automatically correct it for you.
|
||||
|
||||
This makes selections easier to reason about, while still giving us the benefits of a recursive document tree, and it makes for a lot less boilerplate.
|
||||
|
||||
|
||||
### Leaf Blocks
|
||||
|
||||
When a selection is used to compute a set of [`Block`](../reference/slate/block.md) nodes, by convention those nodes are always the leaf-most `Block` nodes (ie. the lowest `Block` nodes in the tree at their location). This is important, because the nested document model allows for nested `Block` nodes.
|
||||
|
||||
This convention makes it much simpler to implement selection and changeation logic, since the user's actions are very often supposed to effect the leaf blocks.
|
||||
|
||||
|
||||
### Trunk Inlines
|
||||
|
||||
Unline `Block` nodes, when a selection is used to compute a set of [`Inline`](../reference/slate/inline.md) nodes, the trunk-most nodes are used (ie. the highest `Inline` nodes in the tree at their location). This is done for the same reason, that most user actions are supposed to act at the highest level of inline nodes.
|
125
docs/guides/data-model.md
Normal file
125
docs/guides/data-model.md
Normal file
@ -0,0 +1,125 @@
|
||||
|
||||
# Data Model
|
||||
|
||||
Slate is based on an immutable data model that closely resembles the DOM. When you start using Slate, one of the most important things to do is familiarize yourself with this data model. This guide will help you do just that!
|
||||
|
||||
|
||||
## Mirror the DOM
|
||||
|
||||
One of the main principles of Slate is that it tries to mirror the native DOM API's as much as possible.
|
||||
|
||||
If you think about it, this makes sense. Slate is kind of like a nicer implementation of `contenteditable`, which itself is based on the DOM. And people use the DOM to represent document with rich-text-like structures all the time. Mirroring the DOM helps make the library familiar for new users, and it lets us reuse battle-tested patterns without having to reinvent them ourselves.
|
||||
|
||||
Because it mirrors the DOM, Slate's data model features a [`Document`](../reference/slate/document.md) with [`Block`](../reference/slate/block.md), [`Inline`](../reference/slate/inline.md) and [`Text`](../reference/slate/text.md) nodes. You can reference parts of the document with a [`Range`](../reference/slate/range.md). And there is a special range called a "selection" that represents the user's current cursor selection.
|
||||
|
||||
|
||||
## Immutable Objects
|
||||
|
||||
Slate's data model is built out of [`Immutable.js`](https://facebook.github.io/immutable-js/) objects. This allows us to make rendering much more performant, and it ensures that we don't end up with hard to track down bugs.
|
||||
|
||||
Specifically, Slate's models are [`Immutable.Record`](https://facebook.github.io/immutable-js/docs/#/Record) objects, which makes them very similar to Javascript objects for retrieiving values:
|
||||
|
||||
```js
|
||||
const block = Block.create({ type: 'paragraph' })
|
||||
|
||||
block.kind // "block"
|
||||
block.type // "paragraph"
|
||||
```
|
||||
|
||||
But for updating values, you'll need to use the [`Immutable.Record` API](https://facebook.github.io/immutable-js/docs/#/Record/set).
|
||||
|
||||
And collections of Slate objects are represented as immutable `Lists`, `Sets`, `Stacks`, etc. Which means we get nice support for expressive methods like `filter`, `includes`, `take`, `skip`, `rest`, `last`, etc.
|
||||
|
||||
If you haven't used Immutable.js before, there is definitely a learning curve. Before you give into Slate, you should check out the [Immutable.js documentation](https://facebook.github.io/immutable-js/docs/#/). Once you get the hang of it you won't think twice about it, but it will take a few days to get used to, and you might write things a little "un-performantly" to start.
|
||||
|
||||
|
||||
## The "State"
|
||||
|
||||
The top-level object in Slate—the object encapsulates the entire value of an Slate editor—is called a [`State`](../reference/slate/state.md).
|
||||
|
||||
It is made up of a document filled with content, and a selection representing the user's current cursor selection. It also has a history, to keep track of changes, and a few other more advanced properties like `decorations` and `data`.
|
||||
|
||||
|
||||
## Documents and Nodes
|
||||
|
||||
Slate documents are nested and recursive. This means that a document has block nodes, and those block nodes can have child block nodes—all the way down. This lets you model more complex nested behaviors like tables, or figures with captions.
|
||||
|
||||
Unlike the DOM though, Slate enforces a few more restrictions on its documents, to reduce the complexity involved in manipulating them, and to prevent "impossible" situations from arising. These restrictions are:
|
||||
|
||||
- **Documents can only contain block nodes as direct children.** This restriction mirrors how rich-text editors work, with the top-most elements being blocks that can be split when pressing enter <kbd>enter</kbd>.
|
||||
|
||||
- **Blocks can only contain either other block nodes, or inlines and text nodes.** This is another "sane" restriction that allows you to avoid lots of boilerplate `if` statements in your code. Blocks either wrap other blocks, or contain actual content.
|
||||
|
||||
- **Inlines can only contain inline or text nodes.** This one is also for sanity and avoiding boilerplate. Once you've descended into an "inline" context, you can't have block nodes inside them.
|
||||
|
||||
- **Inlines can't contain no text.** Any inline node whose text is an empty string (`''`) will be automatically removed. This makes sense when you think about a user backspacing through an inline. When they delete that last character, they'd expect the inline to be removed. And when there are no characters, you can't really put your selection into the inline anymore. So Slate removes them from the document automatically, to simplify things.
|
||||
|
||||
- **Text nodes can't be adjacent to other text nodes.** Any two adjacent text nodes will automatically be merged into one. This prevents ambiguous case where a cursor could be at the end of one text node or at the start of the next. However, you can have an inline node surrounded by two texts.
|
||||
|
||||
- **Blocks and inlines must always contain at least one text node.** This is to ensure that the user's cursor can always "enter" the nodes, and to make sure that ranges can be created referencing them.
|
||||
|
||||
Slate enforces all of these restrictions for you automatically. Any time you [perform changes](./changes.md) to the document, Slate will check if the document is invalid, and if so it will return it to a "normalized" state.
|
||||
|
||||
> 🙃 Fun fact: normalizing the document like this is actually based on the DOM's [`Node.normalize()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize)!
|
||||
|
||||
In addition to documents, blocks and inlines, Slate introduces one other type of markup that the DOM doesn't have natively: the [`Mark`](../reference/slate/mark.md).
|
||||
|
||||
|
||||
## Marks
|
||||
|
||||
Marks are how Slate represents formatting data that is attached to the characters in the text itself—things like **bold**, _italic_, `code`, or even more complex formatting like comments.
|
||||
|
||||
Although you can change styling based on either inlines or marks, marks differ form inlines in that they don't affect the structure of the nodes in the document, they simply attach themselves to the characters.
|
||||
|
||||
This makes marks easier to reason about and easier to manipulate. Because inlines involve editing the document's structure, you have to worry about things like splitting any existing nodes, what their order in the hierarchy is, etc. Marks on the other hand can be applied to characters no matter how the characters are nested in the document. If you can express it as a `Range`, you can add marks to it.
|
||||
|
||||
But this also has implications on how marks are rendered. When marks are rendered, the characters are grouped into "leaves" of text that each contain the same set of marks applied to them. But you cannot guarantee how a set of marks will be ordered.
|
||||
|
||||
This is actually similar to the DOM, where this is invalid:
|
||||
|
||||
```html
|
||||
<em>t<strong>e</em>x</strong>t
|
||||
```
|
||||
|
||||
Because the elements don't properly close themselves. Instead you have to write it like this:
|
||||
|
||||
```html
|
||||
<em>t</em><strong><em>e</em>x</strong>t
|
||||
```
|
||||
|
||||
And if you happened to another overlapping section of `<strike>` to that text, you might have to rearrange the closing tags again. Rendering marks in Slate is similar—you can't guarantee that even though a word has one mark applied that that mark will be contiguous, because it depends on how it overlaps with other marks.
|
||||
|
||||
That all sounds pretty complex, but you don't have to think about it much, as long as you use marks and inlines for their intended purposes...
|
||||
|
||||
- Marks represent **unordered**, character-level formatting.
|
||||
- Inlines represent **contiguous**, semantic elements in the document.
|
||||
|
||||
|
||||
## Ranges and "The Selection"
|
||||
|
||||
Just like in the DOM, you can reference a part of the document using a `Range`. And there's one special range that Slate keeps track of called the "selection" that refers to the user's current cursor selection.
|
||||
|
||||
Ranges are defined by an "anchor" and "focus" point. The anchor is where the range starts, and the focus is where it ends. And each point is a combination of an "key" referencing a specific node, and an "offset". This ends up looking like this:
|
||||
|
||||
```js
|
||||
const range = Range.create({
|
||||
anchorKey: 'node-a',
|
||||
anchorOffset: 0,
|
||||
focusKey: 'node-b',
|
||||
focusOffset: 4,
|
||||
isBackward: false,
|
||||
})
|
||||
```
|
||||
|
||||
The more readable `node-a` name is just pseudocode, because Slate uses auto-incrementing numerical strings by default—`'1', '2', '3', ...` But the important part is that every node has a unique `key` property, and a range references nodes by their keys.
|
||||
|
||||
The terms "anchor" and "focus" are borrowed from the DOM, where they mean the same thing. The anchor is where a range starts, and the focus is where it ends. However, be careful because the anchor point doesnt't always _before_ the focus point in the document. Just like in the DOM, it depends on whether the range is backwards or forwards.
|
||||
|
||||
Here's how MDN explains it:
|
||||
|
||||
> A user may make a selection from left to right (in document order) or right to left (reverse of document order). The anchor is where the user began the selection and the focus is where the user ends the selection. If you make a selection with a desktop mouse, the anchor is placed where you pressed the mouse button and the focus is placed where you released the mouse button. Anchor and focus should not be confused with the start and end positions of a selection, since anchor can be placed before the focus or vice versa, depending on the direction you made your selection.
|
||||
> — [`Selection`, MDN](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
|
||||
|
||||
To make dealing with ranges easier though, they also provide "start" and "end" properties that take whether the range is forward or backward into account. The `startKey` and `startOffset` will always before the `endKey` and `endOffset` in the document.
|
||||
|
||||
One important thing to note is that the anchor and focus points of ranges **always reference the "leaf-most" text nodes**. They never reference blocks or inlines, always their child text nodes. This makes dealing with ranges a _lot_ easier.
|
@ -211,3 +211,89 @@ Framework plugins will often expose objects with `changes`, `helpers` and `plugi
|
||||
You'll often want to encapsulate framework plugins in your own feature plugins, but they can go a long way in terms of reducing your codebase size.
|
||||
|
||||
|
||||
## Best Practices
|
||||
|
||||
When you're writing plugins, there are a few patterns to follow that will make your plugins more flexible, and more familiar for others.
|
||||
|
||||
If you think of another good pattern, feel free to pull request it!
|
||||
|
||||
### Write Plugins as Functions
|
||||
|
||||
You should always write plugins as functions that take `options`.
|
||||
|
||||
```js
|
||||
function YourPlugin(options) {
|
||||
return {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is easy to do, and it means that even if you don't have any options now you won't have to break the API to add them in the future. It also makes it easier to use plugins because you just always assume they're functions.
|
||||
|
||||
### Expose Helpers, Changes, etc.
|
||||
|
||||
This was alluded to in the previous section, but if your plugin defines helpers like `hasBoldMark` or change functions like `addBoldMark`, based on an option the user passed it, it can be helpful to expose those to the user so they can use the same functions in their own code. The way to do this is to return an object instead of an array from your plugin function:
|
||||
|
||||
```js
|
||||
function YourBoldPlugin(options) {
|
||||
return {
|
||||
helpers: {
|
||||
hasBoldMark,
|
||||
...
|
||||
},
|
||||
changes: {
|
||||
addBoldMark,
|
||||
...
|
||||
},
|
||||
plugins: [
|
||||
...
|
||||
],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Accept Change Functions
|
||||
|
||||
It's common for a helper plugins to want to make some change based on an event that is triggered by the user. For example, when you want to write a plugin that adds a mark when a hotkey is pressed.
|
||||
|
||||
If you write this in the naive way as taking a mark `type` string, users won't be able to add data associated with the mark in more complex cases. And if you accept a string or an object, what happens if the user wants to actually add two marks at once, or perform some other piece of logic. You'll have to keep adding esoteric options which make the plugin hard to maintain.
|
||||
|
||||
Instead, let the user pass in a "change function", like so:
|
||||
|
||||
```js
|
||||
const plugins = [
|
||||
AddMark({
|
||||
hotkey: 'cmd+b',
|
||||
change: change => change.addMark('bold'),
|
||||
})
|
||||
]
|
||||
```
|
||||
|
||||
Notice how it's still very terse for the simple case. But it means you can do more complex things easily, without having to accept tons of crazy options:
|
||||
|
||||
```js
|
||||
const plugins = [
|
||||
AddMark({
|
||||
hotkey: 'cmd+opt+c',
|
||||
change: change => {
|
||||
change
|
||||
.addMark({ type: 'comment', data: { id: userId }})
|
||||
.selectAll()
|
||||
}
|
||||
})
|
||||
]
|
||||
```
|
||||
|
||||
And what's even better, since it's a common practice to write change function helpers in your codebase to reuse, users can usually just pass in one of the functions they've already defined:
|
||||
|
||||
```js
|
||||
|
||||
const plugins = [
|
||||
AddMark({
|
||||
hotkey: 'cmd+b',
|
||||
change: addBoldMark,
|
||||
})
|
||||
]
|
||||
```
|
||||
|
||||
|
@ -17,15 +17,39 @@ For convenience, in addition to changes, many of the selection and document prop
|
||||
```js
|
||||
State({
|
||||
document: Document,
|
||||
selection: Range
|
||||
selection: Range,
|
||||
history: History,
|
||||
schema: Schema,
|
||||
data: Data,
|
||||
decorations: List<Ranges>|Null,
|
||||
})
|
||||
```
|
||||
|
||||
### `data`
|
||||
`Data`
|
||||
|
||||
An object containing arbitrary data for the state.
|
||||
|
||||
### `decorations`
|
||||
`List<Ranges>|Null`
|
||||
|
||||
A list of ranges in the document with marks that aren't part of the content itself—like matches for the current search string.
|
||||
|
||||
### `document`
|
||||
`Document`
|
||||
|
||||
The current document of the state.
|
||||
|
||||
### `history`
|
||||
`History`
|
||||
|
||||
An object that stores the history of changes.
|
||||
|
||||
### `schema`
|
||||
`Schema`
|
||||
|
||||
An object representing the schema of the state's document.
|
||||
|
||||
### `selection`
|
||||
`Range`
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user