mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-30 18:39:51 +02:00
remove marks, in favor of text properties (#3235)
* remove marks, in favor of text properties * fix lint * fix more examples * update docs
This commit is contained in:
@@ -5,14 +5,13 @@ Slate works with pure JSON objects. All it requires is that those JSON objects c
|
||||
```ts
|
||||
interface Text {
|
||||
text: string
|
||||
marks: Mark[]
|
||||
[key: string]: any
|
||||
}
|
||||
```
|
||||
|
||||
Which means it must have a `text` property with a string of content, and a `marks` property with an array of formatting marks (or an empty array).
|
||||
Which means it must have a `text` property with a string of content.
|
||||
|
||||
But other custom properties are also allowed, and completely up to you. This lets you tailor your data to your specific domain and use case, without Slate getting in the way.
|
||||
But **any** other custom properties are also allowed, and completely up to you. This lets you tailor your data to your specific domain and use case, adding whatever formatting logic you'd like, without Slate getting in the way.
|
||||
|
||||
This interface-based approach separates Slate from most other rich-text editors which require you to work with their hand-rolled "model" classes, and makes it much easier to reason about. It also means that it avoids startup time penalties related to "initializing" the data model.
|
||||
|
||||
|
@@ -16,7 +16,6 @@ const editor = {
|
||||
children: [
|
||||
{
|
||||
text: 'A line of text!',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -127,18 +126,17 @@ Text nodes are the lowest-level nodes in the tree, containing the text content o
|
||||
```ts
|
||||
interface Text {
|
||||
text: string
|
||||
marks: Mark[]
|
||||
[key: string]: any
|
||||
}
|
||||
```
|
||||
|
||||
We'll cover `Mark` objects shortly, but for now you can get an idea for them:
|
||||
For example, a string of bold text:
|
||||
|
||||
```js
|
||||
const text = {
|
||||
text: 'A string of bold text',
|
||||
marks: [{ type: 'bold' }],
|
||||
bold: true,
|
||||
}
|
||||
```
|
||||
|
||||
Text nodes too can contain any custom properties you want, although it is rarer to need to do that.
|
||||
Text nodes too can contain any custom properties you want, and that's how you implement custom formatting like **bold**, _italic_, `code`, etc.
|
||||
|
@@ -20,7 +20,6 @@ const editor = {
|
||||
children: [
|
||||
{
|
||||
text: 'A line of text!',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -107,7 +106,6 @@ const editor = {
|
||||
children: [
|
||||
{
|
||||
text: 'A line of text!',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Commands
|
||||
|
||||
While editing rich-text content, your users will be doing things like inserting text, deleteing text, splitting paragraphs, adding marks, etc. These edits are expressed using two concepts: commands and operations.
|
||||
While editing rich-text 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:
|
||||
|
||||
@@ -25,12 +25,11 @@ editor.exec({
|
||||
})
|
||||
|
||||
editor.exec({
|
||||
type: 'add_mark',
|
||||
mark: { type: 'bold' },
|
||||
type: 'insert_break',
|
||||
})
|
||||
```
|
||||
|
||||
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 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 `wrap_quote` command, or a `insert_image` command, or a `format_bold` 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.
|
||||
|
@@ -1,49 +0,0 @@
|
||||
# Formatting: Marks and Decorations
|
||||
|
||||
We've already seen how `Element` objects can be extended with custom properties to add semantic meaning to your rich-text documents. But there are other kinds of formatting too.
|
||||
|
||||
## `Mark`
|
||||
|
||||
Marks are the lowest level of formatting that is applied directly to the text nodes of your document, for things like **bold**, _italic_ and `code`. Their interface is:
|
||||
|
||||
```ts
|
||||
interface Mark {
|
||||
[key: string]: any
|
||||
}
|
||||
```
|
||||
|
||||
Which means that they are entirely composed of your domain-specific custom properties. For simple cases, it can suffice to use a `type` string:
|
||||
|
||||
```js
|
||||
const bold = { type: 'bold' }
|
||||
const italic = { type: 'italic' }
|
||||
```
|
||||
|
||||
There are multiple techniques you might choose to format or style text. You can implement styling based on inlines or marks. Unlike inlines, marks do not affect the structure of the nodes in the document. Marks simply attach themselves to the characters.
|
||||
|
||||
Marks may be easier to reason about and manipulate because marks do not affect the structure of the document and are associated to the characters. Marks 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. Working with marks instead of inlines does not require you to edit the document's structure, split existing nodes, determine where nodes are in the hierarchy, or other more complex interactions.
|
||||
|
||||
When marks are rendered, the characters are grouped into "leaves" of text that each contain the same set of marks applied to them. One disadvantage of marks is that you cannot guarantee how a set of marks will be ordered.
|
||||
|
||||
This limitation with respect to the ordering of marks is similar to the DOM, where this is invalid:
|
||||
|
||||
```html
|
||||
<em>t<strong>e</em>x</strong>t
|
||||
```
|
||||
|
||||
Because the elements in the above example do not properly close themselves they are invalid. Instead, you would write the above HTML as follows:
|
||||
|
||||
```html
|
||||
<em>t</em><strong><em>e</em>x</strong>t
|
||||
```
|
||||
|
||||
If you happened to add another overlapping section of `<strike>` to that text, you might have to rearrange the closing tags yet 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.
|
||||
|
||||
Of course, this mark ordering stuff sounds pretty complex. But, you do not 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.
|
||||
|
||||
## Decorations
|
||||
|
||||
Decorations are similar to marks, with each one applying to a range of content. However, they are computed at render-time based on the content itself. This is helpful for dynamic formatting like syntax highlighting or search keywords, where changes to the content (or some external data) has the potential to change the formatting.
|
@@ -17,7 +17,6 @@ editor.apply({
|
||||
path: [0, 0],
|
||||
node: {
|
||||
text: 'A line of text!',
|
||||
marks: [],
|
||||
},
|
||||
})
|
||||
|
@@ -95,8 +95,8 @@ for (const [element, path] of Editor.elements(editor, { at: range })) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Iterate over every mark in every text node in the current selection.
|
||||
for (const [mark, index, text, path] of Editor.marks(editor)) {
|
||||
// Iterate over every point in every text node in the current selection.
|
||||
for (const [point] of Editor.positions(editor)) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -110,8 +110,8 @@ Editor.insertNodes(editor, [element], { at: path })
|
||||
// Split the nodes in half at a specific point.
|
||||
Editor.splitNodes(editor, { at: point })
|
||||
|
||||
// Add a mark to all the text in a range.
|
||||
Editor.addMarks(editor, mark, { at: range })
|
||||
// Add a quote format to all the block nodes in the selection.
|
||||
Editor.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.
|
@@ -13,8 +13,7 @@ const withImages = editor => {
|
||||
editor.exec = command => {
|
||||
if (command.type === 'insert_image') {
|
||||
const { url } = command
|
||||
const text = { text: '', marks: [] }
|
||||
const element = { type: 'image', url, children: [text] }
|
||||
const element = { type: 'image', url, children: [{ text: '' }] }
|
||||
Editor.insertNodes(editor)
|
||||
} else {
|
||||
exec(command)
|
@@ -2,7 +2,7 @@
|
||||
|
||||
One of the best parts of Slate is that it's built with React, so it fits right into your existing application. It doesn't re-invent its own view layer that you have to learn. It tries to keep everything as React-y as possible.
|
||||
|
||||
To that end, Slate gives you control over the rendering behavior of your custom elements and custom marks in your rich-text domain.
|
||||
To that end, Slate gives you control over the rendering behavior of your custom nodes and properties in your rich-text domain.
|
||||
|
||||
You can define these behaviors by passing "render props" to the top-level `<Editable>` component.
|
||||
|
||||
@@ -54,24 +54,58 @@ const renderElement = useCallback(props => {
|
||||
}, [])
|
||||
```
|
||||
|
||||
The same thing goes for a custom `renderMark` prop:
|
||||
## Leaves
|
||||
|
||||
When text-level formatting is rendered, the characters are grouped into "leaves" of text that each contain the same formatting applied to them.
|
||||
|
||||
To customize the rendering of each leaf, you use a custom `renderLeaf` prop:
|
||||
|
||||
```jsx
|
||||
const renderMark = useCallback(({ attributes, children, mark }) => {
|
||||
switch (mark.type) {
|
||||
case 'bold':
|
||||
return <strong {...attributes}>{children}</strong>
|
||||
case 'italic':
|
||||
return <em {...attributes}>{children}</em>
|
||||
}
|
||||
const renderLeaf = useCallback(({ attributes, children, leaf }) => {
|
||||
return (
|
||||
<span
|
||||
{...attributes}
|
||||
style={{
|
||||
fontWeight: leaf.bold ? 'bold' : 'normal',
|
||||
fontStyle: leaf.italic ? 'italic' : 'normal',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}, [])
|
||||
```
|
||||
|
||||
> 🤖 Be aware though that marks aren't guaranteed to be "contiguous". Which means even though a **word** is bolded, it's not guaranteed to render as a single `<strong>` element. If some of its characters are also italic, it might be split up into multiple elements—one `<strong>wo</strong>` and one `<em><strong>rd</strong></em>`.
|
||||
Notice though how we've handled it slightly differently than `renderElement`. Since text formatting texts to be fairly simple, we've opted to ditch the `switch` statement and just toggle on/off a few styles instead. (But there's nothing preventing you from using custom components if you'd like!)
|
||||
|
||||
One disadvantage of text-level formatting is that you cannot guarantee that any given format is "contiguous"—meaning that it stays as a single leaf. This limitation with respect to leaves is similar to the DOM, where this is invalid:
|
||||
|
||||
```html
|
||||
<em>t<strong>e</em>x</strong>t
|
||||
```
|
||||
|
||||
Because the elements in the above example do not properly close themselves they are invalid. Instead, you would write the above HTML as follows:
|
||||
|
||||
```html
|
||||
<em>t</em><strong><em>e</em>x</strong>t
|
||||
```
|
||||
|
||||
If you happened to add another overlapping section of `<strike>` to that text, you might have to rearrange the closing tags yet again. Rendering leaves in Slate is similar—you can't guarantee that even though a word has one type of formatting applied to it that that leaf will be contiguous, because it depends on how it overlaps with other formatting.
|
||||
|
||||
Of course, this leaf stuff sounds pretty complex. But, you do not have to think about it much, as long as you use text-level formatting and element-level formatting for their intended purposes:
|
||||
|
||||
- Text properties are for **non-contiguous**, character-level formatting.
|
||||
- Element properties are for **contiguous**, semantic elements in the document.
|
||||
|
||||
## Decorations
|
||||
|
||||
Decorations are another type of text-level formatting. They are similar to regular old custom properties, except each one applies to a `Range` of the document instead of being associated with a given text node.
|
||||
|
||||
However, decorations are computed at **render-time** based on the content itself. This is helpful for dynamic formatting like syntax highlighting or search keywords, where changes to the content (or some external data) has the potential to change the formatting.
|
||||
|
||||
## Toolbars, Menus, Overlays, and more!
|
||||
|
||||
In addition to controlling the rendering of elements and marks inside Slate, you can also retrieve the current editor context from inside other components using the `useSlate` hook.
|
||||
In addition to controlling the rendering of nodes inside Slate, you can also retrieve the current editor context from inside other components using the `useSlate` hook.
|
||||
|
||||
That way other components can execute commands, query the editor state, or anything else.
|
||||
|
@@ -68,9 +68,13 @@ We now use the `beforeinput` event almost exclusively. Instead of having relying
|
||||
|
||||
The core history logic has now finally been extracted into a standalone plugin. This makes it much easier for people to implement their own custom history behaviors. And it ensures that plugins have enough control to augment the editor in complex ways, because the history requires it.
|
||||
|
||||
### Mark-less
|
||||
|
||||
Marks have been removed the Slate data model. Now that we have the ability to define custom properties right on the nodes themselves, you can model marks as custom properties of text nodes. For example bold can be modelled simply as a `bold: true` property.
|
||||
|
||||
### Annotation-less
|
||||
|
||||
Similarly, annotations have been removed from Slate's core. They can be fully implemented now in userland by defining custom operations and rendering annotated ranges using decorations. But most cases should be using plain marks or plain decorations anyways. There were not that many use cases that benefitted from annotations.
|
||||
Similarly, annotations have been removed from Slate's core. They can be fully implemented now in userland by defining custom operations and rendering annotated ranges using decorations. But most cases should be using custom text node properties or decorations anyways. There were not that many use cases that benefitted from annotations.
|
||||
|
||||
## Reductions
|
||||
|
||||
@@ -79,23 +83,23 @@ One of the goals was to dramatically simplify a lot of the logic in Slate to mak
|
||||
To give you a sense for the change in total lines of code:
|
||||
|
||||
```
|
||||
slate 8,436 -> 4,038 (48%)
|
||||
slate-react 3,905 -> 715 (18%)
|
||||
slate 8,436 -> 3,724 (48%)
|
||||
slate-react 3,905 -> 627 (18%)
|
||||
|
||||
slate-base64-serializer 38 -> 0
|
||||
slate-dev-benchmark 340 -> 0
|
||||
slate-dev-environment 102 -> 0
|
||||
slate-dev-test-utils 44 -> 0
|
||||
slate-history 0 -> 201
|
||||
slate-history 0 -> 209
|
||||
slate-hotkeys 62 -> 0
|
||||
slate-html-serializer 253 -> 0
|
||||
slate-hyperscript 447 -> 410
|
||||
slate-hyperscript 447 -> 345
|
||||
slate-plain-serializer 56 -> 0
|
||||
slate-prop-types 62 -> 0
|
||||
slate-react-placeholder 62 -> 0
|
||||
slate-schema 0 -> 504
|
||||
slate-schema 0 -> 439
|
||||
|
||||
total 13,807 -> 5,868 (43%)
|
||||
total 13,807 -> 5,344 (39%)
|
||||
```
|
||||
|
||||
It's quite a big difference! And that doesn't even include the dependencies that were shed in the process too.
|
||||
|
Reference in New Issue
Block a user