mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-31 10:51:44 +02:00
Add controller (#2221)
* fold Stack into Editor * switch Change objects to be tied to editors, not values * introduce controller * add the "commands" concept * convert history into commands on `value.data` * add the ability to not normalize on editor creation/setting * convert schema to a mutable constructor * add editor.command method * convert plugin handlers to receive `next` * switch commands to use the onCommand middleware * add queries support, convert schema to queries * split out browser plugin * remove noop util * fixes * fixes * start fixing tests, refactor hyperscript to be more literal * fix slate-html-serializer tests * fix schema tests with hyperscript * fix text model tests with hyperscript * fix more tests * get all tests passing * fix lint * undo decorations example update * update examples * small changes to the api to make it nicer * update docs * update commands/queries plugin logic * change normalizeNode and validateNode to be middleware * fix decoration removal * rename commands tests * add useful errors to existing APIs * update changelogs * cleanup * fixes * update docs * add editor docs
This commit is contained in:
@@ -80,11 +80,7 @@ Slate encourages you to write small, reusable modules. Check out the public ones
|
||||
|
||||
* [`slate-auto-replace`](https://github.com/ianstormtaylor/slate-auto-replace) auto-replaces text as the user types. Useful for "smart" typography!
|
||||
* [`slate-collapse-on-escape`](https://github.com/ianstormtaylor/slate-collapse-on-escape) simply collapses the selection when `escape` is pressed.
|
||||
* [`slate-edit-code`](https://github.com/GitbookIO/slate-edit-code) adds code editing behavior like tab-to-indent, and enter-to-soft-break.
|
||||
* [`slate-edit-list`](https://github.com/GitbookIO/slate-edit-list) adds rich, nested list editing behavior.
|
||||
* [`slate-edit-table`](https://github.com/GitbookIO/slate-edit-table) adds complex table editing behavior!
|
||||
* [`slate-paste-linkify`](https://github.com/ianstormtaylor/slate-paste-linkify) wraps the selected text in a link when a URL is pasted from the clipboard.
|
||||
* [`slate-prism`](https://github.com/GitbookIO/slate-prism) highlights code blocks with [Prism.js](http://prismjs.com/)!
|
||||
* [`slate-soft-break`](https://github.com/ianstormtaylor/slate-soft-break) adds a soft break when `enter` is pressed.
|
||||
* [`slate-drop-or-paste-images`](https://github.com/ianstormtaylor/slate-drop-or-paste-images) lets users drop or paste images to insert them!
|
||||
* [**View all plugins...**](https://github.com/ianstormtaylor/slate/blob/master/docs/general/plugins.md)
|
||||
|
@@ -12,7 +12,7 @@
|
||||
|
||||
## Guides
|
||||
|
||||
* [Changes](./guides/changes.md)
|
||||
* [Commands & Queries](./guides/commands-and-queries.md)
|
||||
* [Data Model](./guides/data-model.md)
|
||||
* [Plugins](./guides/plugins.md)
|
||||
* [Rendering](./guides/rendering.md)
|
||||
@@ -34,33 +34,25 @@
|
||||
* [Data](./reference/slate/data.md)
|
||||
* [Decoration](./reference/slate/decoration.md)
|
||||
* [Document](./reference/slate/document.md)
|
||||
* [Editor](./reference/slate/editor.md)
|
||||
* [Inline](./reference/slate/inline.md)
|
||||
* [Mark](./reference//slate/mark.md)
|
||||
* [Node](./reference/slate/node.md)
|
||||
* [Operation](./reference/slate/operation.md)
|
||||
* [Plugins](./reference/slate/plugins.md)
|
||||
* [Point](./reference/slate/point.md)
|
||||
* [Range](./reference/slate/range.md)
|
||||
* [Schema](./reference/slate/schema.md)
|
||||
* [Selection](./reference/slate/selection.md)
|
||||
* [Text](./reference/slate/text.md)
|
||||
* [Utils](./reference/slate/utils.md)
|
||||
* [Value](./reference/slate/value.md)
|
||||
* [setKeyGenerator](./reference/slate/utils.md)
|
||||
* [resetKeyGenerator](./reference/slate/utils.md)
|
||||
|
||||
## Slate React
|
||||
|
||||
* [Editor](./reference/slate-react/editor.md)
|
||||
* [Plugins](./reference/slate-react/plugins.md)
|
||||
* [Custom Nodes](./reference/slate-react/custom-nodes.md)
|
||||
* [Core Plugins](./reference/slate-react/core-plugins.md)
|
||||
* [cloneFragment](./reference/slate-react/utils.md)
|
||||
* [findDOMNode](./reference/slate-react/utils.md)
|
||||
* [findDOMRange](./reference/slate-react/utils.md)
|
||||
* [findNode](./reference/slate-react/utils.md)
|
||||
* [findRange](./reference/slate-react/utils.md)
|
||||
* [getEventRange](./reference/slate-react/utils.md)
|
||||
* [getEventTransfer](./reference/slate-react/utils.md)
|
||||
* [setEventTransfer](./reference/slate-react/utils.md)
|
||||
* [Rendering](./reference/slate-react/rendering.md)
|
||||
* [Utils](./reference/slate-react/utils.md)
|
||||
|
||||
## Other Packages
|
||||
|
||||
@@ -68,4 +60,3 @@
|
||||
* [`slate-hyperscript`](./reference/slate-hyperscript/index.md)
|
||||
* [`slate-plain-serializer`](./reference/slate-plain-serializer/index.md)
|
||||
* [`slate-prop-types`](./reference/slate-prop-types/index.md)
|
||||
* [`slate-simulator`](./reference/slate-simulator/index.md)
|
||||
|
@@ -11,11 +11,6 @@ Plugins that add specific behaviors to your editor.
|
||||
| [`slate-auto-replace`](https://yarnpkg.com/en/package/slate-auto-replace) | Automatically transform certain input as a user types. |  |
|
||||
| [`slate-collapse-on-escape`](https://yarnpkg.com/en/package/slate-collapse-on-escape) | Collapse the selection when users hit <kbd>esc</kbd>. |  |
|
||||
| [`slate-drop-or-paste-images`](https://yarnpkg.com/en/package/slate-drop-or-paste-images) | Allows users to insert images by drag-dropping or copy-pasting. |  |
|
||||
| [`slate-edit-blockquote`](https://yarnpkg.com/en/package/slate-edit-blockquote) | Adds blockquote editing behaviors to an editor. |  |
|
||||
| [`slate-edit-code`](https://yarnpkg.com/en/package/slate-edit-code) | Adds code block editing behaviors to an editor. |  |
|
||||
| [`slate-edit-footnote`](https://yarnpkg.com/en/package/slate-edit-footnote) | Adds footnote editing behaviors to an editor. |  |
|
||||
| [`slate-edit-list`](https://yarnpkg.com/en/package/slate-edit-list) | Adds list editing behaviors to an editor. |  |
|
||||
| [`slate-edit-table`](https://yarnpkg.com/en/package/slate-edit-table) | Adds common table editing behaviors to an editor. |  |
|
||||
| [`slate-mark-hotkeys`](https://yarnpkg.com/en/package/slate-mark-hotkeys) | Adds common hotkey formatting utils to an editor. |  |
|
||||
| [`slate-no-empty`](https://yarnpkg.com/en/package/slate-no-empty) | Prevents documents from being empty. |  |
|
||||
| [`slate-paste-linkify`](https://yarnpkg.com/en/package/slate-paste-linkify) | Automatically linkify URLs when they are pasted. |  |
|
||||
|
@@ -17,9 +17,10 @@ These tools are helpful when developing with Slate:
|
||||
|
||||
## Products
|
||||
|
||||
These products are built with Slate, and can give you an idea of what's possible:
|
||||
These products use Slate, and can give you an idea of what's possible:
|
||||
|
||||
* [Cake](https://www.cake.co/)
|
||||
* [Chatterbug](https://chatterbug.com)
|
||||
* [GitBook](https://www.gitbook.com/)
|
||||
* [Grafana](https://grafana.com/)
|
||||
* [Guru](https://www.getguru.com/)
|
||||
@@ -36,4 +37,6 @@ These pre-packaged editors are built on top of Slate, and can be helpful to see
|
||||
* [Nossas Editor](http://slate-editor.bonde.org/) is a drop-in WYSIWYG editor.
|
||||
* [ORY Editor](https://editor.ory.am/) is a self-contained, inline WYSIWYG editor library.
|
||||
* [Outline Editor](https://github.com/outline/rich-markdown-editor) is the editor that powers the [Outline](https://www.getoutline.com/) wiki.
|
||||
* [Chatterslate](https://github.com/chatterbugapp/chatterslate) helps teach language grammar and more at [Chatterbug](https://chatterbug.com)
|
||||
* [Chatterslate](https://github.com/chatterbugapp/chatterslate) helps teach language grammar and more at [Chatterbug](https://chatterbug.com).
|
||||
|
||||
(Or, if you have their exact use case, can be a drop-in editor for you.)
|
||||
|
@@ -1,197 +0,0 @@
|
||||
# Changes
|
||||
|
||||
All changes to a Slate editor's value, whether it's the `selection`, `document`, `history`, etc. happen via "changes"—specifically, via the [`Change`](../reference/slate/change.md) model.
|
||||
|
||||
This is important because the `Change` model is responsible for ensuring that every change to a Slate value can be expressed in terms of low-level [operations](../reference/slate/operation.md). But you don't have to worry about that, because it happens automatically.
|
||||
|
||||
You just need to understand changes...
|
||||
|
||||
## Expressiveness is Key
|
||||
|
||||
Changes in Slate are designed to prioritize expressiveness above almost all else.
|
||||
|
||||
If you're building a powerful editor, it's going to be somewhat complex, and you're going to be writing code to perform all different kinds of programmatic changes. You'll be removing nodes, inserting fragments, moving the selection around, etc.
|
||||
|
||||
And if the API for changes was verbose, or if it required lots of in between steps to be continually performed, your code would balloon to be impossible to understand very quickly.
|
||||
|
||||
To solve this, Slate has very expressive, chainable changes. Like this:
|
||||
|
||||
```js
|
||||
change
|
||||
.focus()
|
||||
.moveToRangeOfDocument()
|
||||
.delete()
|
||||
.insertText('A bit of rich text, followed by...')
|
||||
.moveTo(10)
|
||||
.moveFocusForward(4)
|
||||
.addMark('bold')
|
||||
.moveToEndOfBlock()
|
||||
.insertBlock({
|
||||
type: 'image',
|
||||
data: {
|
||||
src: 'http://placekitten.com/200/300',
|
||||
alt: 'Kittens',
|
||||
className: 'img-responsive',
|
||||
},
|
||||
})
|
||||
.insertBlock('paragraph')
|
||||
```
|
||||
|
||||
Hopefully from reading that you can discern that those changes result in... the entire document's content being selected and deleted, some text being written, a word being bolded, and finally an image block and a paragraph block being added.
|
||||
|
||||
Of course you're not usually going to chain that much.
|
||||
|
||||
Point is, you can get pretty expressive in just a few lines of code.
|
||||
|
||||
That way, when you're scanning to see what behaviors are being triggered, you can understand your code easily. You don't have to sit there and try to parse out a bunch of interim variables to figure out what you're trying to achieve.
|
||||
|
||||
To that end, Slate defines _lots_ of change methods.
|
||||
|
||||
The change methods are the one place in Slate where overlap and near-duplication isn't stomped out. Because sometimes the exact-right change method is the difference between one line of code and ten. And not just ten once, but ten repeated everywhere throughout your codebase.
|
||||
|
||||
## Change Categories
|
||||
|
||||
There are a handful of different categories of changes that ship with Slate by default, and understanding them may help you understand which methods to reach for when trying to write your editor's logic...
|
||||
|
||||
### At a Specific Range
|
||||
|
||||
These are changes like `deleteAtRange()`, `addMarkAtArange()`, `unwrapBlockAtRange()`, etc. that take in a [`Range`](../reference/slate/range.md) argument and apply a change to the document for all of the content in that range. These aren't used that often, because you'll usually be able to get away with using the next category of changes instead...
|
||||
|
||||
### At the Current Selection
|
||||
|
||||
These are changes like `delete()`, `addMark()`, `insertBlock()`, etc. that are the same as the `*AtRange` equivalents, but don't need to take in a range argument, because they apply their edits based on where the user's current selection is. These are often what you want to use when programmatically editing "like a user".
|
||||
|
||||
### On the Selection
|
||||
|
||||
These are changes like `blur()`, `moveToStart()`, `moveToRangeOfNode()`, etc. that change the `value.selection` model and update the user's cursor without affecting the content of the document.
|
||||
|
||||
### On a Specific Node
|
||||
|
||||
There are two types of changes referring to specific nodes, either by `path` or by `key`. These are often what you use when making programmatic changes from inside your custom node components, where you already have a reference to `props.node.key`.
|
||||
|
||||
Path-based changes are ones like `removeNodeByPath()`, `insertNodeByPath()`, etc. that take a `path` pinpointing the node in the document. And key-based changes are ones like `removeNodeByKey()`, `setNodeByKey()`, `removeMarkByKey()`, etc. that take a `key` string referring to a specific node, and then change that node in different ways.
|
||||
|
||||
### On the Top-level Value
|
||||
|
||||
These are changes like `setData()`, `setDecorations()`, etc. that act on the other top-level properties of the [`Value`](../reference/slate/value.md) object. These are more advanced.
|
||||
|
||||
### On the History
|
||||
|
||||
These are changes like `undo()`, `redo()`, etc. that use the operation history and redo or undo changes that have already happened. You generally don't need to worry about these, because they're already bound to the keyboard shortcuts you'd expect, and the user can use them.
|
||||
|
||||
## Making Changes
|
||||
|
||||
When you decide you want to make a change to the Slate value, you're almost always in one of four places...
|
||||
|
||||
### 1. In Slate Handlers
|
||||
|
||||
The first place, is inside a Slate-controlled event handler, like `onKeyDown` or `onPaste`. These handlers take a signature of `event, change, editor`. That `change` argument is a [`Change`](../reference/slate/change.md) object that you can manipulate. For example...
|
||||
|
||||
```js
|
||||
function onKeyDown(event, change, editor) {
|
||||
if (event.key == 'Enter') {
|
||||
change.splitBlock()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Any change methods you call will be applied, and when the event handler stack is finished resolving, the editor will automatically update with those changes.
|
||||
|
||||
### 2. From Custom Node Components
|
||||
|
||||
The second place is inside a custom node component. For example, you might have an `<Image>` component and you want to make a change when the image is clicked.
|
||||
|
||||
In that case, you'll need to use the `change()` method on the Slate [`<Editor>`](../reference/slate-react/editor.md) which you have available as `props.editor`. For example...
|
||||
|
||||
```js
|
||||
class Image extends React.Component {
|
||||
onClick = event => {
|
||||
this.props.editor.change(change => {
|
||||
change.removeNodeByKey(this.props.node.key)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <img {...this.props.attributes} onClick={this.onClick} />
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `editor.change()` method will create a new [`Change`](../reference/slate/change.md) object for you, based on the editor's current value. You can then call any change methods you want, and the new value will be applied to the editor.
|
||||
|
||||
### 3. From Schema Rules
|
||||
|
||||
The third place you may perform change operations—for more complex use cases—is from inside a custom normalization rule in your editor's [`Schema`](../references/slate/schema.md). For example...
|
||||
|
||||
```js
|
||||
{
|
||||
blocks: {
|
||||
list: {
|
||||
nodes: [{
|
||||
match: { type: 'item' }
|
||||
}],
|
||||
normalize: (change, error) => {
|
||||
if (error.code == 'child_type_invalid') {
|
||||
change.wrapBlockByKey(error.child.key, 'item')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When a rule's validation fails, Slate passes a [`Change`](../reference/slate/change.md) object to the `normalize` function of the rule, if one exists. You can use this object to apply the changes necessary to make your document valid on the next normalization pass.
|
||||
|
||||
### 4. From Outside Slate
|
||||
|
||||
This is the fourth place you might want to make changes, and also the most dangerous. You should know that any changes you make outside of the Slate editor might not be seen by your plugins (eg. if they register an `onChange` handler) and may not work with collaborative editing implements.
|
||||
|
||||
That said, if that's okay with you, you can make changes manually by using the `change()` method on a Slate [`Value`](../reference/slate/value.md). For example:
|
||||
|
||||
```js
|
||||
const change = value
|
||||
.change()
|
||||
.moveToRangeOfDocument()
|
||||
.delete()
|
||||
|
||||
const newValue = change.value
|
||||
```
|
||||
|
||||
Note that you'll need to then grab the new value by accessing the `change.value` property directly.
|
||||
|
||||
## Reusing Changes
|
||||
|
||||
In addition to using the built-in changes, if your editor is of any complexity you'll want to write your own reusable changes. That way, you can reuse a single `insertImage` change instead of constantly writing `insertBlock(...args)`.
|
||||
|
||||
To do that, you should define change functions just like Slate's core does—as functions that take `(change, ...args)` arguments. Where `change` is the current mutable change object, and `...args` is anything else you want to accept to perform your change.
|
||||
|
||||
For example, here are two simple block inserting changes...
|
||||
|
||||
```js
|
||||
function insertParagraph(change) {
|
||||
change.insertBlock('paragraph')
|
||||
}
|
||||
|
||||
function insertImage(change, src) {
|
||||
change.insertBlock({
|
||||
type: 'image',
|
||||
data: { src },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Notice how rewriting that image inserting logic multiple times without having it encapsulated in a single function would get tedious. Now with those change functions defined, you can reuse them!
|
||||
|
||||
But sadly you can't chain with those functions directly, since `change` objects don't actually know about them. Instead, you use the `.call()` method:
|
||||
|
||||
```js
|
||||
change.call(insertParagraph).call(insertImage, 'https://google.com/logo')
|
||||
```
|
||||
|
||||
Not only can you use them with `.call()`, but if you're making one-off changes to the `editor`, you can use them with `editor.change()` as well. For example:
|
||||
|
||||
```js
|
||||
editor.change(insertImage, 'https://google.com/logo')
|
||||
```
|
||||
|
||||
That's the benefit of standardizing a function signature!
|
223
docs/guides/commands-and-queries.md
Normal file
223
docs/guides/commands-and-queries.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Commands & Queries
|
||||
|
||||
All commands to a Slate editor's value, whether it's the `selection`, `document`, `history`, etc. happen via "commands" that are applied to a [`Change`]([operations](../reference/slate/change.md).
|
||||
|
||||
Under the covers, Slate takes care of converting each command into a set of low-level [operations](../reference/slate/operation.md) 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.
|
||||
|
||||
You just need to understand commands...
|
||||
|
||||
## Expressiveness is Key
|
||||
|
||||
Commands in Slate are designed to prioritize expressiveness above almost all else.
|
||||
|
||||
If you're building a powerful editor, it's going to be somewhat complex, and you're going to be writing code to perform all different kinds of programmatic commands. You'll be removing nodes, inserting fragments, moving the selection around, etc.
|
||||
|
||||
And if the API for commands was verbose, or if it required lots of in between steps to be continually performed, your code would balloon to be impossible to understand very quickly.
|
||||
|
||||
To solve this, Slate has very expressive, chainable commands. Like this:
|
||||
|
||||
```js
|
||||
change
|
||||
.focus()
|
||||
.moveToRangeOfDocument()
|
||||
.delete()
|
||||
.insertText('A bit of rich text, followed by...')
|
||||
.moveTo(10)
|
||||
.moveFocusForward(4)
|
||||
.addMark('bold')
|
||||
.moveToEndOfBlock()
|
||||
.insertBlock({
|
||||
type: 'image',
|
||||
data: {
|
||||
src: 'http://placekitten.com/200/300',
|
||||
alt: 'Kittens',
|
||||
className: 'img-responsive',
|
||||
},
|
||||
})
|
||||
.insertBlock('paragraph')
|
||||
```
|
||||
|
||||
Hopefully from reading that you can discern that those commands result in... the entire document's content being selected and deleted, some text being written, a word being bolded, and finally an image block and a paragraph block being added.
|
||||
|
||||
Of course you're not usually going to chain that much.
|
||||
|
||||
Point is, you can get pretty expressive in just a few lines of code.
|
||||
|
||||
That way, when you're scanning to see what behaviors are being triggered, you can understand your code easily. You don't have to sit there and try to parse out a bunch of interim variables to figure out what you're trying to achieve.
|
||||
|
||||
To that end, Slate defines _lots_ of commands.
|
||||
|
||||
The commands are the one place in Slate where overlap and near-duplication isn't stomped out. Because sometimes the exact-right command is the difference between one line of code and ten. And not just ten once, but ten repeated everywhere throughout your codebase.
|
||||
|
||||
## Command Categories
|
||||
|
||||
There are a handful of different categories of commands that ship with Slate by default, and understanding them may help you understand which methods to reach for when trying to write your editor's logic...
|
||||
|
||||
### At a Specific Range
|
||||
|
||||
These are commands like `deleteAtRange()`, `addMarkAtArange()`, `unwrapBlockAtRange()`, etc. that take in a [`Range`](../reference/slate/range.md) argument and apply a change to the document for all of the content in that range. These aren't used that often, because you'll usually be able to get away with using the next category of commands instead...
|
||||
|
||||
### At the Current Selection
|
||||
|
||||
These are commands like `delete()`, `addMark()`, `insertBlock()`, etc. that are the same as the `*AtRange` equivalents, but don't need to take in a range argument, because they apply their edits based on where the user's current selection is. These are often what you want to use when programmatically editing "like a user".
|
||||
|
||||
### On the Selection
|
||||
|
||||
These are commands like `blur()`, `moveToStart()`, `moveToRangeOfNode()`, etc. that change the `value.selection` model and update the user's cursor without affecting the content of the document.
|
||||
|
||||
### On a Specific Node
|
||||
|
||||
There are two types of commands referring to specific nodes, either by `path` or by `key`. These are often what you use when making programmatic commands from inside your custom node components, where you already have a reference to `props.node.key`.
|
||||
|
||||
Path-based commands are ones like `removeNodeByPath()`, `insertNodeByPath()`, etc. that take a `path` pinpointing the node in the document. And key-based commands are ones like `removeNodeByKey()`, `setNodeByKey()`, `removeMarkByKey()`, etc. that take a `key` string referring to a specific node, and then change that node in different ways.
|
||||
|
||||
### On the Top-level Value
|
||||
|
||||
These are commands like `setData()`, `setDecorations()`, etc. that act on the other top-level properties of the [`Value`](../reference/slate/value.md) object. These are more advanced.
|
||||
|
||||
### On the History
|
||||
|
||||
These are commands like `undo()`, `redo()`, etc. that use the operation history and redo or undo commands that have already happened. You generally don't need to worry about these, because they're already bound to the keyboard shortcuts you'd expect, and the user can use them.
|
||||
|
||||
## Running Commands
|
||||
|
||||
When you decide you want to make a change to the Slate value, you're almost always in one of four places...
|
||||
|
||||
### 1. In Slate Handlers
|
||||
|
||||
The first place, is inside a Slate-controlled event handler, like `onKeyDown` or `onPaste`. These handlers take a signature of `event, change, next`. That `change` argument is a [`Change`](../reference/slate/change.md) object that you can manipulate. For example...
|
||||
|
||||
```js
|
||||
function onKeyDown(event, change, editor) {
|
||||
if (event.key == 'Enter') {
|
||||
change.splitBlock()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Any commands you call will be applied, and when the event handler stack is finished resolving, the editor will automatically update with those commands.
|
||||
|
||||
### 2. From Custom Node Components
|
||||
|
||||
The second place is inside a custom node component. For example, you might have an `<Image>` component and you want to make a change when the image is clicked.
|
||||
|
||||
In that case, you'll need to use the `change()` method on the Slate [`<Editor>`](../reference/slate-react/editor.md) which you have available as `props.editor`. For example...
|
||||
|
||||
```js
|
||||
class Image extends React.Component {
|
||||
onClick = event => {
|
||||
this.props.editor.change(change => {
|
||||
change.removeNodeByKey(this.props.node.key)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <img {...this.props.attributes} onClick={this.onClick} />
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `editor.change()` method will create a new [`Change`](../reference/slate/change.md) object for you, based on the editor's current plugins and value. You can then call any commands you want, and the new value will be applied to the editor.
|
||||
|
||||
### 3. From Schema Rules
|
||||
|
||||
The third place you may perform change operations—for more complex use cases—is from inside a custom normalization rule in your editor's [`Schema`](../references/slate/schema.md). For example...
|
||||
|
||||
```js
|
||||
{
|
||||
blocks: {
|
||||
list: {
|
||||
nodes: [{
|
||||
match: { type: 'item' }
|
||||
}],
|
||||
normalize: (change, error) => {
|
||||
if (error.code == 'child_type_invalid') {
|
||||
change.wrapBlockByKey(error.child.key, 'item')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When a rule's validation fails, Slate passes a [`Change`](../reference/slate/change.md) object to the `normalize` function of the rule, if one exists. You can use this object to apply the commands necessary to make your document valid on the next normalization pass.
|
||||
|
||||
### 4. From Outside Slate
|
||||
|
||||
The last place is from outside of Slate. Sometimes you'll have components that live next to your
|
||||
editor in the render tree, and you'll need to explicitly pass them a reference to the Slate `editor` to run changes. In React you do this with the `ref={}` prop...
|
||||
|
||||
```js
|
||||
<Editor ref={this.editor = editor} ...>
|
||||
```
|
||||
|
||||
Which gives you a reference to the Slate editor. And from there you can use the same `editor.change` syntax from above to apply changes.
|
||||
|
||||
## Running Queries
|
||||
|
||||
Queries are similar to commands, but instead of manipulating the current value of the editor, they return information about the current value, or a specific node, etc.
|
||||
|
||||
By default, Slate only defines two queries: `isAtomic` for marks and decorations, and `isVoid` for nodes. You can access them directly on the change object:
|
||||
|
||||
```js
|
||||
const isVoid = change.isVoid(node)
|
||||
```
|
||||
|
||||
But you can also define your own queries that are specific to your schema. For example, you might use a query to determine whether the "bold" mark is active...
|
||||
|
||||
```js
|
||||
const isBold = change.isBoldActive(value)
|
||||
```
|
||||
|
||||
And then use that information to mark the <kbd>bold</kbd> button in your editor's toolbar as active or not.
|
||||
|
||||
## Reusing Commands and Queries
|
||||
|
||||
In addition to using the built-in commands, if your editor is of any complexity you'll want to write your own reusable commands. That way, you can reuse a single `insertImage` change instead of constantly writing `insertBlock(...args)`.
|
||||
|
||||
To do that, you can define commands in your own Slate plugin, which will be made available as methods on the `change` object specific to your editor. For example, here are two simple block inserting commands...
|
||||
|
||||
```js
|
||||
const yourPlugin = {
|
||||
commands: {
|
||||
insertParagraph(change) {
|
||||
change.insertBlock('paragraph')
|
||||
},
|
||||
|
||||
insertImage(change, src) {
|
||||
change.insertBlock({
|
||||
type: 'image',
|
||||
data: { src },
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notice how rewriting that image inserting logic multiple times without having it encapsulated in a single function would get tedious. Now with those change functions defined, you can reuse them!
|
||||
|
||||
```js
|
||||
change.insertImage('https://google.com/logo.png')
|
||||
```
|
||||
|
||||
And any arguments you pass in are sent to your custom command functions.
|
||||
|
||||
The same thing goes for queries, which can be defined in plugins and re-used across your entire codebase. To do so, define a `queries` object:
|
||||
|
||||
```js
|
||||
const yourPlugin = {
|
||||
queries: {
|
||||
getActiveListItem(value) {
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And then you can use them:
|
||||
|
||||
```js
|
||||
change.getActiveListItem(change.value)
|
||||
```
|
||||
|
||||
This reusability is key to being able to organize your commands and queries, and compose them together to create more advanced behaviors.
|
@@ -14,7 +14,7 @@ Because it mirrors the DOM, Slate's data model features a [`Document`](../refere
|
||||
|
||||
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 due to accidentally modifying objects in-place.
|
||||
|
||||
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:
|
||||
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' })
|
||||
@@ -33,7 +33,7 @@ If you haven't used Immutable.js before, there is definitely a learning curve. B
|
||||
|
||||
The top-level object in Slate—the object encapsulating the entire value of an Slate editor—is called a [`Value`](../reference/slate/value.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`.
|
||||
It is made up of a document filled with content, and a selection representing the user's current cursor selection. It also has a few other more advanced properties like `decorations` and `data`.
|
||||
|
||||
> 📋 For more info, check out the [`Value` reference](../reference/slate/value.md).
|
||||
|
||||
@@ -53,7 +53,7 @@ Unlike the DOM though, Slate enforces a few more restrictions on its documents.
|
||||
|
||||
* **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" value.
|
||||
Slate enforces all of these restrictions for you automatically. Any time you [run commands](./commands-and-queries.md) that manipulate the document, Slate will check if the document is invalid, and if so, it will return it to a "normalized" value.
|
||||
|
||||
> 🙃 Fun fact: "normalizing" is actually based on the DOM's [`Node.normalize()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize)!
|
||||
|
||||
|
@@ -2,38 +2,46 @@
|
||||
|
||||
With Slate, _all_ of your editor's logic is controlled by "plugins".
|
||||
|
||||
Plugins have complete control over the schema, the behaviors, and the rendering of the editor—they can add any kind of functionality they want. So much so that even the core logic of Slate is provided via two "core" plugins.
|
||||
Plugins have complete control over the schema, the behaviors, and the rendering of the editor—they can add any kind of functionality they want. So much so that even the core logic of Slate is defined by its own plugins.
|
||||
|
||||
Slate encourages you to break up code into small, reusable modules that can be shared with others, and easily reasoned about.
|
||||
|
||||
## What Are Plugins?
|
||||
|
||||
Slate's plugins are simply a collection of functions that all contribute to a shared behavior—each with a specific name and set of arguments. For a full list of the arguments, check out the [`Plugins` reference](../reference/slate-react/plugins.md).
|
||||
Slate's plugins are plain JavaScript objects containing a collection of functions that all contribute to a shared behavior—each with a specific name and set of arguments. For a full list of the arguments, check out the [Plugins](../reference/slate/plugins.md) and [React Plugins](../reference/slate-react/plugins.md) references.
|
||||
|
||||
Here's a really simple plugin:
|
||||
When building a plugin module, it should always export a function that takes options. This way even if it doesn't take any options now, it won't be a breaking API change to take more options in the future.
|
||||
|
||||
So a basic plugin might look like this:
|
||||
|
||||
```js
|
||||
{
|
||||
onKeyDown(event, change, editor) {
|
||||
if (event.key == 'Escape') {
|
||||
change.blur()
|
||||
}
|
||||
},
|
||||
onClick(event, change, editor) {
|
||||
if (change.value.selection.isBlurred) {
|
||||
change.moveToRangeOfDocument().focus()
|
||||
}
|
||||
}
|
||||
export default function MySlatePlugin(options) {
|
||||
return {
|
||||
onKeyDown(event, change, next) {
|
||||
if (event.key == options.key) {
|
||||
change.blur()
|
||||
return true
|
||||
}
|
||||
},
|
||||
onClick(event, change, next) {
|
||||
if (change.value.selection.isBlurred) {
|
||||
change.moveToRangeOfDocument().focus()
|
||||
return true
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It focuses the editor and selects everything when it is clicked, and it blurs the editor when <kbd>esc</kbd> is pressed.
|
||||
It focuses the editor and selects everything when it is clicked, and it blurs the editor when `options.key` is pressed.
|
||||
|
||||
Notice how it's able to define a set of behaviors that work together to form a single "feature" in the editor. That's what makes Slate's plugins a powerful form of encapsulation.
|
||||
Notice how it's able to define a set of behaviors, reacting to different events, that work together to form a single "feature" in the editor. That's what makes Slate's plugins a powerful form of encapsulation.
|
||||
|
||||
Also notice how it returns `true`. This is a convention that allows other plugins in the stack to know that an event has been "handled" and that they can abort without duplicating logic.
|
||||
|
||||
## The Plugins "Stack"
|
||||
|
||||
Slate's editor takes a list of plugins as one of its arguments. We refer to this list as the plugins "stack". It is very similar to "middleware" from Express or Koa.
|
||||
Slate's editor takes a list of plugins as one of its arguments. We refer to this list as the plugins "stack". It is very similar to "middleware" from Express or Koa, except instead of just a single stack of handler functions, there are multiple stacks for each type of request.
|
||||
|
||||
```js
|
||||
const plugins = [
|
||||
@@ -58,9 +66,7 @@ The core plugins doesn't have any assumptions about your schema, or what types o
|
||||
|
||||
These are behaviors that all rich-text editors exhibit, and that don't make sense for userland to have to re-invent for every new editor.
|
||||
|
||||
There are two core plugins: the "before plugin" and the "after plugin". They get their names because one of them is before all other plugins in the stack, and the other is after them.
|
||||
|
||||
For the most part you don't need to worry about the core plugins. The before plugin helps to pave over editing inconsistencies, and the after plugin serves as a fallback, to implement the default behavior in the event that your own plugins choose not to handle a specific event.
|
||||
For the most part you don't need to worry about the core plugins.
|
||||
|
||||
_To learn more, check out the [Core Plugin reference](../reference/slate-react/core-plugins.md)._
|
||||
|
||||
@@ -80,9 +86,9 @@ const plugins = [
|
||||
/>
|
||||
```
|
||||
|
||||
This is nice because it makes simple cases easier, and nicely mimics the native DOM API of `<input>` and `<textarea>`.
|
||||
This is nice because it makes the editor feel like a proper React component, it makes writing simple editors easier, and nicely mimics the native DOM API of `<input>` and `<textarea>`.
|
||||
|
||||
But under the covers, those editor handlers are actually just a convenient way of writing a plugin. Internally, the editor grabs all of those plugin-like properties, and turns them into an "editor" plugin that it places first in the plugins stack. So that example above is actually equivalent to...
|
||||
But under the covers, those editor handlers are actually just a convenient way of writing a plugin. Internally, the editor grabs all of those plugin-like properties, and turns them into an "editor" plugin that it places at the beginning of its plugins array. So that example above is actually equivalent to...
|
||||
|
||||
```js
|
||||
const plugins = [
|
||||
@@ -99,7 +105,7 @@ This isn't something you need to remember, but it's helpful to know that even th
|
||||
|
||||
## Helper Plugins vs. Feature Plugins
|
||||
|
||||
Plugins _can_ do anything and everything. But that doesn't mean you should build plugins that are thousands of lines long that implement every single feature in your editor—your codebase would become hell to maintain. Instead, just like all modules, you should split them up into pieces with separate concerns.
|
||||
Plugins _can_ do anything and everything. But that doesn't mean you should build a single plugin that is thousands of lines long that implements every single feature in your editor—your codebase would be hell to maintain. Instead, just like all modules, you should split them up into pieces with separate concerns.
|
||||
|
||||
A distinction that helps with this is to consider two different types of plugins: "helper plugins" and "feature plugins".
|
||||
|
||||
@@ -132,15 +138,15 @@ const plugins = [
|
||||
]
|
||||
```
|
||||
|
||||
These types of plugins are critical to keeping your code maintainable. And they're good candidates for open-sourcing for others to use. A few examples of plugins like this in the wild are [`slate-auto-replace`](https://github.com/ianstormtaylor/slate-plugins/tree/master/packages/slate-auto-replace), [`slate-prism`](https://github.com/GitbookIO/slate-prism), [`slate-collapse-on-escape`](https://github.com/ianstormtaylor/slate-plugins/tree/master/packages/slate-collapse-on-escape), etc.
|
||||
These types of plugins are critical to keeping your code maintainable. And they're good candidates for open-sourcing for others to use. A few examples of plugins like this in the wild are [`slate-auto-replace`](https://github.com/ianstormtaylor/slate-plugins/tree/master/packages/slate-auto-replace), [`slate-collapse-on-escape`](https://github.com/ianstormtaylor/slate-plugins/tree/master/packages/slate-collapse-on-escape), etc.
|
||||
|
||||
There's almost no piece of logic too small to abstract out and share, as long as it's reusable.
|
||||
There's almost no piece of logic too small to abstract out and share, as long as it's reusable and not opinionated about the editor's schema.
|
||||
|
||||
But hotkey binding logic by itself isn't a "feature". It's just a small helper that makes building more complex features a lot more expressive.
|
||||
|
||||
### Feature Plugins
|
||||
|
||||
Feature plugins are much larger in scope, and serve to define an entire series of behaviors that make up a single "feature" in your editor. They're not as concrete as util plugins, but they make reasoning about complex editors much simpler.
|
||||
Feature plugins are larger in scope, and serve to define an entire series of behaviors that make up a single "feature" in your editor. They're not as concrete as helper plugins, but they make reasoning about complex editors much simpler.
|
||||
|
||||
For example, you maybe decide you want to allow **bold** formatting in your editor. To do that, you need a handful of different behaviors.
|
||||
|
||||
@@ -163,61 +169,15 @@ This is just pseudo-code, but you get the point.
|
||||
|
||||
You've created a single plugin that defines the entire bold "feature". If you go to your editor and you removed the `Bold` plugin, the entire bold "feature" would be removed. Having it encapsulated like this makes it much easier to maintain.
|
||||
|
||||
More often than not you actually want to change the return value to not just be an array of plugins, but actually an object containing other helpful tools that are associated with the feature, like pre-defined change functions. For example:
|
||||
|
||||
```js
|
||||
function Bold(options) {
|
||||
return {
|
||||
changes: {
|
||||
addBoldMark,
|
||||
removeBoldMark,
|
||||
toggleBoldMark,
|
||||
},
|
||||
components: {
|
||||
BoldMark,
|
||||
BoldButton,
|
||||
},
|
||||
helpers: {
|
||||
hasBoldMark,
|
||||
},
|
||||
plugins: [
|
||||
Hotkey('cmd+b', addBoldMark),
|
||||
RenderMark('bold', props => <BoldMark {...props} />),
|
||||
RenderButton(props => <BoldButton {...props} />),
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With things like `changes` and `helpers`, you can define your bold logic in a single place, and allow other parts of your codebase to use the exposed API to keep consistent. Then you can use them like so:
|
||||
|
||||
```js
|
||||
const bold = Bold()
|
||||
const italic = Italic()
|
||||
...
|
||||
|
||||
const plugins = [
|
||||
...bold.plugins,
|
||||
...italic.plugins,
|
||||
...
|
||||
]
|
||||
|
||||
<Editor
|
||||
plugins={plugins}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
Feature plugins like that are almost always made up of many smaller helper plugins. And they are usually app-specific, so they don't make great open-source candidates.
|
||||
Feature plugins are usually app-specific, so they don't make great open-source candidates.
|
||||
|
||||
### Framework Plugins
|
||||
|
||||
That said, there might be another type of plugins that kind of straddle the line. Continuining our analogy to the Javascript package landscape, you might call these "framework" plugins.
|
||||
That said, there might be another type of plugins that kind of straddle the line. Continuining our analogy to the JavaScript package landscape, you might call these "framework" plugins.
|
||||
|
||||
These are plugins that bundle up a set of logic, similar to how a feature might, but in a way that is re-usable across codebases. Some examples of these would be [`slate-edit-code`](https://github.com/GitbookIO/slate-edit-code), [`slate-edit-list`](https://github.com/GitbookIO/slate-edit-list), [`slate-edit-table`](https://github.com/GitbookIO/slate-edit-table), etc.
|
||||
|
||||
Framework plugins will often expose objects with `changes`, `helpers` and `plugins` instead of a simple array. Or, they may choose to just augment a single returned plugin object with some of the other exports.
|
||||
Framework plugins will often define their own commands, queries and even schema—ideally letting you customize these as needed. And they'll use these commands to provide some larger behavior that's common to many apps, like editing lists or tables.
|
||||
|
||||
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.
|
||||
|
||||
@@ -241,68 +201,41 @@ function YourPlugin(options) {
|
||||
|
||||
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.
|
||||
### Register Commands and Queries
|
||||
|
||||
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:
|
||||
This was alluded to in the previous section, but if your plugin defines queries like `hasBoldMark` or commands like `addBoldMark`, it can be helpful to expose those to the user so they can use the same functions in their own code.
|
||||
|
||||
```js
|
||||
function YourBoldPlugin(options) {
|
||||
return {
|
||||
helpers: {
|
||||
queries: {
|
||||
hasBoldMark,
|
||||
...
|
||||
},
|
||||
changes: {
|
||||
commands: {
|
||||
addBoldMark,
|
||||
...
|
||||
},
|
||||
plugins: [
|
||||
...
|
||||
],
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Accept Change Functions
|
||||
Even better is to have a default behavior for these commands and queries, but to allow the user to override them, or provide their own command string to use instead. This way you make the default easy, but still allow for use cases with slightly different needs.
|
||||
|
||||
It's common for 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.
|
||||
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:
|
||||
Instead, let the user pass in a command name, like so:
|
||||
|
||||
```js
|
||||
const plugins = [
|
||||
AddMark({
|
||||
hotkey: 'cmd+b',
|
||||
change: change => change.addMark('bold'),
|
||||
command: 'addBoldMark',
|
||||
}),
|
||||
]
|
||||
```
|
||||
|
||||
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 } })
|
||||
.moveBackward(3)
|
||||
.delete()
|
||||
},
|
||||
}),
|
||||
]
|
||||
```
|
||||
|
||||
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,
|
||||
}),
|
||||
]
|
||||
```
|
||||
That way they can choose exactly what logic adding a bold mark entails.
|
||||
|
@@ -13,7 +13,7 @@ Using custom components for the nodes and marks is the most common rendering nee
|
||||
The function is called with the node's props, including `props.node` which is the node itself. You can use these to determine what to render. For example, you can render nodes using simple HTML elements:
|
||||
|
||||
```js
|
||||
function renderNode(props) {
|
||||
function renderNode(props, next) {
|
||||
const { node, attributes, children } = props
|
||||
|
||||
switch (node.type) {
|
||||
@@ -25,6 +25,8 @@ function renderNode(props) {
|
||||
const src = node.data.get('src')
|
||||
return <img {...attributes} src={src} />
|
||||
}
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -34,11 +36,16 @@ function renderNode(props) {
|
||||
You don't have to use simple HTML elements, you can use your own custom React components too:
|
||||
|
||||
```js
|
||||
function renderNode(props) {
|
||||
function renderNode(props, next) {
|
||||
switch (props.node.type) {
|
||||
case 'paragraph': <ParagraphComponent {...props} />
|
||||
case 'quote': <QuoteComponent {...props} />
|
||||
...
|
||||
case 'paragraph':
|
||||
return <ParagraphComponent {...props} />
|
||||
case 'quote':
|
||||
return <QuoteComponent {...props} />
|
||||
case 'image':
|
||||
return <ImageComponent {...props} />
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -48,7 +55,7 @@ And you can just as easily put that `renderNode` logic into a plugin, and pass t
|
||||
```js
|
||||
function SomeRenderingPlugin() {
|
||||
return {
|
||||
renderNode(props) {
|
||||
renderNode(props, next) {
|
||||
...
|
||||
}
|
||||
}
|
||||
@@ -68,7 +75,7 @@ const plugins = [
|
||||
Marks work the same way, except they invoke the `renderMark` function. Like so:
|
||||
|
||||
```js
|
||||
function renderMark(props) {
|
||||
function renderMark(props, next) {
|
||||
const { children, mark, attributes } = props
|
||||
switch (mark.type) {
|
||||
case 'bold':
|
||||
@@ -81,6 +88,8 @@ function renderMark(props) {
|
||||
return <u {...{ attributes }}>{children}</u>
|
||||
case 'strikethrough':
|
||||
return <strike {...{ attributes }}>{children}</strike>
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -98,11 +107,11 @@ By default Slate will render a placeholder for you which mimics the native DOM `
|
||||
However sometimes you want to customize things. Or maybe you want to render placeholders inside specific blocks like inside an image caption. To do that, you can define your own `renderPlaceholder` function:
|
||||
|
||||
```js
|
||||
function renderPlaceholder(props) {
|
||||
function renderPlaceholder(props, next) {
|
||||
const { node, editor } = props
|
||||
if (node.object != 'block') return
|
||||
if (node.type != 'caption') return
|
||||
if (node.text != '') return
|
||||
if (node.object != 'block') return next()
|
||||
if (node.type != 'caption') return next()
|
||||
if (node.text != '') return next()
|
||||
|
||||
return (
|
||||
<span
|
||||
@@ -129,14 +138,14 @@ Not only can you control the rendering behavior of the components inside the edi
|
||||
This sounds weird, but it can be pretty useful if you want to render additional top-level elements from inside a plugin. To do so, you use the `renderEditor` function:
|
||||
|
||||
```js
|
||||
function renderEditor(props) {
|
||||
function renderEditor(props, next) {
|
||||
const { children, editor } = props
|
||||
const wordCount = countWords(editor.value.text)
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
<React.Fragment>
|
||||
{props.children}
|
||||
<span className="word-count">{wordCount}</span>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -6,15 +6,15 @@ This turns out to be extremely helpful when building complex editors, because it
|
||||
|
||||
That said, just because Slate is agnostic doesn't mean you aren't going to need to enforce a "schema" for your documents.
|
||||
|
||||
To that end, Slate provides a `Schema` model, which allows you to easily define validations for the structure of your documents, and to fix them if the document ever becomes invalid. This guide will show you how they work.
|
||||
To that end, Slate lets you define validations for the structure of your documents, and to fix them if the document ever becomes invalid. This guide will show you how they work.
|
||||
|
||||
> ❗️To tell Slate about your custom schema add it to the editor as a prop [like this](https://github.com/ianstormtaylor/slate/blob/405cef0225c314b4162d587c74cfce6b65a7b257/examples/forced-layout/index.js#L62).
|
||||
> 🤖 To see a full example of a schema in affect, check out the [Forced Layout](https://github.com/ianstormtaylor/slate/blob/405cef0225c314b4162d587c74cfce6b65a7b257/examples/forced-layout/index.js#L62) example.
|
||||
|
||||
## Basic Schemas
|
||||
|
||||
Slate schemas are defined as Javascript objects, with properties that describe the document, block nodes, and inline nodes in your editor. Here's a simple schema:
|
||||
Slate schemas are defined as JavaScript objects, with properties that describe the document, block nodes, and inline nodes in your editor. Here's a simple schema:
|
||||
|
||||
```js
|
||||
```jsx
|
||||
const schema = {
|
||||
document: {
|
||||
nodes: [
|
||||
@@ -39,9 +39,13 @@ const schema = {
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
> 🤖 Internally, Slate instantiates schemas as immutable `Schema` models, but you don't have to worry about that. In user-land schemas can always be defined as plain Javascript objects, and you can let Slate handle the rest.
|
||||
<Editor
|
||||
schema={schema}
|
||||
value={this.state.value}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
Hopefully just by reading this definition you'll understand what kinds of blocks are allowed in the document and what properties they can have—schemas are designed to prioritize legibility.
|
||||
|
||||
@@ -51,6 +55,8 @@ The magic is that by passing a schema like this into your editor, it will automa
|
||||
|
||||
This way you can guarantee that the data is in a format that you expect, so you don't have to handle tons of edge-cases or invalid states in your own code.
|
||||
|
||||
> 🤖 Internally, Slate converts those schema definitions into plugins that enforce certain behaviors when changes are applied to the document.
|
||||
|
||||
## Custom Normalizers
|
||||
|
||||
By default, Slate will normalize any invalid states to ensure that the document is valid again. However, since Slate doesn't have that much information about your schema, its default normalization techniques might not always be what you want.
|
||||
|
@@ -81,7 +81,7 @@ Each rule must define two properties:
|
||||
|
||||
`rule.deserialize(el: Element, next: Function) => Object || Void`
|
||||
|
||||
The `deserialize` function receives a DOM element and should return a plain Javascript object representing the deserialized value, or nothing if the rule in question doesn't know how to deserialize the object, in which case the next rule in the stack will be attempted.
|
||||
The `deserialize` function receives a DOM element and should return a plain JavaScript object representing the deserialized value, or nothing if the rule in question doesn't know how to deserialize the object, in which case the next rule in the stack will be attempted.
|
||||
|
||||
The object should be one of:
|
||||
|
||||
|
@@ -81,7 +81,6 @@ The other export is a `createHyperscript` helper that you can use to create your
|
||||
inlines: Object,
|
||||
marks: Object,
|
||||
decorations: Object,
|
||||
schema: Object,
|
||||
creators: Object,
|
||||
}
|
||||
```
|
||||
|
@@ -16,7 +16,6 @@ class Toolbar extends React.Component {
|
||||
|
||||
propTypes = {
|
||||
block: Types.block,
|
||||
schema: Types.schema.isRequired,
|
||||
value: Types.value.isRequired,
|
||||
}
|
||||
|
||||
@@ -47,10 +46,6 @@ Ensure that a value is a Slate `Data`.
|
||||
|
||||
Ensure that a value is a Slate `Document`.
|
||||
|
||||
### `history`
|
||||
|
||||
Ensure that a value is a Slate `History`.
|
||||
|
||||
### `inline`
|
||||
|
||||
Ensure that a value is a Slate `Inline`.
|
||||
@@ -91,14 +86,6 @@ Ensure that a value is a Slate `Range`.
|
||||
|
||||
Ensure that a value is an immutable `List` of Slate [`Range`](../slate/range.md) objects.
|
||||
|
||||
### `schema`
|
||||
|
||||
Ensure that a value is a Slate `Schema`.
|
||||
|
||||
### `stack`
|
||||
|
||||
Ensure that a value is a Slate `Stack`.
|
||||
|
||||
### `text`
|
||||
|
||||
Ensure that a value is a Slate [`Text`](../slate/text.md).
|
||||
|
@@ -1,70 +0,0 @@
|
||||
# Core Plugin
|
||||
|
||||
Slate's editor is very unopinionated. The only logic it handles by default is logic associated with the `contenteditable` functionality itself—managing text, selections, etc. That logic is contained in two plugins, called the "core" plugins. One runs before all other plugins, and one runs after.
|
||||
|
||||
## Default Behavior
|
||||
|
||||
The default behavior of the core plugin performs the following logic:
|
||||
|
||||
### `onBeforeInput`
|
||||
|
||||
When text is entered, the core plugin inserts the text from `event.data` into the editor.
|
||||
|
||||
### `onBlur`
|
||||
|
||||
When the editor is blurred, the core plugin updates the selection in Slate's internal data model without re-rendering.
|
||||
|
||||
### `onFocus`
|
||||
|
||||
When the editor is focused, the core plugin updates the selection in Slate's internal data model without re-rendering.
|
||||
|
||||
### `onCopy`
|
||||
|
||||
When the user copies part of the document, the core plugin adds the copied text to the clipboard with a serialized version of the document intact, so that it can be deserialized and inserted on paste, preserving formatting.
|
||||
|
||||
### `onCut`
|
||||
|
||||
When the user cuts part of the document, the core plugin runs the same logic it runs for `onCopy`, but it also delete's the content in the current selection.
|
||||
|
||||
### `onDrop`
|
||||
|
||||
When the user drops content into the editor, the core plugin handles drops of type `text` and `html` as plain text, and does nothing for drops of type `files`.
|
||||
|
||||
### `onKeyDown`
|
||||
|
||||
When a key is pressed, the core plugin handles performing some of the "native" behavior that `contenteditable` elements must do. For example it splits blocks on `enter`, removes characters `backspace`, triggers an undo from the history on `cmd-z`, etc.
|
||||
|
||||
### `onPaste`
|
||||
|
||||
When the user pastes content into the editor, the core plugin handles all pastes of type `text` and `html` as plain text, and does nothing for pastes of type `files`.
|
||||
|
||||
### `onSelect`
|
||||
|
||||
When the user makes a new selection in the DOM, the core plugin updates that selection in Slate's internal data model, re-rendering if it needs to.
|
||||
|
||||
### `renderEditor`
|
||||
|
||||
Renders all of the default contents of the editor!
|
||||
|
||||
### `schema`
|
||||
|
||||
The core plugin defines a schema that enforces a few constraints on the content and defines default block and inline node renderer components—wrapping in a `<div>` and `<span>`, respectively. Each of these components contains `shouldComponentUpdate` logic that prevents unnecessary re-renders.
|
||||
|
||||
The default block component also controls its own placeholder logic, which is controlled via the [`<Editor>`](../slate-react/editor.md)'s placeholder options.
|
||||
|
||||
## Overriding Defaults
|
||||
|
||||
Any plugin you add to the editor will override the default behavior of the core plugin, because it is always resolved last.
|
||||
|
||||
However, sometimes you might want to disable the logic of the core plugin without actually adding any logic yourself. For example, you might want to prevent the `enter` key from performing any action. In those cases, you'll need to define a "noop" handler.
|
||||
|
||||
A noop `onBeforeInput` handler looks like:
|
||||
|
||||
```js
|
||||
function onBeforeInput(event, change, editor) {
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
Notice that it calls `event.preventDefault()` to prevent the default browser behavior, and it returns `false` to prevent the editor from continuing to resolve its plugins stack.
|
@@ -13,11 +13,14 @@ The top-level React component that renders the Slate editor itself.
|
||||
autoCorrect={Boolean}
|
||||
autoFocus={Boolean}
|
||||
className={String}
|
||||
commands={Object}
|
||||
onChange={Function}
|
||||
placeholder={String || Element}
|
||||
placeholder={String | Element}
|
||||
plugins={Array}
|
||||
queries={Object}
|
||||
readOnly={Boolean}
|
||||
role={String}
|
||||
schema={Object}
|
||||
spellCheck={Boolean}
|
||||
value={Value}
|
||||
style={Object}
|
||||
@@ -174,25 +177,43 @@ Programmatically invoke a change `fn` on the editor. The function will be invoke
|
||||
|
||||
If extra `...args` are passed in, the change `fn` will be invoked with `(change, ...args)`, so you can use this as a shorthand for performing single-function changes.
|
||||
|
||||
### `command`
|
||||
|
||||
`command(name, ...args) => Void`
|
||||
|
||||
Invoke a command by `name` on the editor with `args`.
|
||||
|
||||
### `event`
|
||||
|
||||
`event(handler, event, ...args) => Any`
|
||||
|
||||
Programmatically invoke an `event` on the editor. This isn't something you should normally use except in test environments.
|
||||
|
||||
### `focus`
|
||||
|
||||
`focus() => Void`
|
||||
|
||||
Programmatically focus the editor.
|
||||
|
||||
### `query`
|
||||
|
||||
`query(name, ...args) => Any`
|
||||
|
||||
Invoke a query by `name` on the editor with `args`, returning its result.
|
||||
|
||||
### `run`
|
||||
|
||||
`run(key, ...args) => Any`
|
||||
|
||||
Run the middleware stack by `key` with `args`, returning its result.
|
||||
|
||||
## Instance Properties
|
||||
|
||||
### `schema`
|
||||
### `readOnly`
|
||||
|
||||
`Schema`
|
||||
`Boolean`
|
||||
|
||||
The editor's current schema.
|
||||
|
||||
### `stack`
|
||||
|
||||
`Stack`
|
||||
|
||||
The editor's current stack.
|
||||
Whether the editor is currently read-only or not.
|
||||
|
||||
### `value`
|
||||
|
||||
|
@@ -1,49 +1,47 @@
|
||||
# Plugins
|
||||
|
||||
Plugins can be attached to an editor to alter its behavior in different ways. Plugins are just simple Javascript objects, containing a set of properties that control different behaviors—event handling, change handling, rendering, etc.
|
||||
Plugins can be attached to an editor to alter its behavior in different ways. Each editor has a "stack" of plugins, which has a specific order, which it runs through when certain hooks are triggered.
|
||||
|
||||
Each editor has a "middleware stack" of plugins, which has a specific order.
|
||||
Plugins are plain JavaScript objects, containing a set of middleware functions that run for each hook they choose to implement.
|
||||
|
||||
When the editor needs to resolve a plugin-related handler, it will loop through its plugin stack, searching for the first plugin that successfully returns a value. After receiving that value, the editor will **not** continue to search the remaining plugins; it returns early. If you'd like for the stack to continue, a plugin handler should return `undefined`.
|
||||
## Hooks
|
||||
|
||||
## Conventions
|
||||
|
||||
A plugin should always export a function that takes options. This way even if it doesn't take any options now, it won't be a breaking API change to take more options in the future. So a basic plugin might look like this:
|
||||
|
||||
```js
|
||||
export default function MySlatePlugin(options) {
|
||||
return {
|
||||
// Return properties that describe your logic here...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Handler Properties
|
||||
In addition to the [core plugin hooks](../slate/plugins.md), when using `slate-react` there are additional browser-specific event handling hooks, and React-specific rendering hooks available to plugins.
|
||||
|
||||
```js
|
||||
{
|
||||
decorateNode: Function,
|
||||
onBeforeInput: Function,
|
||||
onBlur: Function,
|
||||
onFocus: Function,
|
||||
onCopy: Function,
|
||||
onCut: Function,
|
||||
onDrop: Function,
|
||||
onFocus: Function,
|
||||
onKeyDown: Function,
|
||||
onKeyUp: Function,
|
||||
onPaste: Function,
|
||||
onSelect: Function
|
||||
onSelect: Function,
|
||||
renderEditor: Function,
|
||||
renderMark: Function,
|
||||
renderNode: Function,
|
||||
renderPlaceholder: Function,
|
||||
shouldNodeComponentUpdate: Function,
|
||||
}
|
||||
```
|
||||
|
||||
All of the event handler properties are passed the same React `event` object you are used to from React's event handlers. They are also passed a `change` object representing any changes that have resulted from the event, and the `editor` instance itself.
|
||||
The event hooks have a signature of `(event, change, next)`—the `event` is a React object that you are used to from React's event handlers.
|
||||
|
||||
Each event handler can choose to call methods on the `change` object, in which case the editor's value will be updated.
|
||||
The rendering hooks are just like render props common to other React API's, and receive `(props, editor, next)`. For more information, see the [Rendering](./rendering.md) reference.
|
||||
|
||||
If the return value of a plugin handler is `null`, the editor will simply continue resolving the plugin stack. However, if you return a non-null value, the editor will break out of the loop.
|
||||
### `decorateNode`
|
||||
|
||||
`Function decorateNode(node: Node, next: Function) => Array<Decoration>|Void`
|
||||
|
||||
The `decorateNode` hook takes a `node` and returns an array of decorations with marks to be applied to the node when it is rendered.
|
||||
|
||||
### `onBeforeInput`
|
||||
|
||||
`Function onBeforeInput(event: Event, change: Change, editor: Editor) => Change || Void`
|
||||
`Function onBeforeInput(event: Event, change: Change, next: Function) => Boolean`
|
||||
|
||||
This handler is called right before a string of text is inserted into the `contenteditable` element.
|
||||
|
||||
@@ -51,37 +49,37 @@ Make sure to `event.preventDefault()` if you do not want the default insertion b
|
||||
|
||||
### `onBlur`
|
||||
|
||||
`Function onBlur(event: Event, change: Change, editor: Editor) => Change || Void`
|
||||
`Function onBlur(event: Event, change: Change, next: Function) => Boolean`
|
||||
|
||||
This handler is called when the editor's `contenteditable` element is blurred.
|
||||
|
||||
### `onFocus`
|
||||
|
||||
`Function onFocus(event: Event, change: Change, editor: Editor) => Change || Void`
|
||||
`Function onFocus(event: Event, change: Change, next: Function) => Boolean`
|
||||
|
||||
This handler is called when the editor's `contenteditable` element is focused.
|
||||
|
||||
### `onCopy`
|
||||
|
||||
`Function onCopy(event: Event, change: Change, editor: Editor) => Change || Void`
|
||||
`Function onCopy(event: Event, change: Change, next: Function) => Boolean`
|
||||
|
||||
This handler is called when there is a copy event in the editor's `contenteditable` element.
|
||||
|
||||
### `onCut`
|
||||
|
||||
`Function onCut(event: Event, change: Change, editor: Editor) => Change || Void`
|
||||
`Function onCut(event: Event, change: Change, next: Function) => Boolean`
|
||||
|
||||
This handler is equivalent to the `onCopy` handler.
|
||||
|
||||
### `onDrop`
|
||||
|
||||
`Function onDrop(event: Event, change: Change, editor: Editor) => Change || Void`
|
||||
`Function onDrop(event: Event, change: Change, next: Function) => Boolean`
|
||||
|
||||
This handler is called when the user drops content into the `contenteditable` element. The event is already prevented by default, so you must define a value change to have any affect occur.
|
||||
|
||||
### `onKeyDown`
|
||||
|
||||
`Function onKeyDown(event: Event, change: Change, editor: Editor) => Change || Void`
|
||||
`Function onKeyDown(event: Event, change: Change, next: Function) => Boolean`
|
||||
|
||||
This handler is called when any key is pressed in the `contenteditable` element, before any action is taken.
|
||||
|
||||
@@ -89,104 +87,89 @@ Make sure to `event.preventDefault()` if you do not want the default insertion b
|
||||
|
||||
### `onKeyUp`
|
||||
|
||||
`Function onKeyUp(event: Event, change: Change, editor: Editor) => Change || Void`
|
||||
`Function onKeyUp(event: Event, change: Change, next: Function) => Boolean`
|
||||
|
||||
This handler is called when any key is released in the `contenteditable` element.
|
||||
|
||||
### `onPaste`
|
||||
|
||||
`Function onPaste(event: Event, change: Change, editor: Editor) => Change || Void`
|
||||
`Function onPaste(event: Event, change: Change, next: Function) => Boolean`
|
||||
|
||||
This handler is called when the user pastes content into the `contenteditable` element. The event is already prevented by default, so you must define a value change to have any affect occur.
|
||||
|
||||
### `onSelect`
|
||||
|
||||
`Function onSelect(event: Event, change: Change, editor: Editor) => Change || Void`
|
||||
`Function onSelect(event: Event, change: Change, next: Function) => Boolean`
|
||||
|
||||
This handler is called whenever the native DOM selection changes.
|
||||
|
||||
_Note: This is **not** Slate's internal selection representation (although it mirrors it). If you want to get notified when Slate's selection changes, use the [`onChange`](../slate-react/editor.md#onchange) property of the `<Editor>`. This handler is instead meant to give you lower-level access to the DOM selection handling, which **is not always triggered** as you'd expect._
|
||||
|
||||
## Slate-React Properties
|
||||
|
||||
```js
|
||||
{
|
||||
onChange: Function,
|
||||
shouldNodeComponentUpdate: Function
|
||||
}
|
||||
```
|
||||
|
||||
### `onChange`
|
||||
|
||||
`Function onChange(change: Change, editor: Editor) => Any || Void`
|
||||
|
||||
The `onChange` handler isn't a native browser event handler. Instead, it is invoked whenever the editor value changes. This allows plugins to augment a change however they want.
|
||||
|
||||
### `shouldNodeComponentUpdate`
|
||||
|
||||
`Function shouldNodeComponentUpdate(previousEditorProps: Object, editorProps: Object) => true || Void`
|
||||
|
||||
If this function returns `true`, it can force updating the editor where otherwise it wouldn't.
|
||||
|
||||
## Slate-React Rendering
|
||||
|
||||
```js
|
||||
{
|
||||
renderEditor: Function,
|
||||
renderMark: Function,
|
||||
renderNode: Function,
|
||||
renderPlaceholder: Function,
|
||||
}
|
||||
```
|
||||
|
||||
These renderProps are used to create the Editor UI. They are called for each plugin in reverse plugin order (so the last plugin in the array is called first) and results are passed on as `children` to the next plugin.
|
||||
> 🤖 This is **not** Slate's internal selection representation. If you want to get notified when Slate's `value.selection` changes, use the [`onChange`](../slate-react/editor.md#onchange) property of the `<Editor>`. This handler is instead meant to give you lower-level access to the DOM selection handling, which **is not always triggered** as you'd expect.
|
||||
|
||||
### `renderEditor`
|
||||
|
||||
`Function renderEditor(props: Object, editor: Editor) => ReactNode || Void`
|
||||
`Function renderEditor(props: Object, editor: Editor) => ReactNode|Void`
|
||||
|
||||
The `renderEditor` property allows you to define higher-order-component-like behavior. It is passed all of the properties of the editor, including `props.children`. You can then choose to wrap the existing `children` in any custom elements or proxy the properties however you choose. This can be useful for rendering toolbars, styling the editor, rendering validation, etc. Remember that the `renderEditor` function has to render `props.children` for editor's children to render.
|
||||
The `renderEditor` property allows you to define higher-order-component-like behavior. It is passed all of the properties of the editor, including `props.children`. You can then choose to wrap the existing `children` in any custom elements or proxy the properties however you choose. This can be useful for rendering toolbars, styling the editor, rendering validation, etc. Remember that the `renderEditor` function has to render `props.children` for editor's content to render.
|
||||
|
||||
### `renderMark`
|
||||
|
||||
`Function renderMark({ editor, mark, marks, node, offset, text, children, attributes }) => ReactNode || Void`
|
||||
`Function renderMark(props: Object, next: Function) => ReactNode|Void`
|
||||
|
||||
Render a `Mark`.
|
||||
|
||||
### `renderNode`
|
||||
|
||||
`Function renderNode({ key, editor, isFocused, isSelected, node, parent, readOnly, children, attributes }) => ReactNode || Void`
|
||||
|
||||
Render a `Node`.
|
||||
|
||||
### `renderPlaceholder`
|
||||
|
||||
`Function renderPlaceholder({ editor, mark, marks, node, offset, text, children, attributes }) => ReactNode || Void`
|
||||
|
||||
Render the placeholder that is shown when the editor has no `value`.
|
||||
|
||||
The `placeholder` prop that was passed to the editor can be found at `editor.props.placeholder`.
|
||||
|
||||
## Other Properties
|
||||
Render a `Mark` with `props`. The `props` object contains:
|
||||
|
||||
```js
|
||||
{
|
||||
decorateNode: Function,
|
||||
normalizeNode: Function,
|
||||
schema: Object
|
||||
attributes: Object,
|
||||
children: ReactNode,
|
||||
editor: Editor,
|
||||
mark: Mark,
|
||||
marks: Set<Mark>,
|
||||
node: Node,
|
||||
offset: Number,
|
||||
text: String,
|
||||
}
|
||||
```
|
||||
|
||||
### `decorateNode`
|
||||
You must spread the `props.attributes` onto the top-level DOM node you use to render the mark.
|
||||
|
||||
`Function decorateNode(node: Node) => [Range] || Void`
|
||||
### `renderNode`
|
||||
|
||||
### `normalizeNode`
|
||||
`Function renderNode(props: Object, next: Function) => ReactNode|Void`
|
||||
|
||||
`Function normalizeNode(node: Node) => Function(change: Change) || Void`
|
||||
Render a `Node` with `props`. The `props` object contains:
|
||||
|
||||
### `schema`
|
||||
```js
|
||||
{
|
||||
attributes: Object,
|
||||
children: ReactNode,
|
||||
editor: Editor,
|
||||
isFocused: Boolean,
|
||||
isSelected: BOolean,
|
||||
node: Node,
|
||||
parent: Node,
|
||||
readOnly: Boolean,
|
||||
}
|
||||
```
|
||||
|
||||
`Object`
|
||||
You must spread the `props.attributes` onto the top-level DOM node you use to render the node.
|
||||
|
||||
The `schema` property allows you to define a set of rules that will be added to the editor's schema. The rules from each of the schemas returned by the plugins are collected into a single schema for the editor.
|
||||
### `renderPlaceholder`
|
||||
|
||||
`Function renderPlaceholder(props: Object, next: Function) => ReactNode|Void`
|
||||
|
||||
Render the placeholder that is shown when the editor has no `value`. The `props` object contains:
|
||||
|
||||
```js
|
||||
{
|
||||
editor: Editor,
|
||||
readOnly: Boolean,
|
||||
}
|
||||
```
|
||||
|
||||
The `placeholder` prop that was passed to the editor can be found at `editor.props.placeholder`.
|
||||
|
||||
### `shouldNodeComponentUpdate`
|
||||
|
||||
`Function shouldNodeComponentUpdate(previousProps: Object, props: Object, next: Function) => Boolean|Void`
|
||||
|
||||
If this function returns `true`, it can force updating the node's component where otherwise it wouldn't for performance.
|
||||
|
@@ -1,20 +1,20 @@
|
||||
# Custom Nodes
|
||||
# Rendering
|
||||
|
||||
Slate will render custom nodes for [`Block`](../slate/block.md) and [`Inline`](../slate/inline.md) models, based on what you pass in as your schema. This allows you to completely customize the rendering behavior of your Slate editor.
|
||||
|
||||
## Props
|
||||
|
||||
```js
|
||||
<{Custom}
|
||||
attributes={Object}
|
||||
children={Object}
|
||||
editor={Editor}
|
||||
isSelected={Boolean}
|
||||
isFocused={Boolean}
|
||||
node={Node}
|
||||
parent={Node}
|
||||
readOnly={Boolean}
|
||||
/>
|
||||
{
|
||||
attributes: Object,
|
||||
children: Object,
|
||||
editor: Editor,
|
||||
isSelected: Boolean,
|
||||
isFocused: Boolean,
|
||||
node: Node,
|
||||
parent: Node,
|
||||
readOnly: Boolean,
|
||||
}
|
||||
```
|
||||
|
||||
### `attributes`
|
@@ -19,17 +19,17 @@ React-specific utility functions for Slate that may be useful in certain use cas
|
||||
|
||||
### `cloneFragment`
|
||||
|
||||
`cloneFragment(event: DOMEvent|ReactEvent, value: Value, fragment: Document)`
|
||||
`cloneFragment(event: DOMEvent|ReactEvent, editor: Editor, fragment: Document)`
|
||||
|
||||
During a cut or copy event, sets `fragment` as the Slate document fragment to be copied.
|
||||
|
||||
```js
|
||||
function onCopy(event, change, editor) {
|
||||
const { value } = change
|
||||
function onCopy(event, change, next) {
|
||||
const { editor } = change
|
||||
const fragment = // ... create a fragment from a set of nodes ...
|
||||
|
||||
if (fragment) {
|
||||
cloneFragment(event, value, fragment)
|
||||
cloneFragment(event, editor, fragment)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -38,12 +38,12 @@ function onCopy(event, change, editor) {
|
||||
Note that calling `cloneFragment` should be the last thing you do in your event handler. If you change the window selection after calling `cloneFragment`, the browser may copy the wrong content. If you need to perform an action after calling `cloneFragment`, wrap it in `requestAnimationFrame`:
|
||||
|
||||
```js
|
||||
function onCut(event, change, editor) {
|
||||
const { value } = change
|
||||
function onCut(event, change, next) {
|
||||
const { editor } = change
|
||||
const fragment = // ... create a fragment from a set of nodes ...
|
||||
|
||||
if (fragment) {
|
||||
cloneFragment(event, value, fragment)
|
||||
cloneFragment(event, editor, fragment)
|
||||
window.requestAnimationFrame(() => {
|
||||
editor.change(change => change.delete())
|
||||
})
|
||||
@@ -82,45 +82,45 @@ function onChange(change) {
|
||||
|
||||
### `findNode`
|
||||
|
||||
`findNode(element: DOMElement, value: Value) => Node`
|
||||
`findNode(element: DOMElement, editor: Editor) => Node`
|
||||
|
||||
Find the Slate node from a DOM `element` and Slate `value`.
|
||||
Find the Slate node from a DOM `element` and Slate `editor`.
|
||||
|
||||
```js
|
||||
function onSomeNativeEvent(event) {
|
||||
const node = findNode(event.target)
|
||||
const node = findNode(event.target, editor)
|
||||
// Do something with `node`...
|
||||
}
|
||||
```
|
||||
|
||||
### `findRange`
|
||||
|
||||
`findRange(selection: DOMSelection, value: Value) => Range`
|
||||
`findRange(range: DOMRange, value: Value) => Range`
|
||||
`findRange(selection: DOMSelection, editor: Editor) => Range`
|
||||
`findRange(range: DOMRange, editor: Editor) => Range`
|
||||
|
||||
Find the Slate range from a DOM `range` or `selection` and a Slate `value`.
|
||||
Find the Slate range from a DOM `range` or `selection` and a Slate `editor`.
|
||||
|
||||
```js
|
||||
function onSomeNativeEvent() {
|
||||
// You can find a range from a native DOM selection...
|
||||
const nativeSelection = window.getSelection()
|
||||
const range = findRange(nativeSelection, value)
|
||||
const range = findRange(nativeSelection, editor)
|
||||
|
||||
// ...or from a native DOM range...
|
||||
const nativeRange = nativeSelection.getRangeAt(0)
|
||||
const range = findRange(nativeRange, value)
|
||||
const range = findRange(nativeRange, editor)
|
||||
}
|
||||
```
|
||||
|
||||
### `getEventRange`
|
||||
|
||||
`getEventRange(event: DOMEvent|ReactEvent, value: Value) => Range`
|
||||
`getEventRange(event: DOMEvent|ReactEvent, editor: Editor) => Range`
|
||||
|
||||
Get the affected Slate range from a DOM `event` and Slate `value`.
|
||||
Get the affected Slate range from a DOM `event` and Slate `editor`.
|
||||
|
||||
```js
|
||||
function onDrop(event, change, editor) {
|
||||
const targetRange = getEventRange(event)
|
||||
function onDrop(event, change, next) {
|
||||
const targetRange = getEventRange(event, editor)
|
||||
// Do something at the drop `targetRange`...
|
||||
}
|
||||
```
|
||||
@@ -132,7 +132,7 @@ function onDrop(event, change, editor) {
|
||||
Get the Slate-related data from a DOM `event` and Slate `value`.
|
||||
|
||||
```js
|
||||
function onDrop(event, change, editor) {
|
||||
function onDrop(event, change, next) {
|
||||
const transfer = getEventTransfer(event)
|
||||
const { type, node } = transfer
|
||||
|
||||
@@ -149,7 +149,7 @@ function onDrop(event, change, editor) {
|
||||
Sets the Slate-related `data` with `type` on an `event`. The `type` must be one of the types Slate recognizes: `'fragment'`, `'html'`, `'node'`, `'rich'`, or `'text'`.
|
||||
|
||||
```js
|
||||
function onDragStart(event, change, editor) {
|
||||
function onDragStart(event, change, next) {
|
||||
const { value } = change
|
||||
const { startNode } = value
|
||||
setEventTransfer(event, 'node', startNode)
|
||||
|
@@ -1,142 +0,0 @@
|
||||
# `slate-simulator`
|
||||
|
||||
```js
|
||||
import Simulator from 'slate-simulator'
|
||||
```
|
||||
|
||||
A simulator to help writing tests for Slate editors and plugins. By default the simulator does not include the [core plugins](https://docs.slatejs.org/guides/plugins#core-plugins) as they have a lot of dependencies on browser-specific globals, so running them in CI environments is very hard. If you need the core plugins for your use case you need to import them manually.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Simulator from 'slate-simulator'
|
||||
|
||||
const value = ...
|
||||
const plugins = [ ... ]
|
||||
const simulator = new Simulator({ value, plugins })
|
||||
|
||||
simulator
|
||||
.focus()
|
||||
.beforeInput({ data: 'H' })
|
||||
.beforeInput({ data: 'e' })
|
||||
.beforeInput({ data: 'l' })
|
||||
.beforeInput({ data: 'l' })
|
||||
.beforeInput({ data: 'o' })
|
||||
.beforeInput({ data: '!' })
|
||||
.keyDown({ key: 'Enter' })
|
||||
|
||||
const newValue = simulator.value
|
||||
```
|
||||
|
||||
## Example with core plugins
|
||||
|
||||
```js
|
||||
import Simulator from 'slate-simulator'
|
||||
import { BeforePlugin, AfterPlugin } from 'slate-react'
|
||||
|
||||
const value = ...
|
||||
const plugins = [ BeforePlugin(), ... , AfterPlugin() ]
|
||||
const simulator = new Simulator({ value, plugins })
|
||||
```
|
||||
|
||||
Core plugins will trigger default behaviour for the events. Without them, only the changes defined in the plugins passed to slate-simulator will be applied. For example, `beforeInput()` event will not insert the data's text if none of the plugins being tested does it.
|
||||
|
||||
## Example for `DataTransfer` events
|
||||
|
||||
In order to simulate paste and drop events you will need to create a [DataTransfer](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer). However this object is browser dependant and is not easy to manipulate in CI environments. The easiest work around is to fake its API with a mockup class:
|
||||
|
||||
```js
|
||||
class FakeDataTransfer {
|
||||
constructor(props) {
|
||||
this.items = []
|
||||
this.types = []
|
||||
}
|
||||
|
||||
getData(key) {
|
||||
return this.items.find(item => item.key === key).value
|
||||
}
|
||||
|
||||
setData(key, value) {
|
||||
this.types.push(key)
|
||||
this.items.push({ key, value })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Later, you can use it this way to fake a paste event:
|
||||
|
||||
```js
|
||||
import { setEventTransfer } from 'slate-react'
|
||||
|
||||
const pastedText = 'slatejs.org'
|
||||
|
||||
const fakeDataTransfer = new FakeDataTransfer()
|
||||
fakeDataTransfer.setData('text', "this text doesn't matter")
|
||||
|
||||
const pasteEvent = { dataTransfer: fakeDataTransfer }
|
||||
setEventTransfer(pasteEvent, 'text', pastedText)
|
||||
|
||||
simulator.paste(pasteEvent)
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
### `beforeInput`
|
||||
|
||||
`beforeInput(event: Object) => Simulator`
|
||||
|
||||
Simulator a `beforeinput` event with an `event` object.
|
||||
|
||||
### `blur`
|
||||
|
||||
`blur(event: Object) => Simulator`
|
||||
|
||||
Simulator a `blur` event with an `event` object.
|
||||
|
||||
### `copy`
|
||||
|
||||
`copy(event: Object) => Simulator`
|
||||
|
||||
Simulator a `copy` event with an `event` object.
|
||||
|
||||
### `cut`
|
||||
|
||||
`cut(event: Object) => Simulator`
|
||||
|
||||
Simulator a `cut` event with an `event` object.
|
||||
|
||||
### `drop`
|
||||
|
||||
`drop(event: Object) => Simulator`
|
||||
|
||||
Simulator a `drop` event with an `event` object.
|
||||
|
||||
### `focus`
|
||||
|
||||
`focus(event: Object) => Simulator`
|
||||
|
||||
Simulator a `focus` event with an `event` object.
|
||||
|
||||
### `keyDown`
|
||||
|
||||
`keyDown(event: Object) => Simulator`
|
||||
|
||||
Simulator a `keyDown` event with an `event` object.
|
||||
|
||||
### `keyUp`
|
||||
|
||||
`keyUp(event: Object) => Simulator`
|
||||
|
||||
Simulator a `keyUp` event with an `event` object.
|
||||
|
||||
### `paste`
|
||||
|
||||
`paste(event: Object) => Simulator`
|
||||
|
||||
Simulator a `paste` event with an `event` object.
|
||||
|
||||
### `select`
|
||||
|
||||
`select(event: Object) => Simulator`
|
||||
|
||||
Simulator a `select` event with an `event` object.
|
@@ -63,13 +63,13 @@ A concatenated string of all of the descendant [`Text`](./text.md) nodes of this
|
||||
|
||||
`Block.create(properties: Object) => Block`
|
||||
|
||||
Create a block from a plain Javascript object of `properties`.
|
||||
Create a block from a plain JavaScript object of `properties`.
|
||||
|
||||
### `Block.createList`
|
||||
|
||||
`Block.createList(array: Array) => List`
|
||||
|
||||
Create a list of block nodes from a plain Javascript `array`.
|
||||
Create a list of block nodes from a plain JavaScript `array`.
|
||||
|
||||
### `Block.fromJSON`
|
||||
|
||||
|
@@ -1,15 +1,17 @@
|
||||
# `Change`
|
||||
|
||||
```js
|
||||
import { Change } from 'slate'
|
||||
```
|
||||
|
||||
A change allows you to define a series of changes you'd like to make to the current [`Value`](./value.md).
|
||||
|
||||
All changes are performed through `Change` objects, so that a history of changes can be preserved for use in undo/redo operations, and to make collaborative editing possible.
|
||||
|
||||
## Properties
|
||||
|
||||
### `editor`
|
||||
|
||||
`Editor`
|
||||
|
||||
A reference to the editor the change is being made in.
|
||||
|
||||
### `object`
|
||||
|
||||
`String`
|
||||
@@ -18,6 +20,8 @@ A string with a value of `'change'`.
|
||||
|
||||
### `value`
|
||||
|
||||
`Value`
|
||||
|
||||
A [`Value`](./value.md) with the change's current operations applied. Each time you run a new change function this property will be updated.
|
||||
|
||||
## Methods
|
||||
@@ -366,7 +370,7 @@ Move the current selection's anchor point to the start of the document and its f
|
||||
|
||||
`select(properties: Range || Object) => Change`
|
||||
|
||||
Set the current selection to a range with merged `properties`. The `properties` can either be a [`Range`](./range.md) object or a plain Javascript object of selection properties.
|
||||
Set the current selection to a range with merged `properties`. The `properties` can either be a [`Range`](./range.md) object or a plain JavaScript object of selection properties.
|
||||
|
||||
## Document Range Changes
|
||||
|
||||
|
@@ -14,7 +14,7 @@ A data object can have any properties associated with it.
|
||||
|
||||
`Data.create(properties: Object) => Data`
|
||||
|
||||
Create a data object from a plain Javascript object of `properties`.
|
||||
Create a data object from a plain JavaScript object of `properties`.
|
||||
|
||||
### `Data.fromJSON`
|
||||
|
||||
|
@@ -50,7 +50,7 @@ A concatenated string of all of the descendant [`Text`](./text.md) nodes of this
|
||||
|
||||
`Document.create(properties: Object) => Document`
|
||||
|
||||
Create a document from a plain Javascript object of `properties`.
|
||||
Create a document from a plain JavaScript object of `properties`.
|
||||
|
||||
### `Document.fromJSON`
|
||||
|
||||
|
113
docs/reference/slate/editor.md
Normal file
113
docs/reference/slate/editor.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# `<Editor>`
|
||||
|
||||
```js
|
||||
import { Editor } from 'slate'
|
||||
```
|
||||
|
||||
The top-level controller that holds a [`Value`](./value.md) over time, and contains all of the plugins that determine the editor's behavior.
|
||||
|
||||
> 🤖 In `slate-react`, the [`<Editor>`](../slate-react/editor.md) component creates an instance of the `Editor` controller which manages its value under the covers.
|
||||
|
||||
## Properties
|
||||
|
||||
```js
|
||||
new Editor({
|
||||
onChange: Function,
|
||||
plugins: Array,
|
||||
readOnly: Boolean,
|
||||
value: Value,
|
||||
})
|
||||
```
|
||||
|
||||
### `onChange`
|
||||
|
||||
`Function onChange(change: Change)`
|
||||
|
||||
A change handler that will be called with the `change` that applied the change. When the `onChange` handler is called, the `editor.value` will already reflect the new state.
|
||||
|
||||
### `plugins`
|
||||
|
||||
`Array`
|
||||
|
||||
An array of [`Plugins`](./plugins.md) that define the editor's behavior. These plugins are only definable when an instance of `Editor` is constructed, and are constant throught the editor's lifecycle.
|
||||
|
||||
> 🤖 In `slate-react`, when the `plugins` prop to the [`<Editor>`](../slate-react/editor.md) component changes, an entirely new `Editor` controller is created under the covers. This is why plugins should not be defined inline in the render function.
|
||||
|
||||
### `readOnly`
|
||||
|
||||
`Boolean`
|
||||
|
||||
Whether the editor is in "read-only" mode, where the user is prevented from editing the editor's content.
|
||||
|
||||
### `value`
|
||||
|
||||
`Value`
|
||||
|
||||
A [`Value`](../slate/value.md) object representing the current value of the editor.
|
||||
|
||||
## Methods
|
||||
|
||||
### `change`
|
||||
|
||||
`change(fn) => Void`
|
||||
`change(fn, ...args) => Void`
|
||||
|
||||
Programmatically invoke a change `fn` on the editor. The function will be invoked with a new `change` object representing the editor's current value.
|
||||
|
||||
If extra `...args` are passed in, the change `fn` will be invoked with `(change, ...args)`, so you can use this as a shorthand for performing single-function changes.
|
||||
|
||||
### `command`
|
||||
|
||||
`command(name, ...args) => Void`
|
||||
|
||||
Invoke a command by `name` on the editor with `args`.
|
||||
|
||||
### `event`
|
||||
|
||||
`event(handler, event, ...args) => Any`
|
||||
|
||||
Programmatically invoke an `event` on the editor. This isn't something you should normally use except in test environments.
|
||||
|
||||
### `focus`
|
||||
|
||||
`focus() => Void`
|
||||
|
||||
Programmatically focus the editor.
|
||||
|
||||
### `query`
|
||||
|
||||
`query(name, ...args) => Any`
|
||||
|
||||
Invoke a query by `name` on the editor with `args`, returning its result.
|
||||
|
||||
### `registerCommand`
|
||||
|
||||
`registerCommand(command: String) => Void`
|
||||
|
||||
Register a new `command` by name with the editor. This will make the command available as a method on the editor's `Change` objects.
|
||||
|
||||
### `registerQuery`
|
||||
|
||||
`registerQuery(query: String) => Void`
|
||||
|
||||
Register a new `query` by name with the editor. This will make the query available as a method on the editor's `Change` objects.
|
||||
|
||||
### `run`
|
||||
|
||||
`run(key, ...args) => Any`
|
||||
|
||||
Run the middleware stack by `key` with `args`, returning its result.
|
||||
|
||||
### `setReadOnly`
|
||||
|
||||
`setReadOnly(readOnly: Boolean) => Editor`
|
||||
|
||||
Set the editor's `readOnly` state.
|
||||
|
||||
### `setValue`
|
||||
|
||||
`setValue(value: Value, options: Object) => Editor`
|
||||
|
||||
Set the editor's `value` state.
|
||||
|
||||
You can optionally provide a `normalize` option to either for the editor to completely re-normalize the new value based on its schema or not. By default, the editor will re-normalize only if the value is not equal to its previously seen value (which it knows was normalized).
|
@@ -63,13 +63,13 @@ A concatenated string of all of the descendant [`Text`](./text.md) nodes of this
|
||||
|
||||
`Inline.create(properties: Object) => Inline`
|
||||
|
||||
Create an inline from a plain Javascript object of `properties`.
|
||||
Create an inline from a plain JavaScript object of `properties`.
|
||||
|
||||
### `Inline.createList`
|
||||
|
||||
`Inline.createList(array: Array) => List`
|
||||
|
||||
Create a list of inline nodes from a plain Javascript `array`.
|
||||
Create a list of inline nodes from a plain JavaScript `array`.
|
||||
|
||||
### `Inline.fromJSON`
|
||||
|
||||
|
@@ -39,13 +39,13 @@ The custom type of the mark (eg. `bold` or `italic`).
|
||||
|
||||
`Mark.create(properties: Object) => Mark`
|
||||
|
||||
Create a mark from a plain Javascript object of `properties`.
|
||||
Create a mark from a plain JavaScript object of `properties`.
|
||||
|
||||
### `Mark.createSet`
|
||||
|
||||
`Mark.createSet(array: Array) => Set`
|
||||
|
||||
Create a set of marks from a plain Javascript `array`.
|
||||
Create a set of marks from a plain JavaScript `array`.
|
||||
|
||||
### `Mark.fromJSON`
|
||||
|
||||
|
203
docs/reference/slate/plugins.md
Normal file
203
docs/reference/slate/plugins.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Plugins
|
||||
|
||||
Plugins can be attached to an editor to alter its behavior in different ways. Each editor has a "stack" of plugins, which has a specific order, which it runs through when certain hooks are triggered.
|
||||
|
||||
> 🤖 The core `slate` editor is designed for use across all environments, and defines a limited set of plugin hooks. But when using `slate-react` there are more hooks defined, for managing in rendering, DOM events, etc. Check out the [React Plugins](../slate-react/plugins.md) reference for more information.
|
||||
|
||||
## Hooks
|
||||
|
||||
Plugins are plain JavaScript objects, containing a set of middleware functions that run for each hook they choose to implement.
|
||||
|
||||
```js
|
||||
{
|
||||
normalizeNode: Function,
|
||||
onChange: Function,
|
||||
onCommand: Function,
|
||||
onConstruct: Function,
|
||||
onQuery: Function,
|
||||
validateNode: Function,
|
||||
}
|
||||
```
|
||||
|
||||
When a hook is triggered, the middleware function is passed a set of arguments, with the last argument being a `next` function. Choosing whether to call `next` or not determines whether the editor will continue traversing the stack.
|
||||
|
||||
### `normalizeNode`
|
||||
|
||||
`Function normalizeNode(node: Node, next: Function) => Function(change: Change)|Void`
|
||||
|
||||
The `normalizeNode` hook takes a `node` and either returns `undefined` if the node is valid, or a change function that normalizes the node into a valid state if not.
|
||||
|
||||
### `onChange`
|
||||
|
||||
`onChange(change: Change, next: Function) => Void`
|
||||
|
||||
```js
|
||||
{
|
||||
onChange(change, next) {
|
||||
...
|
||||
return next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `onChange` hook is called whenever a new change is about to be applied to an editor. This is useful if you'd like to apply some behavior to every change, or even abort certain changes.
|
||||
|
||||
### `onCommand`
|
||||
|
||||
`onCommand(command: Object, change: Change, ) => Void`
|
||||
|
||||
```js
|
||||
{
|
||||
onCommand(command, change, next) {
|
||||
const { type, args } = command
|
||||
|
||||
if (type === 'wrapQuote') {
|
||||
change.wrapBlock('quote')
|
||||
} else {
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
{
|
||||
type: String,
|
||||
args: Array,
|
||||
}
|
||||
```
|
||||
|
||||
The `onQuery` hook is called with a `query` object resulting from an `editor.query(type, ...args)` or a `change[query](...args)` call:
|
||||
|
||||
The `onQuery` hook is a low-level way to have access to all of the queries passing through the editor. Most of the time you should use the `queries` shorthand instead.
|
||||
|
||||
### `onConstruct`
|
||||
|
||||
`onConstruct(editor: Editor, next: Function) => Void`
|
||||
|
||||
```js
|
||||
{
|
||||
onConstruct(editor, next) {
|
||||
editor.registerCommand('wrapList')
|
||||
return next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `onConstruct` hook is called when a new instance of `Editor` is created. This is where you can call `editor.registerCommand` or `editor.registerQuery`.
|
||||
|
||||
> 🤖 This is always called with the low-level `Editor` instance, and not the React `<Editor>` component. And it is called before the React editor has its `value` set based on its props. It is purely used for editor-related configuration setup, and not for any schema-related or value-related purposes.
|
||||
|
||||
### `onQuery`
|
||||
|
||||
`onQuery(query: Object, next: Function) => Void`
|
||||
|
||||
```js
|
||||
{
|
||||
onQuery(query, next) {
|
||||
const { type, args } = query
|
||||
|
||||
if (type === 'getActiveList') {
|
||||
return ...
|
||||
} else {
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
{
|
||||
type: String,
|
||||
args: Array,
|
||||
}
|
||||
```
|
||||
|
||||
The `onQuery` hook is called with a `query` object resulting from an `editor.query(type, ...args)` or a `change[query](...args)` call:
|
||||
|
||||
The `onQuery` hook is a low-level way to have access to all of the queries passing through the editor. Most of the time you should use the `queries` shorthand instead.
|
||||
|
||||
### `validateNode`
|
||||
|
||||
`Function validateNode(node: Node, next: Function) => SlateError|Void`
|
||||
|
||||
The `validateNode` hook takes a `node` and either returns `undefined` if the node is valid, or a `SlateError` object if it is invalid.
|
||||
|
||||
## Shorthands
|
||||
|
||||
In addition to the middleware functions, Slate also provides three shorthands which implement common behaviors in `commands`, `queries` and `schema`.
|
||||
|
||||
```js
|
||||
{
|
||||
commands: Object,
|
||||
queries: Object,
|
||||
schema: Object,
|
||||
}
|
||||
```
|
||||
|
||||
### `commands`
|
||||
|
||||
`commands: Object`
|
||||
|
||||
```js
|
||||
{
|
||||
commands: {
|
||||
setHeader(change, level) {
|
||||
change.setBlocks({ type: 'header', data: { level }})
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `commands` shorthand defines a set of custom commands that are made available in the editor, and as first-class methods on the `change` objects created by the editor.
|
||||
|
||||
Each command has a signature of `(change, ...args)`.
|
||||
|
||||
### `queries`
|
||||
|
||||
`queries: Object`
|
||||
|
||||
```js
|
||||
{
|
||||
queries: {
|
||||
getActiveList(value) {
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `queries` shorthand defines a set of custom queries that are made available in the editor, and as first-class methods on the `change` objects created by the editor.
|
||||
|
||||
Each query has a signature of `(...args)`.
|
||||
|
||||
### `schema`
|
||||
|
||||
`schema: Object`
|
||||
|
||||
```js
|
||||
{
|
||||
schema: {
|
||||
blocks: {
|
||||
image: {
|
||||
isVoid: true,
|
||||
parent: { type: 'figure' },
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
{
|
||||
document: Object,
|
||||
blocks: Object,
|
||||
inlines: Object,
|
||||
rules: Array,
|
||||
}
|
||||
```
|
||||
|
||||
The `schema` shorthand defines your custom requires for the data in your editor. It allows you to enforce rules about what "valid" content is in the editor, and how nodes behave.
|
||||
|
||||
Check out the [Schema](./schema.md) reference for more information.
|
@@ -1,4 +1,4 @@
|
||||
# `Schema`
|
||||
# Schema
|
||||
|
||||
Every Slate editor has a "schema" associated with it, which contains information about the structure of its content. For the most basic cases, you'll just rely on Slate's default core schema. But for advanced use cases, you can enforce rules about what the content of a Slate document can contain.
|
||||
|
||||
@@ -9,6 +9,7 @@ Every Slate editor has a "schema" associated with it, which contains information
|
||||
document: Object,
|
||||
blocks: Object,
|
||||
inlines: Object,
|
||||
rules: Array,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -285,34 +286,6 @@ Will validate the previous sibling node against a [`match`](#match).
|
||||
|
||||
Will validate a node's text with a regex or function.
|
||||
|
||||
## Static Methods
|
||||
|
||||
### `Schema.create`
|
||||
|
||||
`Schema.create(properties: Object) => Schema`
|
||||
|
||||
Create a new `Schema` instance with `properties`.
|
||||
|
||||
### `Schema.fromJSON`
|
||||
|
||||
`Schema.fromJSON(object: Object) => Schema`
|
||||
|
||||
Create a schema from a JSON `object`.
|
||||
|
||||
### `Schema.isSchema`
|
||||
|
||||
`Schema.isSchema(maybeSchema: Any) => Boolean`
|
||||
|
||||
Returns a boolean if the passed in argument is a `Schema`.
|
||||
|
||||
## Instance Methods
|
||||
|
||||
### `toJSON`
|
||||
|
||||
`toJSON() => Object`
|
||||
|
||||
Returns a JSON representation of the schema.
|
||||
|
||||
## Errors
|
||||
|
||||
When supplying your own `normalize` property for a schema rule, it will be called with `(change, error)`. The error `code` will be one of a set of potential code strings, and it will contain additional helpful properties depending on the type of error.
|
||||
|
@@ -40,7 +40,7 @@ A concatenated string of all of the characters in the text node.
|
||||
|
||||
`Text.create(properties: Object) => Text`
|
||||
|
||||
Create a text from a plain Javascript object of `properties`.
|
||||
Create a text from a plain JavaScript object of `properties`.
|
||||
|
||||
### `Text.fromJSON`
|
||||
|
||||
|
@@ -6,18 +6,12 @@ import { Value } from 'slate'
|
||||
|
||||
A `Value` is the top-level representation of data in Slate, containing both a [`Document`](./document.md) and a [`Selection`](./selection.md). It's what you need to pass into the Slate [`<Editor>`](../slate-react/editor.md) to render something onto the page.
|
||||
|
||||
All changes to the document and selection are also performed through the value object, so that they can stay in sync, and be propagated to its internal history of undo/redo value.
|
||||
|
||||
For convenience, in addition to changes, many of the selection and document properties are exposed as proxies on the `Value` object.
|
||||
|
||||
## Properties
|
||||
|
||||
```js
|
||||
Value({
|
||||
document: Document,
|
||||
selection: Selection,
|
||||
history: History,
|
||||
schema: Schema,
|
||||
data: Data,
|
||||
decorations: List<Decoration>,
|
||||
})
|
||||
@@ -41,24 +35,12 @@ A list of ranges in the document with marks that aren't part of the content itse
|
||||
|
||||
The current document of the value.
|
||||
|
||||
### `history`
|
||||
|
||||
`History`
|
||||
|
||||
An object that stores the history of changes.
|
||||
|
||||
### `object`
|
||||
|
||||
`String`
|
||||
|
||||
A string with a value of `'value'`.
|
||||
|
||||
### `schema`
|
||||
|
||||
`Schema`
|
||||
|
||||
An object representing the schema of the value's document.
|
||||
|
||||
### `selection`
|
||||
|
||||
`Selection`
|
||||
@@ -139,12 +121,6 @@ Returns a boolean if the passed in argument is a `Value`.
|
||||
|
||||
## Instance Methods
|
||||
|
||||
### `change`
|
||||
|
||||
`change() => Change`
|
||||
|
||||
Create a new [`Change`](./change.md) that acts on the current value.
|
||||
|
||||
### `toJSON`
|
||||
|
||||
`toJSON() => Object`
|
||||
|
@@ -41,8 +41,9 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
// Define a new handler which prints the key that was pressed.
|
||||
onKeyDown = (event, change) => {
|
||||
onKeyDown = (event, change, next) => {
|
||||
console.log(event.key)
|
||||
return next()
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -73,9 +74,9 @@ class App extends React.Component {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
onKeyDown = (event, change) => {
|
||||
onKeyDown = (event, change, next) => {
|
||||
// Return with no changes if the keypress is not '&'
|
||||
if (event.key !== '&') return
|
||||
if (event.key !== '&') return next()
|
||||
|
||||
// Prevent the ampersand character from being inserted.
|
||||
event.preventDefault()
|
||||
|
@@ -20,8 +20,8 @@ class App extends React.Component {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
onKeyDown = (event, change) => {
|
||||
if (event.key != '`' || !event.ctrlKey) return
|
||||
onKeyDown = (event, change, next) => {
|
||||
if (event.key != '`' || !event.ctrlKey) return next()
|
||||
event.preventDefault()
|
||||
const isCode = change.value.blocks.some(block => block.type == 'code')
|
||||
|
||||
@@ -40,10 +40,12 @@ class App extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderNode = props => {
|
||||
renderNode = (props, next) => {
|
||||
switch (props.node.type) {
|
||||
case 'code':
|
||||
return <CodeNode {...props} />
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,8 +63,8 @@ class App extends React.Component {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
onKeyDown = (event, change) => {
|
||||
if (!event.ctrlKey) return
|
||||
onKeyDown = (event, change, next) => {
|
||||
if (!event.ctrlKey) return next()
|
||||
|
||||
// Decide what to do based on the key code...
|
||||
switch (event.key) {
|
||||
@@ -79,6 +81,10 @@ class App extends React.Component {
|
||||
change.setBlocks(isCode ? 'paragraph' : 'code')
|
||||
return true
|
||||
}
|
||||
// Otherwise, let other plugins handle it.
|
||||
default: {
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,10 +99,12 @@ class App extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderNode = props => {
|
||||
renderNode = (props, next) => {
|
||||
switch (props.node.type) {
|
||||
case 'code':
|
||||
return <CodeNode {...props} />
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,8 +139,8 @@ class App extends React.Component {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
onKeyDown = (event, change) => {
|
||||
if (!event.ctrlKey) return
|
||||
onKeyDown = (event, change, next) => {
|
||||
if (!event.ctrlKey) return next()
|
||||
|
||||
switch (event.key) {
|
||||
case 'b': {
|
||||
@@ -146,6 +154,9 @@ class App extends React.Component {
|
||||
change.setBlocks(isCode ? 'paragraph' : 'code')
|
||||
return true
|
||||
}
|
||||
default: {
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,18 +173,22 @@ class App extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderNode = props => {
|
||||
renderNode = (props, next) => {
|
||||
switch (props.node.type) {
|
||||
case 'code':
|
||||
return <CodeNode {...props} />
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
// Add a `renderMark` method to render marks.
|
||||
renderMark = props => {
|
||||
renderMark = (props, next) => {
|
||||
switch (props.mark.type) {
|
||||
case 'bold':
|
||||
return <BoldMark {...props} />
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,8 +20,8 @@ class App extends React.Component {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
onKeyDown = (event, change) => {
|
||||
if (event.key != '&') return
|
||||
onKeyDown = (event, change, next) => {
|
||||
if (event.key != '&') return next()
|
||||
event.preventDefault()
|
||||
change.insertText('and')
|
||||
return true
|
||||
@@ -82,8 +82,8 @@ class App extends React.Component {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
onKeyDown = (event, change) => {
|
||||
if (event.key != '&') return
|
||||
onKeyDown = (event, change, next) => {
|
||||
if (event.key != '&') return next()
|
||||
event.preventDefault()
|
||||
change.insertText('and')
|
||||
return true
|
||||
@@ -102,10 +102,12 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
// Add a `renderNode` method to render a `CodeNode` for code blocks.
|
||||
renderNode = props => {
|
||||
renderNode = (props, next) => {
|
||||
switch (props.node.type) {
|
||||
case 'code':
|
||||
return <CodeNode {...props} />
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,9 +133,9 @@ class App extends React.Component {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
onKeyDown = (event, change) => {
|
||||
onKeyDown = (event, change, next) => {
|
||||
// Return with no changes if it's not the "`" key with ctrl pressed.
|
||||
if (event.key != '`' || !event.ctrlKey) return
|
||||
if (event.key != '`' || !event.ctrlKey) return next()
|
||||
|
||||
// Prevent the "`" from being inserted by default.
|
||||
event.preventDefault()
|
||||
@@ -154,10 +156,12 @@ class App extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderNode = props => {
|
||||
renderNode = (props, next) => {
|
||||
switch (props.node.type) {
|
||||
case 'code':
|
||||
return <CodeNode {...props} />
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,8 +191,8 @@ class App extends React.Component {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
onKeyDown = (event, change) => {
|
||||
if (event.key != '`' || !event.ctrlKey) return
|
||||
onKeyDown = (event, change, next) => {
|
||||
if (event.key != '`' || !event.ctrlKey) return next()
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
@@ -211,10 +215,12 @@ class App extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderNode = props => {
|
||||
renderNode = (props, next) => {
|
||||
switch (props.node.type) {
|
||||
case 'code':
|
||||
return <CodeNode {...props} />
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -263,7 +263,7 @@ class App extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderNode = props => {
|
||||
renderNode = (props, next) => {
|
||||
switch (props.node.type) {
|
||||
case 'code':
|
||||
return (
|
||||
@@ -279,11 +279,13 @@ class App extends React.Component {
|
||||
)
|
||||
case 'quote':
|
||||
return <blockquote {...props.attributes}>{props.children}</blockquote>
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
// Add a `renderMark` method to render marks.
|
||||
renderMark = props => {
|
||||
renderMark = (props, next) => {
|
||||
const { mark, attributes } = props
|
||||
switch (mark.type) {
|
||||
case 'bold':
|
||||
@@ -292,6 +294,8 @@ class App extends React.Component {
|
||||
return <em {...attributes}>{props.children}</em>
|
||||
case 'underline':
|
||||
return <u {...attributes}>{props.children}</u>
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -22,8 +22,8 @@ class App extends React.Component {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
onKeyDown = (event, change) => {
|
||||
if (event.key != 'b' || !event.ctrlKey) return
|
||||
onKeyDown = (event, change, next) => {
|
||||
if (event.key != 'b' || !event.ctrlKey) return next()
|
||||
event.preventDefault()
|
||||
change.toggleMark('bold')
|
||||
return true
|
||||
@@ -40,10 +40,12 @@ class App extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderMark = props => {
|
||||
renderMark = (props, next) => {
|
||||
switch (props.mark.type) {
|
||||
case 'bold':
|
||||
return <strong {...props.attributes}>{props.children}</strong>
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,9 +72,9 @@ function MarkHotkey(options) {
|
||||
|
||||
// Return our "plugin" object, containing the `onKeyDown` handler.
|
||||
return {
|
||||
onKeyDown(event, change) {
|
||||
// Check that the key pressed matches our `key` option.
|
||||
if (!event.ctrlKey || event.key != key) return
|
||||
onKeyDown(event, change, next) {
|
||||
// If it doesn't match our `key`, let other plugins handle it.
|
||||
if (!event.ctrlKey || event.key != key) return next()
|
||||
|
||||
// Prevent the default characters from being inserted.
|
||||
event.preventDefault()
|
||||
@@ -120,10 +122,12 @@ class App extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderMark = props => {
|
||||
renderMark = (props, next) => {
|
||||
switch (props.mark.type) {
|
||||
case 'bold':
|
||||
return <strong>{props.children}</strong>
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +167,7 @@ class App extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderMark = props => {
|
||||
renderMark = (props, next) => {
|
||||
switch (props.mark.type) {
|
||||
case 'bold':
|
||||
return <strong>{props.children}</strong>
|
||||
@@ -176,6 +180,8 @@ class App extends React.Component {
|
||||
return <del>{props.children}</del>
|
||||
case 'underline':
|
||||
return <u>{props.children}</u>
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -188,8 +188,9 @@ class CodeHighlighting extends React.Component {
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
decorateNode = node => {
|
||||
if (node.type != 'code') return
|
||||
decorateNode = (node, next) => {
|
||||
const others = next() || []
|
||||
if (node.type != 'code') return others
|
||||
|
||||
const language = node.data.get('language')
|
||||
const texts = node.getTexts().toArray()
|
||||
@@ -245,7 +246,7 @@ class CodeHighlighting extends React.Component {
|
||||
start = end
|
||||
}
|
||||
|
||||
return decorations
|
||||
return [...others, ...decorations]
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -81,6 +81,16 @@ class Emojis extends React.Component {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a reference to the `editor`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
ref = editor => {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the app.
|
||||
*
|
||||
@@ -99,6 +109,7 @@ class Emojis extends React.Component {
|
||||
</Toolbar>
|
||||
<Editor
|
||||
placeholder="Write some 😍👋🎉..."
|
||||
ref={this.ref}
|
||||
value={this.state.value}
|
||||
schema={this.schema}
|
||||
onChange={this.onChange}
|
||||
@@ -156,18 +167,16 @@ class Emojis extends React.Component {
|
||||
|
||||
onClickEmoji = (e, code) => {
|
||||
e.preventDefault()
|
||||
const { value } = this.state
|
||||
const change = value.change()
|
||||
|
||||
change
|
||||
.insertInline({
|
||||
type: 'emoji',
|
||||
data: { code },
|
||||
})
|
||||
.moveToStartOfNextText()
|
||||
.focus()
|
||||
|
||||
this.onChange(change)
|
||||
this.editor.change(change => {
|
||||
change
|
||||
.insertInline({
|
||||
type: 'emoji',
|
||||
data: { code },
|
||||
})
|
||||
.moveToStartOfNextText()
|
||||
.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -22,6 +22,16 @@ class History extends React.Component {
|
||||
value: Value.fromJSON(initialValue),
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a reference to the `editor`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
ref = editor => {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the editor.
|
||||
*
|
||||
@@ -30,7 +40,9 @@ class History extends React.Component {
|
||||
|
||||
render() {
|
||||
const { value } = this.state
|
||||
const { history } = value
|
||||
const { data } = value
|
||||
const undos = data.get('undos')
|
||||
const redos = data.get('redos')
|
||||
return (
|
||||
<div>
|
||||
<Toolbar>
|
||||
@@ -40,11 +52,12 @@ class History extends React.Component {
|
||||
<Button onMouseDown={this.onClickRedo}>
|
||||
<Icon>redo</Icon>
|
||||
</Button>
|
||||
<span>Undos: {history.undos.size}</span>
|
||||
<span>Redos: {history.redos.size}</span>
|
||||
<span>Undos: {undos ? undos.size : 0}</span>
|
||||
<span>Redos: {redos ? redos.size : 0}</span>
|
||||
</Toolbar>
|
||||
<Editor
|
||||
placeholder="Enter some text..."
|
||||
ref={this.ref}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
@@ -69,9 +82,7 @@ class History extends React.Component {
|
||||
|
||||
onClickRedo = event => {
|
||||
event.preventDefault()
|
||||
const { value } = this.state
|
||||
const change = value.change().redo()
|
||||
this.onChange(change)
|
||||
this.editor.change(change => change.redo())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,9 +92,7 @@ class History extends React.Component {
|
||||
|
||||
onClickUndo = event => {
|
||||
event.preventDefault()
|
||||
const { value } = this.state
|
||||
const change = value.change().undo()
|
||||
this.onChange(change)
|
||||
this.editor.change(change => change.undo())
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -63,7 +63,8 @@ class HoverMenu extends React.Component {
|
||||
*/
|
||||
|
||||
renderMarkButton(type, icon) {
|
||||
const { value } = this.props
|
||||
const { editor } = this.props
|
||||
const { value } = editor
|
||||
const isActive = value.activeMarks.some(mark => mark.type == type)
|
||||
return (
|
||||
<Button
|
||||
@@ -84,10 +85,9 @@ class HoverMenu extends React.Component {
|
||||
*/
|
||||
|
||||
onClickMark(event, type) {
|
||||
const { value, onChange } = this.props
|
||||
const { editor } = this.props
|
||||
event.preventDefault()
|
||||
const change = value.change().toggleMark(type)
|
||||
onChange(change)
|
||||
editor.change(change => change.toggleMark(type))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,21 +157,34 @@ class HoveringMenu extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<HoverMenu
|
||||
innerRef={menu => (this.menu = menu)}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<Editor
|
||||
placeholder="Enter some text..."
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
renderEditor={this.renderEditor}
|
||||
renderMark={this.renderMark}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the editor.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Editor} editor
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderEditor = (props, editor) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{props.children}
|
||||
<HoverMenu innerRef={menu => (this.menu = menu)} editor={editor} />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Slate mark.
|
||||
*
|
||||
|
@@ -93,6 +93,16 @@ class Images extends React.Component {
|
||||
value: Value.fromJSON(initialValue),
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a reference to the `editor`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
ref = editor => {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the app.
|
||||
*
|
||||
@@ -109,6 +119,7 @@ class Images extends React.Component {
|
||||
</Toolbar>
|
||||
<Editor
|
||||
placeholder="Enter some text..."
|
||||
ref={this.ref}
|
||||
value={this.state.value}
|
||||
schema={schema}
|
||||
onChange={this.onChange}
|
||||
@@ -158,10 +169,7 @@ class Images extends React.Component {
|
||||
event.preventDefault()
|
||||
const src = window.prompt('Enter the URL of the image:')
|
||||
if (!src) return
|
||||
|
||||
const change = this.state.value.change().call(insertImage, src)
|
||||
|
||||
this.onChange(change)
|
||||
this.editor.change(insertImage, src)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,11 +177,11 @@ class Images extends React.Component {
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Change} change
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
onDropOrPaste = (event, change, editor) => {
|
||||
const target = getEventRange(event, change.value)
|
||||
onDropOrPaste = (event, change) => {
|
||||
const { editor } = change
|
||||
const target = getEventRange(event, editor)
|
||||
if (!target && event.type == 'drop') return
|
||||
|
||||
const transfer = getEventTransfer(event)
|
||||
|
@@ -210,12 +210,17 @@ class InputTester extends React.Component {
|
||||
window.document.addEventListener('selectionchange', this.onEvent)
|
||||
}
|
||||
|
||||
ref = editor => {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Wrapper innerRef={this.onRef}>
|
||||
<Editor
|
||||
spellCheck
|
||||
placeholder="Enter some text..."
|
||||
ref={this.ref}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
renderNode={({ attributes, children, node }) => {
|
||||
@@ -268,19 +273,20 @@ class InputTester extends React.Component {
|
||||
}
|
||||
|
||||
recordEvent = event => {
|
||||
const { value } = this.state
|
||||
const { editor } = this
|
||||
const { value } = editor
|
||||
let targetRange
|
||||
|
||||
if (event.getTargetRanges) {
|
||||
const [nativeTargetRange] = event.getTargetRanges()
|
||||
targetRange = nativeTargetRange && findRange(nativeTargetRange, value)
|
||||
targetRange = nativeTargetRange && findRange(nativeTargetRange, editor)
|
||||
}
|
||||
|
||||
const nativeSelection = window.getSelection()
|
||||
const nativeRange = nativeSelection.rangeCount
|
||||
? nativeSelection.getRangeAt(0)
|
||||
: undefined
|
||||
const selection = nativeRange && findRange(nativeRange, value)
|
||||
const selection = nativeRange && findRange(nativeRange, editor)
|
||||
|
||||
EventsValue.push({
|
||||
event,
|
||||
@@ -291,12 +297,13 @@ class InputTester extends React.Component {
|
||||
}
|
||||
|
||||
logEvent = event => {
|
||||
const { value } = this.state
|
||||
const { editor } = this
|
||||
const { value } = editor
|
||||
const nativeSelection = window.getSelection()
|
||||
const nativeRange = nativeSelection.rangeCount
|
||||
? nativeSelection.getRangeAt(0)
|
||||
: undefined
|
||||
const selection = nativeRange && findRange(nativeRange, value)
|
||||
const selection = nativeRange && findRange(nativeRange, editor)
|
||||
|
||||
const {
|
||||
type,
|
||||
@@ -323,7 +330,7 @@ class InputTester extends React.Component {
|
||||
style += '; background-color: lightskyblue'
|
||||
const [nativeTargetRange] = event.getTargetRanges()
|
||||
const targetRange =
|
||||
nativeTargetRange && findRange(nativeTargetRange, value)
|
||||
nativeTargetRange && findRange(nativeTargetRange, editor)
|
||||
|
||||
details = {
|
||||
inputType,
|
||||
|
@@ -60,6 +60,16 @@ class Links extends React.Component {
|
||||
return value.inlines.some(inline => inline.type == 'link')
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a reference to the `editor`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
ref = editor => {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the app.
|
||||
*
|
||||
@@ -76,6 +86,7 @@ class Links extends React.Component {
|
||||
</Toolbar>
|
||||
<Editor
|
||||
placeholder="Enter some text..."
|
||||
ref={this.ref}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
onPaste={this.onPaste}
|
||||
@@ -127,40 +138,40 @@ class Links extends React.Component {
|
||||
|
||||
onClickLink = event => {
|
||||
event.preventDefault()
|
||||
const { value } = this.state
|
||||
const hasLinks = this.hasLinks()
|
||||
const change = value.change()
|
||||
|
||||
if (hasLinks) {
|
||||
change.call(unwrapLink)
|
||||
} else if (value.selection.isExpanded) {
|
||||
const href = window.prompt('Enter the URL of the link:')
|
||||
this.editor.change(change => {
|
||||
const { value } = change
|
||||
const hasLinks = this.hasLinks()
|
||||
|
||||
if (href === null) {
|
||||
return
|
||||
if (hasLinks) {
|
||||
change.call(unwrapLink)
|
||||
} else if (value.selection.isExpanded) {
|
||||
const href = window.prompt('Enter the URL of the link:')
|
||||
|
||||
if (href === null) {
|
||||
return
|
||||
}
|
||||
|
||||
change.call(wrapLink, href)
|
||||
} else {
|
||||
const href = window.prompt('Enter the URL of the link:')
|
||||
|
||||
if (href === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const text = window.prompt('Enter the text for the link:')
|
||||
|
||||
if (text === null) {
|
||||
return
|
||||
}
|
||||
|
||||
change
|
||||
.insertText(text)
|
||||
.moveFocusBackward(text.length)
|
||||
.call(wrapLink, href)
|
||||
}
|
||||
|
||||
change.call(wrapLink, href)
|
||||
} else {
|
||||
const href = window.prompt('Enter the URL of the link:')
|
||||
|
||||
if (href === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const text = window.prompt('Enter the text for the link:')
|
||||
|
||||
if (text === null) {
|
||||
return
|
||||
}
|
||||
|
||||
change
|
||||
.insertText(text)
|
||||
.moveFocusBackward(text.length)
|
||||
.call(wrapLink, href)
|
||||
}
|
||||
|
||||
this.onChange(change)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -135,11 +135,13 @@ class MarkdownPreview extends React.Component {
|
||||
* Define a decorator for markdown styles.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @param {Function} next
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
decorateNode(node) {
|
||||
if (node.object != 'block') return
|
||||
decorateNode(node, next) {
|
||||
const others = next() || []
|
||||
if (node.object != 'block') return others
|
||||
|
||||
const string = node.text
|
||||
const texts = node.getTexts().toArray()
|
||||
@@ -202,7 +204,7 @@ class MarkdownPreview extends React.Component {
|
||||
start = end
|
||||
}
|
||||
|
||||
return decorations
|
||||
return [...others, ...decorations]
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -66,6 +66,16 @@ class RichTextExample extends React.Component {
|
||||
return value.blocks.some(node => node.type == type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a reference to the `editor`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
ref = editor => {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
/**
|
||||
* Render.
|
||||
*
|
||||
@@ -90,6 +100,7 @@ class RichTextExample extends React.Component {
|
||||
spellCheck
|
||||
autoFocus
|
||||
placeholder="Enter some rich text..."
|
||||
ref={this.ref}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
@@ -243,9 +254,10 @@ class RichTextExample extends React.Component {
|
||||
|
||||
onClickMark = (event, type) => {
|
||||
event.preventDefault()
|
||||
const { value } = this.state
|
||||
const change = value.change().toggleMark(type)
|
||||
this.onChange(change)
|
||||
|
||||
this.editor.change(change => {
|
||||
change.toggleMark(type)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,47 +269,47 @@ class RichTextExample extends React.Component {
|
||||
|
||||
onClickBlock = (event, type) => {
|
||||
event.preventDefault()
|
||||
const { value } = this.state
|
||||
const change = value.change()
|
||||
const { document } = value
|
||||
|
||||
// Handle everything but list buttons.
|
||||
if (type != 'bulleted-list' && type != 'numbered-list') {
|
||||
const isActive = this.hasBlock(type)
|
||||
const isList = this.hasBlock('list-item')
|
||||
this.editor.change(change => {
|
||||
const { value } = change
|
||||
const { document } = value
|
||||
|
||||
if (isList) {
|
||||
change
|
||||
.setBlocks(isActive ? DEFAULT_NODE : type)
|
||||
.unwrapBlock('bulleted-list')
|
||||
.unwrapBlock('numbered-list')
|
||||
// Handle everything but list buttons.
|
||||
if (type != 'bulleted-list' && type != 'numbered-list') {
|
||||
const isActive = this.hasBlock(type)
|
||||
const isList = this.hasBlock('list-item')
|
||||
|
||||
if (isList) {
|
||||
change
|
||||
.setBlocks(isActive ? DEFAULT_NODE : type)
|
||||
.unwrapBlock('bulleted-list')
|
||||
.unwrapBlock('numbered-list')
|
||||
} else {
|
||||
change.setBlocks(isActive ? DEFAULT_NODE : type)
|
||||
}
|
||||
} else {
|
||||
change.setBlocks(isActive ? DEFAULT_NODE : type)
|
||||
}
|
||||
} else {
|
||||
// Handle the extra wrapping required for list buttons.
|
||||
const isList = this.hasBlock('list-item')
|
||||
const isType = value.blocks.some(block => {
|
||||
return !!document.getClosest(block.key, parent => parent.type == type)
|
||||
})
|
||||
// Handle the extra wrapping required for list buttons.
|
||||
const isList = this.hasBlock('list-item')
|
||||
const isType = value.blocks.some(block => {
|
||||
return !!document.getClosest(block.key, parent => parent.type == type)
|
||||
})
|
||||
|
||||
if (isList && isType) {
|
||||
change
|
||||
.setBlocks(DEFAULT_NODE)
|
||||
.unwrapBlock('bulleted-list')
|
||||
.unwrapBlock('numbered-list')
|
||||
} else if (isList) {
|
||||
change
|
||||
.unwrapBlock(
|
||||
type == 'bulleted-list' ? 'numbered-list' : 'bulleted-list'
|
||||
)
|
||||
.wrapBlock(type)
|
||||
} else {
|
||||
change.setBlocks('list-item').wrapBlock(type)
|
||||
if (isList && isType) {
|
||||
change
|
||||
.setBlocks(DEFAULT_NODE)
|
||||
.unwrapBlock('bulleted-list')
|
||||
.unwrapBlock('numbered-list')
|
||||
} else if (isList) {
|
||||
change
|
||||
.unwrapBlock(
|
||||
type == 'bulleted-list' ? 'numbered-list' : 'bulleted-list'
|
||||
)
|
||||
.wrapBlock(type)
|
||||
} else {
|
||||
change.setBlocks('list-item').wrapBlock(type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.onChange(change)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -59,6 +59,16 @@ class SearchHighlighting extends React.Component {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a reference to the `editor`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
ref = editor => {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
/**
|
||||
* Render.
|
||||
*
|
||||
@@ -80,6 +90,7 @@ class SearchHighlighting extends React.Component {
|
||||
</Toolbar>
|
||||
<Editor
|
||||
placeholder="Enter some rich text..."
|
||||
ref={this.ref}
|
||||
value={this.state.value}
|
||||
schema={this.schema}
|
||||
onChange={this.onChange}
|
||||
@@ -127,38 +138,36 @@ class SearchHighlighting extends React.Component {
|
||||
*/
|
||||
|
||||
onInputChange = event => {
|
||||
const { value } = this.state
|
||||
const string = event.target.value
|
||||
const texts = value.document.getTexts()
|
||||
const decorations = []
|
||||
this.editor.change(change => {
|
||||
const { value } = change
|
||||
const string = event.target.value
|
||||
const texts = value.document.getTexts()
|
||||
const decorations = []
|
||||
|
||||
texts.forEach(node => {
|
||||
const { key, text } = node
|
||||
const parts = text.split(string)
|
||||
let offset = 0
|
||||
texts.forEach(node => {
|
||||
const { key, text } = node
|
||||
const parts = text.split(string)
|
||||
let offset = 0
|
||||
|
||||
parts.forEach((part, i) => {
|
||||
if (i != 0) {
|
||||
decorations.push({
|
||||
anchor: { key, offset: offset - string.length },
|
||||
focus: { key, offset },
|
||||
mark: { type: 'highlight' },
|
||||
})
|
||||
}
|
||||
parts.forEach((part, i) => {
|
||||
if (i != 0) {
|
||||
decorations.push({
|
||||
anchor: { key, offset: offset - string.length },
|
||||
focus: { key, offset },
|
||||
mark: { type: 'highlight' },
|
||||
})
|
||||
}
|
||||
|
||||
offset = offset + part.length + string.length
|
||||
offset = offset + part.length + string.length
|
||||
})
|
||||
})
|
||||
|
||||
// Make the change to decorations without saving it into the undo history,
|
||||
// so that there isn't a confusing behavior when undoing.
|
||||
change.withoutSaving(() => {
|
||||
change.setValue({ decorations })
|
||||
})
|
||||
})
|
||||
|
||||
const change = value.change()
|
||||
|
||||
// Make the change to decorations without saving it into the undo history,
|
||||
// so that there isn't a confusing behavior when undoing.
|
||||
change.withoutSaving(() => {
|
||||
change.setValue({ decorations })
|
||||
})
|
||||
|
||||
this.onChange(change)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -55,9 +55,9 @@ class SyncingEditor extends React.Component {
|
||||
*/
|
||||
|
||||
applyOperations = operations => {
|
||||
const { value } = this.state
|
||||
const change = value.change().applyOperations(operations)
|
||||
this.onChange(change, { remote: true })
|
||||
this.remote = true
|
||||
this.editor.change(change => change.applyOperations(operations))
|
||||
this.remote = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,6 +72,16 @@ class SyncingEditor extends React.Component {
|
||||
return value.activeMarks.some(mark => mark.type == type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a reference to the `editor`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
ref = editor => {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
/**
|
||||
* Render.
|
||||
*
|
||||
@@ -89,6 +99,7 @@ class SyncingEditor extends React.Component {
|
||||
</Toolbar>
|
||||
<Editor
|
||||
placeholder="Enter some text..."
|
||||
ref={this.ref}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
@@ -151,7 +162,7 @@ class SyncingEditor extends React.Component {
|
||||
onChange = (change, options = {}) => {
|
||||
this.setState({ value: change.value })
|
||||
|
||||
if (!options.remote) {
|
||||
if (!this.remote) {
|
||||
this.props.onChange(change)
|
||||
}
|
||||
}
|
||||
@@ -193,9 +204,7 @@ class SyncingEditor extends React.Component {
|
||||
|
||||
onClickMark = (event, type) => {
|
||||
event.preventDefault()
|
||||
const { value } = this.state
|
||||
const change = value.change().toggleMark(type)
|
||||
this.onChange(change)
|
||||
this.editor.change(change => change.toggleMark(type))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,91 +1,28 @@
|
||||
import { Value, Operation } from 'slate'
|
||||
import { Value } from 'slate'
|
||||
import { Editor } from 'slate-react'
|
||||
|
||||
import React from 'react'
|
||||
import styled from 'react-emotion'
|
||||
|
||||
import { Stack } from 'immutable'
|
||||
import { List } from 'immutable'
|
||||
|
||||
import initialValue from './value.json'
|
||||
import { Button, Icon, Toolbar } from '../components'
|
||||
|
||||
const initialVersionState = [
|
||||
{
|
||||
name: 'version 1',
|
||||
isRoot: true,
|
||||
value: {
|
||||
document: {
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
text: 'This example shows versions.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Commands.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
const commands = {
|
||||
resetHistory(change) {
|
||||
const { value } = change
|
||||
const { data } = value
|
||||
const newData = data.set('undos', List()).set('redos', List())
|
||||
|
||||
change.withoutSaving(() => {
|
||||
change.setValue({ data: newData })
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'version 2',
|
||||
changes: [
|
||||
[
|
||||
Operation.create({
|
||||
object: 'operation',
|
||||
path: [0, 0],
|
||||
position: 28,
|
||||
properties: {
|
||||
data: {},
|
||||
type: undefined,
|
||||
},
|
||||
target: null,
|
||||
type: 'split_node',
|
||||
}),
|
||||
Operation.create({
|
||||
object: 'operation',
|
||||
path: [0],
|
||||
position: 1,
|
||||
properties: {
|
||||
data: {},
|
||||
type: 'paragraph',
|
||||
},
|
||||
target: 28,
|
||||
type: 'split_node',
|
||||
}),
|
||||
],
|
||||
[
|
||||
Operation.create({
|
||||
marks: [],
|
||||
object: 'operation',
|
||||
offset: 0,
|
||||
path: [1, 0],
|
||||
text: 'Try adding a new version by clicking the + icon.',
|
||||
type: 'insert_text',
|
||||
}),
|
||||
],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const VersionList = styled('ul')``
|
||||
|
||||
export const VersionListItem = styled('li')`
|
||||
cursor: pointer;
|
||||
color: ${props => (props.active ? 'red' : 'black')};
|
||||
`
|
||||
|
||||
export const Version = ({ active, onClick, name }) => (
|
||||
<VersionListItem active={active} onClick={onClick}>
|
||||
{name}
|
||||
</VersionListItem>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The versions example.
|
||||
@@ -101,132 +38,27 @@ class Versions extends React.Component {
|
||||
*/
|
||||
|
||||
state = {
|
||||
value: Value.fromJSON(initialVersionState[0].value),
|
||||
versions: initialVersionState,
|
||||
activeVersionIndex: 0,
|
||||
value: Value.fromJSON(initialValue),
|
||||
versions: [],
|
||||
v: '',
|
||||
}
|
||||
|
||||
/**
|
||||
* On mounting, save an initial version.
|
||||
*/
|
||||
|
||||
componentDidMount() {
|
||||
this.setVersion(initialVersionState.length - 1)
|
||||
this.saveVersion()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the history stack
|
||||
* Store a reference to the `editor`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
resetHistory = () => {
|
||||
let { value } = this.state
|
||||
const change = value.change()
|
||||
|
||||
const history = value.history
|
||||
.set('undos', new Stack())
|
||||
.set('redos', new Stack())
|
||||
value = value.set('history', history)
|
||||
change.value = value
|
||||
|
||||
this.onChange(change)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a version as the active version
|
||||
*
|
||||
* @param {Number} index
|
||||
*/
|
||||
|
||||
setVersion = index => {
|
||||
const { value, versions, activeVersionIndex } = this.state
|
||||
|
||||
if (index === activeVersionIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
this.resetHistory()
|
||||
|
||||
const change = value.change()
|
||||
const version = versions[index]
|
||||
|
||||
// the root just has a value so set it explicitly.
|
||||
if (version.isRoot) {
|
||||
this.setState({
|
||||
activeVersionIndex: index,
|
||||
value: Value.fromJSON(version.value),
|
||||
})
|
||||
} else {
|
||||
const isForward = index > activeVersionIndex
|
||||
|
||||
let operationsToApply
|
||||
|
||||
if (isForward) {
|
||||
operationsToApply = versions
|
||||
.slice(activeVersionIndex + 1, index + 1)
|
||||
.map(v => v.changes.flat())
|
||||
.flat()
|
||||
} else {
|
||||
operationsToApply = versions
|
||||
.slice(index + 1, activeVersionIndex + 1)
|
||||
.map(v => v.changes.flat())
|
||||
.flat()
|
||||
.reverse()
|
||||
.map(op => op.invert())
|
||||
}
|
||||
|
||||
change.withoutNormalizing(() => {
|
||||
change.withoutSaving(() => {
|
||||
change.applyOperations(operationsToApply)
|
||||
})
|
||||
})
|
||||
|
||||
this.onChange(change)
|
||||
this.setState({ activeVersionIndex: index })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a version below the active version
|
||||
*
|
||||
*/
|
||||
|
||||
addVersion = () => {
|
||||
/*
|
||||
*/
|
||||
|
||||
const versionName = window.prompt('How do you want to call this version?')
|
||||
|
||||
if (!versionName) {
|
||||
return
|
||||
}
|
||||
|
||||
const { value, versions, activeVersionIndex } = this.state
|
||||
const { history } = value
|
||||
|
||||
const newVersion = {
|
||||
name: versionName,
|
||||
value: this.state.value.toJSON(),
|
||||
changes: history.undos
|
||||
.toArray()
|
||||
.reverse()
|
||||
.map(list => list.toArray()),
|
||||
}
|
||||
|
||||
this.setState({
|
||||
versions: [...versions, newVersion],
|
||||
activeVersionIndex: activeVersionIndex + 1,
|
||||
})
|
||||
|
||||
this.resetHistory()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we are at the last version
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
||||
atTail = () => {
|
||||
const { versions, activeVersionIndex } = this.state
|
||||
|
||||
return versions.length - 1 === activeVersionIndex
|
||||
ref = editor => {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,31 +68,31 @@ class Versions extends React.Component {
|
||||
*/
|
||||
|
||||
render() {
|
||||
const { value, versions, activeVersionIndex } = this.state
|
||||
const { history } = value
|
||||
|
||||
const { versions, v } = this.state
|
||||
return (
|
||||
<div>
|
||||
<VersionList>
|
||||
{versions.map((version, index) => (
|
||||
<Version
|
||||
key={index}
|
||||
name={version.name}
|
||||
active={index === activeVersionIndex}
|
||||
onClick={() => this.setVersion(index)}
|
||||
/>
|
||||
))}
|
||||
</VersionList>
|
||||
<Toolbar>
|
||||
<Button active={history.undos.size} onMouseDown={this.addVersion}>
|
||||
<Icon>add</Icon>
|
||||
<select value={v} onChange={this.onVersionSelectChange}>
|
||||
<option disabled>Choose a version to rollback to...</option>
|
||||
{versions.map((version, i) => {
|
||||
const { createdAt } = version
|
||||
const time = createdAt.toLocaleTimeString()
|
||||
const name = `Version ${i + 1} — ${time}`
|
||||
return (
|
||||
<option key={i} value={i}>
|
||||
{name}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
<Button onMouseDown={this.saveVersion}>
|
||||
<Icon>add</Icon> Save Version
|
||||
</Button>
|
||||
<span>Undos: {history.undos.size}</span>
|
||||
<span>Redos: {history.redos.size}</span>
|
||||
</Toolbar>
|
||||
<Editor
|
||||
readOnly={!this.atTail()}
|
||||
placeholder="Enter some text..."
|
||||
commands={commands}
|
||||
ref={this.ref}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
@@ -277,6 +109,93 @@ class Versions extends React.Component {
|
||||
onChange = ({ value }) => {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
/**
|
||||
* On version select change.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onVersionSelectChange = event => {
|
||||
const { value } = event.target
|
||||
const n = parseInt(value, 10)
|
||||
this.setVersion(n)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current version to version number `n`.
|
||||
*
|
||||
* @param {Number} n
|
||||
*/
|
||||
|
||||
setVersion = n => {
|
||||
const { versions, v } = this.state
|
||||
const isForward = n > v
|
||||
if (n === v) return
|
||||
|
||||
this.editor.change(change => {
|
||||
let operations
|
||||
|
||||
if (isForward) {
|
||||
operations = versions
|
||||
.slice(v + 1, n + 1)
|
||||
.map(vers => vers.operations)
|
||||
.flat()
|
||||
} else {
|
||||
operations = versions
|
||||
.slice(n + 1, v + 1)
|
||||
.map(vers => vers.operations)
|
||||
.flat()
|
||||
.reverse()
|
||||
.map(op => op.invert())
|
||||
}
|
||||
|
||||
change.withoutNormalizing(() => {
|
||||
change.withoutSaving(() => {
|
||||
change.applyOperations(operations)
|
||||
change.resetHistory()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
this.setState(state => {
|
||||
return {
|
||||
versions: state.versions.slice(0, n + 1),
|
||||
v: n,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new version checkpoint to the version history.
|
||||
*/
|
||||
|
||||
saveVersion = () => {
|
||||
const { value } = this.state
|
||||
const { data } = value
|
||||
const undos = data.get('undos') || List()
|
||||
const version = {
|
||||
createdAt: new Date(),
|
||||
operations: undos
|
||||
.toArray()
|
||||
.reverse()
|
||||
.map(list => list.toArray()),
|
||||
}
|
||||
|
||||
this.setState(
|
||||
state => {
|
||||
return {
|
||||
versions: [...state.versions, version],
|
||||
v: state.v === '' ? 0 : state.v + 1,
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.editor.change(change => {
|
||||
change.resetHistory()
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Versions
|
||||
|
21
examples/versions/value.json
Normal file
21
examples/versions/value.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"document": {
|
||||
"nodes": [
|
||||
{
|
||||
"object": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"object": "text",
|
||||
"leaves": [
|
||||
{
|
||||
"text":
|
||||
"This example shows how you might implement a version history, where you can save a new version after applying some changes, and then rollback to a previous version at any time."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@@ -1 +0,0 @@
|
||||
This package contains the warning logger that Slate uses to log warnings and deprecations only when in development environments.
|
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "slate-dev-warning",
|
||||
"description": "INTERNAL: A simple, development-only warning helper for Slate.",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"repository": "git://github.com/ianstormtaylor/slate.git",
|
||||
"main": "lib/slate-dev-warning.js",
|
||||
"module": "lib/slate-dev-warning.es.js",
|
||||
"umd": "dist/slate-dev-warning.js",
|
||||
"umdMin": "dist/slate-dev-warning.min.js",
|
||||
"files": [
|
||||
"dist/",
|
||||
"lib/"
|
||||
],
|
||||
"devDependencies": {
|
||||
"mocha": "^2.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist ./lib ./node_modules"
|
||||
}
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* A `warning` helper, modeled after Facebook's and the `tiny-invariant` library.
|
||||
*
|
||||
* @param {Mixed} condition
|
||||
* @param {String} message
|
||||
*/
|
||||
|
||||
export default function warning(condition, message = '') {
|
||||
if (condition) return
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const log = console.warn || console.log // eslint-disable-line no-console
|
||||
|
||||
if (isProduction) {
|
||||
log('Warning')
|
||||
} else {
|
||||
log(`Warning: ${message}`)
|
||||
}
|
||||
}
|
@@ -9,7 +9,9 @@ export const input = ''
|
||||
export const output = (
|
||||
<value>
|
||||
<document>
|
||||
<paragraph />
|
||||
<paragraph>
|
||||
<text />
|
||||
</paragraph>
|
||||
</document>
|
||||
</value>
|
||||
)
|
||||
|
@@ -42,7 +42,13 @@ export const output = (
|
||||
<value>
|
||||
<document>
|
||||
<paragraph>
|
||||
o<b>ne</b>
|
||||
<text>o</text>
|
||||
<text>
|
||||
<b>n</b>
|
||||
</text>
|
||||
<text>
|
||||
<b>e</b>
|
||||
</text>
|
||||
</paragraph>
|
||||
</document>
|
||||
</value>
|
||||
|
@@ -42,9 +42,15 @@ export const output = (
|
||||
<value>
|
||||
<document>
|
||||
<paragraph>
|
||||
o<i>
|
||||
n<b>e</b>
|
||||
</i>
|
||||
<text>o</text>
|
||||
<text>
|
||||
<i>n</i>
|
||||
</text>
|
||||
<text>
|
||||
<i>
|
||||
<b>e</b>
|
||||
</i>
|
||||
</text>
|
||||
</paragraph>
|
||||
</document>
|
||||
</value>
|
||||
|
@@ -41,11 +41,13 @@ export const output = (
|
||||
<value>
|
||||
<document>
|
||||
<paragraph>
|
||||
<b>
|
||||
one
|
||||
<linebreak />
|
||||
two
|
||||
</b>
|
||||
<text>
|
||||
<b>one</b>
|
||||
</text>
|
||||
<linebreak />
|
||||
<text>
|
||||
<b>two</b>
|
||||
</text>
|
||||
</paragraph>
|
||||
</document>
|
||||
</value>
|
||||
|
@@ -36,7 +36,10 @@ export const output = (
|
||||
<value>
|
||||
<document>
|
||||
<paragraph>
|
||||
on<b thing="value">e</b>
|
||||
<text>on</text>
|
||||
<text>
|
||||
<b thing="value">e</b>
|
||||
</text>
|
||||
</paragraph>
|
||||
</document>
|
||||
</value>
|
||||
|
@@ -35,7 +35,10 @@ export const output = (
|
||||
<value>
|
||||
<document>
|
||||
<paragraph>
|
||||
on<b>e</b>
|
||||
<text>on</text>
|
||||
<text>
|
||||
<b>e</b>
|
||||
</text>
|
||||
</paragraph>
|
||||
</document>
|
||||
</value>
|
||||
|
@@ -4,6 +4,68 @@ This document maintains a list of changes to the `slate-hyperscript` package wit
|
||||
|
||||
---
|
||||
|
||||
### `0.11.0` — October 9, 2018
|
||||
|
||||
###### BREAKING
|
||||
|
||||
**Updated to the latest version of `slate`.** The `slate-hyperscript` codebase has been updated to be compatible with the latest version of `slate`, `0.42.0`. This is a backward incompatible upgrade, and so the peer dependency range has been bumped.
|
||||
|
||||
**`slate-hyperscript` no longer normalizes values.** This behavior was very problematic because it meant that you could not determine exactly what output you'd receive from any given hyperscript creation. The logic for creating child nodes was inconsistent, relying on the built-in normalization to help keep it "normal". While this is sometimes helpful, it makes writing tests for invalid states very tricky, if not impossible.
|
||||
|
||||
Now, `slate-hyperscript` does not do any normalization, meaning that you can create any document structure with it. For example, you can create a block node inside an inline node, even though a Slate editor wouldn't allow it. Or, if you don't create leaf text nodes, they won't exist in the output.
|
||||
|
||||
For example these are no longer equivalent:
|
||||
|
||||
```jsx
|
||||
<document>
|
||||
<paragraph>
|
||||
<link>word</link>
|
||||
</paragraph>
|
||||
</document>
|
||||
```
|
||||
|
||||
```jsx
|
||||
<document>
|
||||
<paragraph>
|
||||
<text />
|
||||
<link>word</link>
|
||||
<text />
|
||||
</paragraph>
|
||||
</document>
|
||||
```
|
||||
|
||||
Similarly, these are no longer equivalent either:
|
||||
|
||||
```jsx
|
||||
<document>
|
||||
<paragraph />
|
||||
</document>
|
||||
```
|
||||
|
||||
```jsx
|
||||
<document>
|
||||
<paragraph>
|
||||
<text />
|
||||
</paragraph>
|
||||
</document>
|
||||
```
|
||||
|
||||
This allows you to much more easily test invalid states and transition states. However, it means that you need to be more explicit in the "normal" states than previously.
|
||||
|
||||
**The `<text>` and `<mark>` creators now return useful objects.** This is a related change that makes the library more useful. Previously you could expect to receive a `value` from the `<value>` creator, but the others were less consistent. For example, the `<text>` creator would actually return an array, instead of the `Text` node that you expect.
|
||||
|
||||
```js
|
||||
// Previously you had to do...
|
||||
const text = <text>word</text>[0]
|
||||
|
||||
// But now it's more obvious...
|
||||
const text = <text>word</text>
|
||||
```
|
||||
|
||||
Similarly, the `mark` creator used to return a `Text` node. Now it returns a list of `Leaf` objects, which can be passed directly as children to the `<text>` creator.
|
||||
|
||||
---
|
||||
|
||||
### `0.10.0` — August 22, 2018
|
||||
|
||||
###### BREAKING
|
||||
|
530
packages/slate-hyperscript/src/creators.js
Normal file
530
packages/slate-hyperscript/src/creators.js
Normal file
@@ -0,0 +1,530 @@
|
||||
import {
|
||||
Decoration,
|
||||
Document,
|
||||
Leaf,
|
||||
Mark,
|
||||
Node,
|
||||
Point,
|
||||
Selection,
|
||||
Text,
|
||||
Value,
|
||||
} from 'slate'
|
||||
|
||||
/**
|
||||
* Auto-incrementing ID to keep track of paired decorations.
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
|
||||
let uid = 0
|
||||
|
||||
/**
|
||||
* Create an anchor point.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {AnchorPoint}
|
||||
*/
|
||||
|
||||
export function createAnchor(tagName, attributes, children) {
|
||||
return new AnchorPoint(attributes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a block.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {Block}
|
||||
*/
|
||||
|
||||
export function createBlock(tagName, attributes, children) {
|
||||
const attrs = { ...attributes, object: 'block' }
|
||||
const block = createNode('node', attrs, children)
|
||||
return block
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cursor point.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {CursorPoint}
|
||||
*/
|
||||
|
||||
export function createCursor(tagName, attributes, children) {
|
||||
return new CursorPoint(attributes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a decoration point, or wrap a list of leaves and set the decoration
|
||||
* point tracker on them.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {DecorationPoint|List<Leaf>}
|
||||
*/
|
||||
|
||||
export function createDecoration(tagName, attributes, children) {
|
||||
const { key, data } = attributes
|
||||
const type = tagName
|
||||
|
||||
if (key) {
|
||||
return new DecorationPoint({ id: key, type, data })
|
||||
}
|
||||
|
||||
const leaves = createLeaves('leaves', {}, children)
|
||||
const first = leaves.first()
|
||||
const last = leaves.last()
|
||||
const id = `__decoration_${uid++}__`
|
||||
const start = new DecorationPoint({ id, type, data })
|
||||
const end = new DecorationPoint({ id, type, data })
|
||||
setPoint(first, start, 0)
|
||||
setPoint(last, end, last.text.length)
|
||||
return leaves
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {Document}
|
||||
*/
|
||||
|
||||
export function createDocument(tagName, attributes, children) {
|
||||
const attrs = { ...attributes, object: 'document' }
|
||||
const document = createNode('node', attrs, children)
|
||||
return document
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a focus point.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {FocusPoint}
|
||||
*/
|
||||
|
||||
export function createFocus(tagName, attributes, children) {
|
||||
return new FocusPoint(attributes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an inline.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {Inline}
|
||||
*/
|
||||
|
||||
export function createInline(tagName, attributes, children) {
|
||||
const attrs = { ...attributes, object: 'inline' }
|
||||
const inline = createNode('node', attrs, children)
|
||||
return inline
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a list of leaves.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {List<Leaf>}
|
||||
*/
|
||||
|
||||
export function createLeaves(tagName, attributes, children) {
|
||||
const { marks = Mark.createSet() } = attributes
|
||||
let length = 0
|
||||
let leaves = Leaf.createList([])
|
||||
let leaf
|
||||
|
||||
children.forEach(child => {
|
||||
if (Leaf.isLeafList(child)) {
|
||||
if (leaf) {
|
||||
leaves = leaves.push(leaf)
|
||||
leaf = null
|
||||
}
|
||||
|
||||
child.forEach(l => {
|
||||
l = preservePoint(l, obj => obj.addMarks(marks))
|
||||
leaves = leaves.push(l)
|
||||
})
|
||||
} else {
|
||||
if (!leaf) {
|
||||
leaf = Leaf.create({ marks, text: '' })
|
||||
length = 0
|
||||
}
|
||||
|
||||
if (typeof child === 'string') {
|
||||
const offset = leaf.text.length
|
||||
leaf = preservePoint(leaf, obj => obj.insertText(offset, child))
|
||||
length += child.length
|
||||
}
|
||||
|
||||
if (isPoint(child)) {
|
||||
setPoint(leaf, child, length)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!leaves.size && !leaf) {
|
||||
leaf = Leaf.create({ marks, text: '' })
|
||||
}
|
||||
|
||||
if (leaf) {
|
||||
leaves = leaves.push(leaf)
|
||||
}
|
||||
|
||||
return leaves
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a list of leaves from a mark.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {List<Leaf>}
|
||||
*/
|
||||
|
||||
export function createMark(tagName, attributes, children) {
|
||||
const marks = Mark.createSet([attributes])
|
||||
const leaves = createLeaves('leaves', { marks }, children)
|
||||
return leaves
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a node.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {Node}
|
||||
*/
|
||||
|
||||
export function createNode(tagName, attributes, children) {
|
||||
const { object } = attributes
|
||||
|
||||
if (object === 'text') {
|
||||
return createText('text', {}, children)
|
||||
}
|
||||
|
||||
const nodes = []
|
||||
let others = []
|
||||
|
||||
children.forEach(child => {
|
||||
if (Node.isNode(child)) {
|
||||
if (others.length) {
|
||||
const text = createText('text', {}, others)
|
||||
nodes.push(text)
|
||||
}
|
||||
|
||||
nodes.push(child)
|
||||
others = []
|
||||
} else {
|
||||
others.push(child)
|
||||
}
|
||||
})
|
||||
|
||||
if (others.length) {
|
||||
const text = createText('text', {}, others)
|
||||
nodes.push(text)
|
||||
}
|
||||
|
||||
const node = Node.create({ ...attributes, nodes })
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a selection.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {Selection}
|
||||
*/
|
||||
|
||||
export function createSelection(tagName, attributes, children) {
|
||||
const anchor = children.find(c => c instanceof AnchorPoint)
|
||||
const focus = children.find(c => c instanceof FocusPoint)
|
||||
const { marks, focused } = attributes
|
||||
const selection = Selection.create({
|
||||
marks,
|
||||
isFocused: focused,
|
||||
anchor: anchor && {
|
||||
key: anchor.key,
|
||||
offset: anchor.offset,
|
||||
path: anchor.path,
|
||||
},
|
||||
focus: focus && {
|
||||
key: focus.key,
|
||||
offset: focus.offset,
|
||||
path: focus.path,
|
||||
},
|
||||
})
|
||||
|
||||
return selection
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a text node.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {Text}
|
||||
*/
|
||||
|
||||
export function createText(tagName, attributes, children) {
|
||||
const { key } = attributes
|
||||
const leaves = createLeaves('leaves', {}, children)
|
||||
const text = Text.create({ key, leaves })
|
||||
let length = 0
|
||||
|
||||
leaves.forEach(leaf => {
|
||||
incrementPoint(leaf, length)
|
||||
preservePoint(leaf, () => text)
|
||||
length += leaf.text.length
|
||||
})
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a value.
|
||||
*
|
||||
* @param {String} tagName
|
||||
* @param {Object} attributes
|
||||
* @param {Array} children
|
||||
* @return {Value}
|
||||
*/
|
||||
|
||||
export function createValue(tagName, attributes, children) {
|
||||
const { data } = attributes
|
||||
const document = children.find(Document.isDocument)
|
||||
let selection = children.find(Selection.isSelection)
|
||||
let anchor
|
||||
let focus
|
||||
let decorations = []
|
||||
const partials = {}
|
||||
|
||||
// Search the document's texts to see if any of them have the anchor or
|
||||
// focus information saved, or decorations applied.
|
||||
if (document) {
|
||||
document.getTexts().forEach(text => {
|
||||
if (text.__anchor != null) {
|
||||
anchor = Point.create({ key: text.key, offset: text.__anchor.offset })
|
||||
}
|
||||
|
||||
if (text.__focus != null) {
|
||||
focus = Point.create({ key: text.key, offset: text.__focus.offset })
|
||||
}
|
||||
|
||||
if (text.__decorations != null) {
|
||||
for (const dec of text.__decorations) {
|
||||
const { id } = dec
|
||||
const partial = partials[id]
|
||||
delete partials[id]
|
||||
|
||||
if (!partial) {
|
||||
dec.key = text.key
|
||||
partials[id] = dec
|
||||
continue
|
||||
}
|
||||
|
||||
const decoration = Decoration.create({
|
||||
anchor: {
|
||||
key: partial.key,
|
||||
offset: partial.offset,
|
||||
},
|
||||
focus: {
|
||||
key: text.key,
|
||||
offset: dec.offset,
|
||||
},
|
||||
mark: {
|
||||
type: dec.type,
|
||||
data: dec.data,
|
||||
},
|
||||
})
|
||||
|
||||
decorations.push(decoration)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (Object.keys(partials).length > 0) {
|
||||
throw new Error(
|
||||
`Slate hyperscript must have both a start and an end defined for each decoration using the \`key=\` prop.`
|
||||
)
|
||||
}
|
||||
|
||||
if (anchor && !focus) {
|
||||
throw new Error(
|
||||
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<anchor />\`. For collapsed selections, use \`<cursor />\` instead.`
|
||||
)
|
||||
}
|
||||
|
||||
if (!anchor && focus) {
|
||||
throw new Error(
|
||||
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<focus />\`. For collapsed selections, use \`<cursor />\` instead.`
|
||||
)
|
||||
}
|
||||
|
||||
if (anchor || focus) {
|
||||
if (!selection) {
|
||||
selection = Selection.create({ anchor, focus, isFocused: true })
|
||||
} else {
|
||||
selection = selection.setPoints([anchor, focus])
|
||||
}
|
||||
} else if (!selection) {
|
||||
selection = Selection.create()
|
||||
}
|
||||
|
||||
selection = selection.normalize(document)
|
||||
|
||||
if (decorations.length > 0) {
|
||||
decorations = decorations.map(d => d.normalize(document))
|
||||
}
|
||||
|
||||
const value = Value.fromJSON({
|
||||
data,
|
||||
decorations,
|
||||
document,
|
||||
selection,
|
||||
...attributes,
|
||||
})
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Point classes that can be created at different points in the document and
|
||||
* then searched for afterwards, for creating ranges.
|
||||
*
|
||||
* @type {Class}
|
||||
*/
|
||||
|
||||
class CursorPoint {
|
||||
constructor() {
|
||||
this.offset = null
|
||||
}
|
||||
}
|
||||
|
||||
class AnchorPoint {
|
||||
constructor(attrs = {}) {
|
||||
const { key = null, offset = null, path = null } = attrs
|
||||
this.key = key
|
||||
this.offset = offset
|
||||
this.path = path
|
||||
}
|
||||
}
|
||||
|
||||
class FocusPoint {
|
||||
constructor(attrs = {}) {
|
||||
const { key = null, offset = null, path = null } = attrs
|
||||
this.key = key
|
||||
this.offset = offset
|
||||
this.path = path
|
||||
}
|
||||
}
|
||||
|
||||
class DecorationPoint {
|
||||
constructor(attrs) {
|
||||
const { id = null, data = {}, type } = attrs
|
||||
this.id = id
|
||||
this.offset = null
|
||||
this.type = type
|
||||
this.data = data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment any existing `point` on object by `n`.
|
||||
*
|
||||
* @param {Any} object
|
||||
* @param {Number} n
|
||||
*/
|
||||
|
||||
function incrementPoint(object, n) {
|
||||
const { __anchor, __focus, __decorations } = object
|
||||
|
||||
if (__anchor != null) {
|
||||
__anchor.offset += n
|
||||
}
|
||||
|
||||
if (__focus != null && __focus !== __anchor) {
|
||||
__focus.offset += n
|
||||
}
|
||||
|
||||
if (__decorations != null) {
|
||||
__decorations.forEach(d => (d.offset += n))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an `object` is a point.
|
||||
*
|
||||
* @param {Any} object
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
function isPoint(object) {
|
||||
return (
|
||||
object instanceof AnchorPoint ||
|
||||
object instanceof CursorPoint ||
|
||||
object instanceof DecorationPoint ||
|
||||
object instanceof FocusPoint
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserve any point information on an object.
|
||||
*
|
||||
* @param {Any} object
|
||||
* @param {Function} updator
|
||||
* @return {Any}
|
||||
*/
|
||||
|
||||
function preservePoint(object, updator) {
|
||||
const { __anchor, __focus, __decorations } = object
|
||||
const next = updator(object)
|
||||
if (__anchor != null) next.__anchor = __anchor
|
||||
if (__focus != null) next.__focus = __focus
|
||||
if (__decorations != null) next.__decorations = __decorations
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a `point` on an `object`.
|
||||
*
|
||||
* @param {Any} object
|
||||
* @param {*Point} point
|
||||
* @param {Number} offset
|
||||
*/
|
||||
|
||||
function setPoint(object, point, offset) {
|
||||
if (point instanceof AnchorPoint || point instanceof CursorPoint) {
|
||||
point.offset = offset
|
||||
object.__anchor = point
|
||||
}
|
||||
|
||||
if (point instanceof FocusPoint || point instanceof CursorPoint) {
|
||||
point.offset = offset
|
||||
object.__focus = point
|
||||
}
|
||||
|
||||
if (point instanceof DecorationPoint) {
|
||||
point.offset = offset
|
||||
object.__decorations = object.__decorations || []
|
||||
object.__decorations = object.__decorations.concat(point)
|
||||
}
|
||||
}
|
@@ -1,286 +1,19 @@
|
||||
import isPlainObject from 'is-plain-object'
|
||||
|
||||
import {
|
||||
Block,
|
||||
Decoration,
|
||||
Document,
|
||||
Inline,
|
||||
Mark,
|
||||
Node,
|
||||
Point,
|
||||
Selection,
|
||||
Text,
|
||||
Value,
|
||||
} from 'slate'
|
||||
|
||||
/**
|
||||
* Point classes that can be created at different points in the document and
|
||||
* then searched for afterwards, for creating ranges.
|
||||
*
|
||||
* @type {Class}
|
||||
*/
|
||||
|
||||
class CursorPoint {
|
||||
constructor() {
|
||||
this.offset = null
|
||||
}
|
||||
}
|
||||
|
||||
class AnchorPoint {
|
||||
constructor(attrs = {}) {
|
||||
const { key = null, offset = null, path = null } = attrs
|
||||
this.key = key
|
||||
this.offset = offset
|
||||
this.path = path
|
||||
}
|
||||
}
|
||||
|
||||
class FocusPoint {
|
||||
constructor(attrs = {}) {
|
||||
const { key = null, offset = null, path = null } = attrs
|
||||
this.key = key
|
||||
this.offset = offset
|
||||
this.path = path
|
||||
}
|
||||
}
|
||||
|
||||
class DecorationPoint {
|
||||
constructor(attrs) {
|
||||
const { key = null, data = {}, type } = attrs
|
||||
this.id = key
|
||||
this.offset = 0
|
||||
this.type = type
|
||||
this.data = data
|
||||
}
|
||||
|
||||
combine = focus => {
|
||||
if (!(focus instanceof DecorationPoint)) {
|
||||
throw new Error('misaligned decorations')
|
||||
}
|
||||
|
||||
return Decoration.create({
|
||||
anchor: {
|
||||
key: this.key,
|
||||
offset: this.offset,
|
||||
},
|
||||
focus: {
|
||||
key: focus.key,
|
||||
offset: focus.offset,
|
||||
},
|
||||
mark: {
|
||||
type: this.type,
|
||||
data: this.data,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default Slate hyperscript creator functions.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
const CREATORS = {
|
||||
anchor(tagName, attributes, children) {
|
||||
return new AnchorPoint(attributes)
|
||||
},
|
||||
|
||||
block(tagName, attributes, children) {
|
||||
return Block.create({
|
||||
...attributes,
|
||||
nodes: createChildren(children),
|
||||
})
|
||||
},
|
||||
|
||||
cursor(tagName, attributes, children) {
|
||||
return new CursorPoint()
|
||||
},
|
||||
|
||||
decoration(tagName, attributes, children) {
|
||||
const { key, data } = attributes
|
||||
const type = tagName
|
||||
|
||||
if (key) {
|
||||
return new DecorationPoint({ key, type, data })
|
||||
}
|
||||
|
||||
const nodes = createChildren(children)
|
||||
const node = nodes[0]
|
||||
const { __decorations = [] } = node
|
||||
const __decoration = {
|
||||
anchorOffset: 0,
|
||||
focusOffset: nodes.reduce((len, n) => len + n.text.length, 0),
|
||||
type,
|
||||
data,
|
||||
}
|
||||
|
||||
__decorations.push(__decoration)
|
||||
node.__decorations = __decorations
|
||||
return nodes
|
||||
},
|
||||
|
||||
document(tagName, attributes, children) {
|
||||
return Document.create({
|
||||
...attributes,
|
||||
nodes: createChildren(children),
|
||||
})
|
||||
},
|
||||
|
||||
focus(tagName, attributes, children) {
|
||||
return new FocusPoint(attributes)
|
||||
},
|
||||
|
||||
inline(tagName, attributes, children) {
|
||||
return Inline.create({
|
||||
...attributes,
|
||||
nodes: createChildren(children),
|
||||
})
|
||||
},
|
||||
|
||||
mark(tagName, attributes, children) {
|
||||
const marks = Mark.createSet([attributes])
|
||||
const nodes = createChildren(children, { marks })
|
||||
return nodes
|
||||
},
|
||||
|
||||
selection(tagName, attributes, children) {
|
||||
const anchor = children.find(c => c instanceof AnchorPoint)
|
||||
const focus = children.find(c => c instanceof FocusPoint)
|
||||
const { marks, focused } = attributes
|
||||
const selection = Selection.create({
|
||||
marks,
|
||||
isFocused: focused,
|
||||
anchor: anchor && {
|
||||
key: anchor.key,
|
||||
offset: anchor.offset,
|
||||
path: anchor.path,
|
||||
},
|
||||
focus: focus && {
|
||||
key: focus.key,
|
||||
offset: focus.offset,
|
||||
path: focus.path,
|
||||
},
|
||||
})
|
||||
|
||||
return selection
|
||||
},
|
||||
|
||||
text(tagName, attributes, children) {
|
||||
const nodes = createChildren(children, { key: attributes.key })
|
||||
return nodes
|
||||
},
|
||||
|
||||
value(tagName, attributes, children) {
|
||||
const { data, normalize = true } = attributes
|
||||
const document = children.find(Document.isDocument)
|
||||
let selection = children.find(Selection.isSelection) || Selection.create()
|
||||
let anchor
|
||||
let focus
|
||||
let decorations = []
|
||||
const partials = {}
|
||||
|
||||
// Search the document's texts to see if any of them have the anchor or
|
||||
// focus information saved, or decorations applied.
|
||||
if (document) {
|
||||
document.getTexts().forEach(text => {
|
||||
if (text.__anchor != null) {
|
||||
anchor = Point.create({ key: text.key, offset: text.__anchor.offset })
|
||||
}
|
||||
|
||||
if (text.__focus != null) {
|
||||
focus = Point.create({ key: text.key, offset: text.__focus.offset })
|
||||
}
|
||||
|
||||
if (text.__decorations != null) {
|
||||
text.__decorations.forEach(dec => {
|
||||
const { id } = dec
|
||||
let range
|
||||
|
||||
if (!id) {
|
||||
range = Decoration.create({
|
||||
anchor: {
|
||||
key: text.key,
|
||||
offset: dec.anchorOffset,
|
||||
},
|
||||
focus: {
|
||||
key: text.key,
|
||||
offset: dec.focusOffset,
|
||||
},
|
||||
mark: {
|
||||
type: dec.type,
|
||||
data: dec.data,
|
||||
},
|
||||
})
|
||||
} else if (partials[id]) {
|
||||
const partial = partials[id]
|
||||
delete partials[id]
|
||||
|
||||
range = Decoration.create({
|
||||
anchor: {
|
||||
key: partial.key,
|
||||
offset: partial.offset,
|
||||
},
|
||||
focus: {
|
||||
key: text.key,
|
||||
offset: dec.offset,
|
||||
},
|
||||
mark: {
|
||||
type: dec.type,
|
||||
data: dec.data,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
dec.key = text.key
|
||||
partials[id] = dec
|
||||
}
|
||||
|
||||
if (range) {
|
||||
decorations.push(range)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (Object.keys(partials).length > 0) {
|
||||
throw new Error(
|
||||
`Slate hyperscript must have both a start and an end defined for each decoration using the \`key=\` prop.`
|
||||
)
|
||||
}
|
||||
|
||||
if (anchor && !focus) {
|
||||
throw new Error(
|
||||
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<anchor />\`. For collapsed selections, use \`<cursor />\` instead.`
|
||||
)
|
||||
}
|
||||
|
||||
if (!anchor && focus) {
|
||||
throw new Error(
|
||||
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<focus />\`. For collapsed selections, use \`<cursor />\` instead.`
|
||||
)
|
||||
}
|
||||
|
||||
let value = Value.fromJSON(
|
||||
{ data, document, selection, ...attributes },
|
||||
{ normalize }
|
||||
)
|
||||
|
||||
if (anchor || focus) {
|
||||
selection = selection.setPoints([anchor, focus])
|
||||
selection = selection.setIsFocused(true)
|
||||
selection = selection.normalize(value.document)
|
||||
value = value.set('selection', selection)
|
||||
}
|
||||
|
||||
if (decorations.length > 0) {
|
||||
decorations = decorations.map(d => d.normalize(value.document))
|
||||
decorations = Decoration.createList(decorations)
|
||||
value = value.set('decorations', decorations)
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
}
|
||||
createAnchor,
|
||||
createBlock,
|
||||
createCursor,
|
||||
createDecoration,
|
||||
createDocument,
|
||||
createFocus,
|
||||
createInline,
|
||||
createMark,
|
||||
createNode,
|
||||
createSelection,
|
||||
createText,
|
||||
createValue,
|
||||
} from './creators'
|
||||
|
||||
/**
|
||||
* Create a Slate hyperscript function with `options`.
|
||||
@@ -290,7 +23,39 @@ const CREATORS = {
|
||||
*/
|
||||
|
||||
function createHyperscript(options = {}) {
|
||||
const creators = resolveCreators(options)
|
||||
const { blocks = {}, inlines = {}, marks = {}, decorations = {} } = options
|
||||
|
||||
const creators = {
|
||||
anchor: createAnchor,
|
||||
block: createBlock,
|
||||
cursor: createCursor,
|
||||
decoration: createDecoration,
|
||||
document: createDocument,
|
||||
focus: createFocus,
|
||||
inline: createInline,
|
||||
mark: createMark,
|
||||
node: createNode,
|
||||
selection: createSelection,
|
||||
text: createText,
|
||||
value: createValue,
|
||||
...(options.creators || {}),
|
||||
}
|
||||
|
||||
for (const key in blocks) {
|
||||
creators[key] = normalizeCreator(blocks[key], createBlock)
|
||||
}
|
||||
|
||||
for (const key in inlines) {
|
||||
creators[key] = normalizeCreator(inlines[key], createInline)
|
||||
}
|
||||
|
||||
for (const key in marks) {
|
||||
creators[key] = normalizeCreator(marks[key], createMark)
|
||||
}
|
||||
|
||||
for (const key in decorations) {
|
||||
creators[key] = normalizeCreator(decorations[key], createDecoration)
|
||||
}
|
||||
|
||||
function create(tagName, attributes, ...children) {
|
||||
const creator = creators[tagName]
|
||||
@@ -312,195 +77,22 @@ function createHyperscript(options = {}) {
|
||||
.filter(child => Boolean(child))
|
||||
.reduce((memo, child) => memo.concat(child), [])
|
||||
|
||||
const element = creator(tagName, attributes, children)
|
||||
return element
|
||||
const ret = creator(tagName, attributes, children)
|
||||
return ret
|
||||
}
|
||||
|
||||
return create
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of `children`, storing selection anchor and focus.
|
||||
*
|
||||
* @param {Array} children
|
||||
* @param {Object} options
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
function createChildren(children, options = {}) {
|
||||
const array = []
|
||||
let length = 0
|
||||
|
||||
// When creating the new node, try to preserve a key if one exists.
|
||||
const firstNodeOrText = children.find(c => typeof c !== 'string')
|
||||
const firstText = Text.isText(firstNodeOrText) ? firstNodeOrText : null
|
||||
const key = options.key ? options.key : firstText ? firstText.key : undefined
|
||||
let node = Text.create({ key, leaves: [{ text: '', marks: options.marks }] })
|
||||
|
||||
// Create a helper to update the current node while preserving any stored
|
||||
// anchor or focus information.
|
||||
function setNode(next) {
|
||||
const { __anchor, __focus, __decorations } = node
|
||||
if (__anchor != null) next.__anchor = __anchor
|
||||
if (__focus != null) next.__focus = __focus
|
||||
if (__decorations != null) next.__decorations = __decorations
|
||||
node = next
|
||||
}
|
||||
|
||||
children.forEach((child, index) => {
|
||||
const isLast = index === children.length - 1
|
||||
|
||||
// If the child is a non-text node, push the current node and the new child
|
||||
// onto the array, then creating a new node for future selection tracking.
|
||||
if (Node.isNode(child) && !Text.isText(child)) {
|
||||
if (
|
||||
node.text.length ||
|
||||
node.__anchor != null ||
|
||||
node.__focus != null ||
|
||||
node.getMarksAtIndex(0).size
|
||||
) {
|
||||
array.push(node)
|
||||
}
|
||||
|
||||
array.push(child)
|
||||
|
||||
node = isLast
|
||||
? null
|
||||
: Text.create({ leaves: [{ text: '', marks: options.marks }] })
|
||||
|
||||
length = 0
|
||||
}
|
||||
|
||||
// If the child is a string insert it into the node.
|
||||
if (typeof child == 'string') {
|
||||
setNode(node.insertText(node.text.length, child, options.marks))
|
||||
length += child.length
|
||||
}
|
||||
|
||||
// If the node is a `Text` add its text and marks to the existing node. If
|
||||
// the existing node is empty, and the `key` option wasn't set, preserve the
|
||||
// child's key when updating the node.
|
||||
if (Text.isText(child)) {
|
||||
const { __anchor, __focus, __decorations } = child
|
||||
let i = node.text.length
|
||||
|
||||
if (!options.key && node.text.length == 0) {
|
||||
setNode(node.set('key', child.key))
|
||||
}
|
||||
|
||||
child.getLeaves().forEach(leaf => {
|
||||
let { marks } = leaf
|
||||
if (options.marks) marks = marks.union(options.marks)
|
||||
setNode(node.insertText(i, leaf.text, marks))
|
||||
i += leaf.text.length
|
||||
})
|
||||
|
||||
if (__anchor != null) {
|
||||
node.__anchor = new AnchorPoint()
|
||||
node.__anchor.offset = __anchor.offset + length
|
||||
}
|
||||
|
||||
if (__focus != null) {
|
||||
node.__focus = new FocusPoint()
|
||||
node.__focus.offset = __focus.offset + length
|
||||
}
|
||||
|
||||
if (__decorations != null) {
|
||||
__decorations.forEach(d => {
|
||||
if (d instanceof DecorationPoint) {
|
||||
d.offset += length
|
||||
} else {
|
||||
d.anchorOffset += length
|
||||
d.focusOffset += length
|
||||
}
|
||||
})
|
||||
|
||||
node.__decorations = node.__decorations || []
|
||||
node.__decorations = node.__decorations.concat(__decorations)
|
||||
}
|
||||
|
||||
length += child.text.length
|
||||
}
|
||||
|
||||
if (child instanceof AnchorPoint || child instanceof CursorPoint) {
|
||||
child.offset = length
|
||||
node.__anchor = child
|
||||
}
|
||||
|
||||
if (child instanceof FocusPoint || child instanceof CursorPoint) {
|
||||
child.offset = length
|
||||
node.__focus = child
|
||||
}
|
||||
|
||||
if (child instanceof DecorationPoint) {
|
||||
child.offset = length
|
||||
node.__decorations = node.__decorations || []
|
||||
node.__decorations = node.__decorations.concat(child)
|
||||
}
|
||||
})
|
||||
|
||||
// Make sure the most recent node is added.
|
||||
if (node != null) {
|
||||
array.push(node)
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a set of hyperscript creators an `options` object.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function resolveCreators(options) {
|
||||
const {
|
||||
blocks = {},
|
||||
inlines = {},
|
||||
marks = {},
|
||||
decorations = {},
|
||||
schema,
|
||||
} = options
|
||||
|
||||
const creators = {
|
||||
...CREATORS,
|
||||
...(options.creators || {}),
|
||||
}
|
||||
|
||||
Object.keys(blocks).map(key => {
|
||||
creators[key] = normalizeNode(blocks[key], 'block')
|
||||
})
|
||||
|
||||
Object.keys(inlines).map(key => {
|
||||
creators[key] = normalizeNode(inlines[key], 'inline')
|
||||
})
|
||||
|
||||
Object.keys(marks).map(key => {
|
||||
creators[key] = normalizeMark(marks[key])
|
||||
})
|
||||
|
||||
Object.keys(decorations).map(key => {
|
||||
creators[key] = normalizeNode(decorations[key], 'decoration')
|
||||
})
|
||||
|
||||
creators.value = (tagName, attributes = {}, children) => {
|
||||
const attrs = { schema, ...attributes }
|
||||
return CREATORS.value(tagName, attrs, children)
|
||||
}
|
||||
|
||||
return creators
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a node creator of `value` and `object`.
|
||||
* Normalize a `creator` of `value`.
|
||||
*
|
||||
* @param {Function|Object|String} value
|
||||
* @param {String} object
|
||||
* @param {Function} creator
|
||||
* @return {Function}
|
||||
*/
|
||||
|
||||
function normalizeNode(value, object) {
|
||||
function normalizeCreator(value, creator) {
|
||||
if (typeof value == 'function') {
|
||||
return value
|
||||
}
|
||||
@@ -514,7 +106,6 @@ function normalizeNode(value, object) {
|
||||
const { key, ...rest } = attributes
|
||||
const attrs = {
|
||||
...value,
|
||||
object,
|
||||
key,
|
||||
data: {
|
||||
...(value.data || {}),
|
||||
@@ -522,47 +113,12 @@ function normalizeNode(value, object) {
|
||||
},
|
||||
}
|
||||
|
||||
return CREATORS[object](tagName, attrs, children)
|
||||
return creator(tagName, attrs, children)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Slate hyperscript ${object} creators can be either functions, objects or strings, but you passed: ${value}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a mark creator of `value`.
|
||||
*
|
||||
* @param {Function|Object|String} value
|
||||
* @return {Function}
|
||||
*/
|
||||
|
||||
function normalizeMark(value) {
|
||||
if (typeof value == 'function') {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value == 'string') {
|
||||
value = { type: value }
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
return (tagName, attributes, children) => {
|
||||
const attrs = {
|
||||
...value,
|
||||
data: {
|
||||
...(value.data || {}),
|
||||
...attributes,
|
||||
},
|
||||
}
|
||||
|
||||
return CREATORS.mark(tagName, attrs, children)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Slate hyperscript mark creators can be either functions, objects or strings, but you passed: ${value}`
|
||||
`Slate hyperscript creators can be either functions, objects or strings, but you passed: ${value}`
|
||||
)
|
||||
}
|
||||
|
||||
|
12
packages/slate-hyperscript/test/fixtures/block-empty.js
vendored
Normal file
12
packages/slate-hyperscript/test/fixtures/block-empty.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = <block type="paragraph" />
|
||||
|
||||
export const output = {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [],
|
||||
}
|
23
packages/slate-hyperscript/test/fixtures/block-inline-empty.js
vendored
Normal file
23
packages/slate-hyperscript/test/fixtures/block-inline-empty.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = (
|
||||
<block type="paragraph">
|
||||
<inline type="link" />
|
||||
</block>
|
||||
)
|
||||
|
||||
export const output = {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'link',
|
||||
data: {},
|
||||
nodes: [],
|
||||
},
|
||||
],
|
||||
}
|
@@ -3,18 +3,19 @@
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = (
|
||||
<document>
|
||||
<block type="paragraph">word</block>
|
||||
</document>
|
||||
<block type="paragraph">
|
||||
<inline type="link">word</inline>
|
||||
</block>
|
||||
)
|
||||
|
||||
export const output = {
|
||||
object: 'document',
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
object: 'inline',
|
||||
type: 'link',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
33
packages/slate-hyperscript/test/fixtures/block-mark-empty.js
vendored
Normal file
33
packages/slate-hyperscript/test/fixtures/block-mark-empty.js
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = (
|
||||
<block type="paragraph">
|
||||
<mark type="bold" />
|
||||
</block>
|
||||
)
|
||||
|
||||
export const output = {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: '',
|
||||
marks: [
|
||||
{
|
||||
object: 'mark',
|
||||
type: 'bold',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
33
packages/slate-hyperscript/test/fixtures/block-mark-full.js
vendored
Normal file
33
packages/slate-hyperscript/test/fixtures/block-mark-full.js
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = (
|
||||
<block type="paragraph">
|
||||
<mark type="bold">word</mark>
|
||||
</block>
|
||||
)
|
||||
|
||||
export const output = {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'word',
|
||||
marks: [
|
||||
{
|
||||
object: 'mark',
|
||||
type: 'bold',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
62
packages/slate-hyperscript/test/fixtures/block-mark-nested.js
vendored
Normal file
62
packages/slate-hyperscript/test/fixtures/block-mark-nested.js
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = (
|
||||
<block type="paragraph">
|
||||
<mark type="bold">
|
||||
w<mark type="italic">or</mark>d
|
||||
</mark>
|
||||
</block>
|
||||
)
|
||||
|
||||
export const output = {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'w',
|
||||
marks: [
|
||||
{
|
||||
object: 'mark',
|
||||
type: 'bold',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'or',
|
||||
marks: [
|
||||
{
|
||||
object: 'mark',
|
||||
type: 'italic',
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
object: 'mark',
|
||||
type: 'bold',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'd',
|
||||
marks: [
|
||||
{
|
||||
object: 'mark',
|
||||
type: 'bold',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
23
packages/slate-hyperscript/test/fixtures/block-string.js
vendored
Normal file
23
packages/slate-hyperscript/test/fixtures/block-string.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = <block type="paragraph">word</block>
|
||||
|
||||
export const output = {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'word',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
27
packages/slate-hyperscript/test/fixtures/block-text-empty.js
vendored
Normal file
27
packages/slate-hyperscript/test/fixtures/block-text-empty.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = (
|
||||
<block type="paragraph">
|
||||
<text />
|
||||
</block>
|
||||
)
|
||||
|
||||
export const output = {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: '',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
27
packages/slate-hyperscript/test/fixtures/block-text-full.js
vendored
Normal file
27
packages/slate-hyperscript/test/fixtures/block-text-full.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = (
|
||||
<block type="paragraph">
|
||||
<text>word</text>
|
||||
</block>
|
||||
)
|
||||
|
||||
export const output = {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'word',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
@@ -21,7 +21,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '3',
|
||||
key: '2',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
|
@@ -6,14 +6,18 @@ export const input = (
|
||||
<value>
|
||||
<document>
|
||||
<block type="paragraph">
|
||||
<text />
|
||||
<inline type="link">
|
||||
on<anchor />e
|
||||
</inline>
|
||||
<text />
|
||||
</block>
|
||||
<block type="paragraph">
|
||||
<text />
|
||||
<inline type="link">
|
||||
t<focus />wo
|
||||
</inline>
|
||||
<text />
|
||||
</block>
|
||||
</document>
|
||||
</value>
|
||||
@@ -33,13 +37,13 @@ export const output = {
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
key: '3',
|
||||
key: '4',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
key: '13',
|
||||
key: '0',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
@@ -50,13 +54,13 @@ export const output = {
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
key: '1',
|
||||
key: '2',
|
||||
type: 'link',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
key: '0',
|
||||
key: '1',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
@@ -69,7 +73,7 @@ export const output = {
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
key: '14',
|
||||
key: '3',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
@@ -82,13 +86,13 @@ export const output = {
|
||||
},
|
||||
{
|
||||
object: 'block',
|
||||
key: '7',
|
||||
key: '9',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
key: '11',
|
||||
key: '5',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
@@ -99,13 +103,13 @@ export const output = {
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
key: '5',
|
||||
key: '7',
|
||||
type: 'link',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
key: '4',
|
||||
key: '6',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
@@ -118,7 +122,7 @@ export const output = {
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
key: '12',
|
||||
key: '8',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
@@ -135,13 +139,13 @@ export const output = {
|
||||
object: 'selection',
|
||||
anchor: {
|
||||
object: 'point',
|
||||
key: '0',
|
||||
key: '1',
|
||||
path: [0, 1, 0],
|
||||
offset: 2,
|
||||
},
|
||||
focus: {
|
||||
object: 'point',
|
||||
key: '4',
|
||||
key: '6',
|
||||
path: [1, 1, 0],
|
||||
offset: 1,
|
||||
},
|
||||
|
@@ -24,7 +24,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '6',
|
||||
key: '4',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
|
@@ -24,7 +24,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '6',
|
||||
key: '4',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
|
@@ -24,7 +24,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '6',
|
||||
key: '4',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
|
@@ -25,7 +25,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '9',
|
||||
key: '6',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
|
@@ -25,7 +25,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '9',
|
||||
key: '6',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
|
@@ -25,7 +25,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '9',
|
||||
key: '6',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
|
@@ -21,7 +21,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '3',
|
||||
key: '2',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
|
@@ -21,7 +21,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '3',
|
||||
key: '2',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
|
@@ -21,7 +21,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '3',
|
||||
key: '2',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
|
@@ -27,7 +27,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '3',
|
||||
key: '2',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
|
@@ -26,7 +26,7 @@ export const output = {
|
||||
document: {
|
||||
object: 'document',
|
||||
data: {},
|
||||
key: '6',
|
||||
key: '5',
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
|
@@ -26,7 +26,7 @@ export const output = {
|
||||
document: {
|
||||
object: 'document',
|
||||
data: {},
|
||||
key: '6',
|
||||
key: '5',
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
|
@@ -26,7 +26,7 @@ export const output = {
|
||||
document: {
|
||||
object: 'document',
|
||||
data: {},
|
||||
key: '6',
|
||||
key: '5',
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
|
@@ -26,7 +26,7 @@ export const output = {
|
||||
document: {
|
||||
object: 'document',
|
||||
data: {},
|
||||
key: '3',
|
||||
key: '2',
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
|
@@ -26,7 +26,7 @@ export const output = {
|
||||
document: {
|
||||
object: 'document',
|
||||
data: {},
|
||||
key: '3',
|
||||
key: '2',
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
|
@@ -26,7 +26,7 @@ export const output = {
|
||||
document: {
|
||||
object: 'document',
|
||||
data: {},
|
||||
key: '3',
|
||||
key: '2',
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
|
@@ -101,18 +101,7 @@ export const output = {
|
||||
data: {
|
||||
src: 'https://...',
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: '',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
nodes: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@@ -30,7 +30,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '3',
|
||||
key: '2',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
@@ -33,7 +33,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '6',
|
||||
key: '4',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
11
packages/slate-hyperscript/test/fixtures/document-empty.js
vendored
Normal file
11
packages/slate-hyperscript/test/fixtures/document-empty.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = <document />
|
||||
|
||||
export const output = {
|
||||
object: 'document',
|
||||
data: {},
|
||||
nodes: [],
|
||||
}
|
12
packages/slate-hyperscript/test/fixtures/inline-empty.js
vendored
Normal file
12
packages/slate-hyperscript/test/fixtures/inline-empty.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = <inline type="link" />
|
||||
|
||||
export const output = {
|
||||
object: 'inline',
|
||||
type: 'link',
|
||||
data: {},
|
||||
nodes: [],
|
||||
}
|
23
packages/slate-hyperscript/test/fixtures/inline-full.js
vendored
Normal file
23
packages/slate-hyperscript/test/fixtures/inline-full.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = <inline type="link">word</inline>
|
||||
|
||||
export const output = {
|
||||
object: 'inline',
|
||||
type: 'link',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'word',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
@@ -1,41 +0,0 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = (
|
||||
<document>
|
||||
<block type="paragraph">
|
||||
<mark type="bold" />
|
||||
</block>
|
||||
</document>
|
||||
)
|
||||
|
||||
export const output = {
|
||||
object: 'document',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: '',
|
||||
marks: [
|
||||
{
|
||||
type: 'bold',
|
||||
object: 'mark',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = (
|
||||
<value>
|
||||
<document>
|
||||
<block type="paragraph">word</block>
|
||||
<text>invalid</text>
|
||||
</document>
|
||||
</value>
|
||||
)
|
||||
|
||||
export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'word',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = (
|
||||
<value normalize={false}>
|
||||
<document>
|
||||
<block type="paragraph">word</block>
|
||||
<text>invalid</text>
|
||||
</document>
|
||||
</value>
|
||||
)
|
||||
|
||||
export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'word',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'invalid',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
@@ -6,7 +6,7 @@ export const input = (
|
||||
<value>
|
||||
<document>
|
||||
<block type="paragraph">
|
||||
one<text key="a">two</text>three
|
||||
<text key="a">two</text>
|
||||
</block>
|
||||
</document>
|
||||
<selection>
|
||||
@@ -25,7 +25,7 @@ export const output = {
|
||||
object: 'value',
|
||||
document: {
|
||||
object: 'document',
|
||||
key: '2',
|
||||
key: '1',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
@@ -40,7 +40,7 @@ export const output = {
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'onetwothree',
|
||||
text: 'two',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
|
16
packages/slate-hyperscript/test/fixtures/text-empty.js
vendored
Normal file
16
packages/slate-hyperscript/test/fixtures/text-empty.js
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = <text />
|
||||
|
||||
export const output = {
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: '',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
}
|
16
packages/slate-hyperscript/test/fixtures/text-full.js
vendored
Normal file
16
packages/slate-hyperscript/test/fixtures/text-full.js
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/** @jsx h */
|
||||
|
||||
import h from 'slate-hyperscript'
|
||||
|
||||
export const input = <text>word</text>
|
||||
|
||||
export const output = {
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: 'word',
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user