mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-20 14:11:35 +02:00
remove marks, in favor of text properties (#3235)
* remove marks, in favor of text properties * fix lint * fix more examples * update docs
This commit is contained in:
@@ -5,14 +5,13 @@ Slate works with pure JSON objects. All it requires is that those JSON objects c
|
|||||||
```ts
|
```ts
|
||||||
interface Text {
|
interface Text {
|
||||||
text: string
|
text: string
|
||||||
marks: Mark[]
|
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Which means it must have a `text` property with a string of content, and a `marks` property with an array of formatting marks (or an empty array).
|
Which means it must have a `text` property with a string of content.
|
||||||
|
|
||||||
But other custom properties are also allowed, and completely up to you. This lets you tailor your data to your specific domain and use case, without Slate getting in the way.
|
But **any** other custom properties are also allowed, and completely up to you. This lets you tailor your data to your specific domain and use case, adding whatever formatting logic you'd like, without Slate getting in the way.
|
||||||
|
|
||||||
This interface-based approach separates Slate from most other rich-text editors which require you to work with their hand-rolled "model" classes, and makes it much easier to reason about. It also means that it avoids startup time penalties related to "initializing" the data model.
|
This interface-based approach separates Slate from most other rich-text editors which require you to work with their hand-rolled "model" classes, and makes it much easier to reason about. It also means that it avoids startup time penalties related to "initializing" the data model.
|
||||||
|
|
||||||
|
@@ -16,7 +16,6 @@ const editor = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'A line of text!',
|
text: 'A line of text!',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -127,18 +126,17 @@ Text nodes are the lowest-level nodes in the tree, containing the text content o
|
|||||||
```ts
|
```ts
|
||||||
interface Text {
|
interface Text {
|
||||||
text: string
|
text: string
|
||||||
marks: Mark[]
|
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
We'll cover `Mark` objects shortly, but for now you can get an idea for them:
|
For example, a string of bold text:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const text = {
|
const text = {
|
||||||
text: 'A string of bold text',
|
text: 'A string of bold text',
|
||||||
marks: [{ type: 'bold' }],
|
bold: true,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Text nodes too can contain any custom properties you want, although it is rarer to need to do that.
|
Text nodes too can contain any custom properties you want, and that's how you implement custom formatting like **bold**, _italic_, `code`, etc.
|
||||||
|
@@ -20,7 +20,6 @@ const editor = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'A line of text!',
|
text: 'A line of text!',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -107,7 +106,6 @@ const editor = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'A line of text!',
|
text: 'A line of text!',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# Commands
|
# Commands
|
||||||
|
|
||||||
While editing rich-text content, your users will be doing things like inserting text, deleteing text, splitting paragraphs, adding marks, etc. These edits are expressed using two concepts: commands and operations.
|
While editing rich-text content, your users will be doing things like inserting text, deleteing text, splitting paragraphs, adding formatting, etc. These edits are expressed using two concepts: commands and operations.
|
||||||
|
|
||||||
Commands are the high-level actions that represent a specific intent of the user. Their interface is simply:
|
Commands are the high-level actions that represent a specific intent of the user. Their interface is simply:
|
||||||
|
|
||||||
@@ -25,12 +25,11 @@ editor.exec({
|
|||||||
})
|
})
|
||||||
|
|
||||||
editor.exec({
|
editor.exec({
|
||||||
type: 'add_mark',
|
type: 'insert_break',
|
||||||
mark: { type: 'bold' },
|
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
But you can (and will!) also define your own custom commands that model your domain. For example, you might want to define a `wrap_quote` command, or a `insert_image` command depending on what types of content you allow.
|
But you can (and will!) also define your own custom commands that model your domain. For example, you might want to define a `wrap_quote` command, or a `insert_image` command, or a `format_bold` command depending on what types of content you allow.
|
||||||
|
|
||||||
Commands always describe an action to be taken as if the user themselves was performing the action. For that reason, they never need to define a location to perform the command, because they always act on the user's current selection.
|
Commands always describe an action to be taken as if the user themselves was performing the action. For that reason, they never need to define a location to perform the command, because they always act on the user's current selection.
|
||||||
|
|
@@ -1,49 +0,0 @@
|
|||||||
# Formatting: Marks and Decorations
|
|
||||||
|
|
||||||
We've already seen how `Element` objects can be extended with custom properties to add semantic meaning to your rich-text documents. But there are other kinds of formatting too.
|
|
||||||
|
|
||||||
## `Mark`
|
|
||||||
|
|
||||||
Marks are the lowest level of formatting that is applied directly to the text nodes of your document, for things like **bold**, _italic_ and `code`. Their interface is:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
interface Mark {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Which means that they are entirely composed of your domain-specific custom properties. For simple cases, it can suffice to use a `type` string:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const bold = { type: 'bold' }
|
|
||||||
const italic = { type: 'italic' }
|
|
||||||
```
|
|
||||||
|
|
||||||
There are multiple techniques you might choose to format or style text. You can implement styling based on inlines or marks. Unlike inlines, marks do not affect the structure of the nodes in the document. Marks simply attach themselves to the characters.
|
|
||||||
|
|
||||||
Marks may be easier to reason about and manipulate because marks do not affect the structure of the document and are associated to the characters. Marks can be applied to characters no matter how the characters are nested in the document. If you can express it as a `Range`, you can add marks to it. Working with marks instead of inlines does not require you to edit the document's structure, split existing nodes, determine where nodes are in the hierarchy, or other more complex interactions.
|
|
||||||
|
|
||||||
When marks are rendered, the characters are grouped into "leaves" of text that each contain the same set of marks applied to them. One disadvantage of marks is that you cannot guarantee how a set of marks will be ordered.
|
|
||||||
|
|
||||||
This limitation with respect to the ordering of marks is similar to the DOM, where this is invalid:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<em>t<strong>e</em>x</strong>t
|
|
||||||
```
|
|
||||||
|
|
||||||
Because the elements in the above example do not properly close themselves they are invalid. Instead, you would write the above HTML as follows:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<em>t</em><strong><em>e</em>x</strong>t
|
|
||||||
```
|
|
||||||
|
|
||||||
If you happened to add another overlapping section of `<strike>` to that text, you might have to rearrange the closing tags yet again. Rendering marks in Slate is similar—you can't guarantee that even though a word has one mark applied that that mark will be contiguous, because it depends on how it overlaps with other marks.
|
|
||||||
|
|
||||||
Of course, this mark ordering stuff sounds pretty complex. But, you do not have to think about it much, as long as you use marks and inlines for their intended purposes:
|
|
||||||
|
|
||||||
- Marks represent **unordered**, character-level formatting.
|
|
||||||
- Inlines represent **contiguous**, semantic elements in the document.
|
|
||||||
|
|
||||||
## Decorations
|
|
||||||
|
|
||||||
Decorations are similar to marks, with each one applying to a range of content. However, they are computed at render-time based on the content itself. This is helpful for dynamic formatting like syntax highlighting or search keywords, where changes to the content (or some external data) has the potential to change the formatting.
|
|
@@ -17,7 +17,6 @@ editor.apply({
|
|||||||
path: [0, 0],
|
path: [0, 0],
|
||||||
node: {
|
node: {
|
||||||
text: 'A line of text!',
|
text: 'A line of text!',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@@ -95,8 +95,8 @@ for (const [element, path] of Editor.elements(editor, { at: range })) {
|
|||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over every mark in every text node in the current selection.
|
// Iterate over every point in every text node in the current selection.
|
||||||
for (const [mark, index, text, path] of Editor.marks(editor)) {
|
for (const [point] of Editor.positions(editor)) {
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -110,8 +110,8 @@ Editor.insertNodes(editor, [element], { at: path })
|
|||||||
// Split the nodes in half at a specific point.
|
// Split the nodes in half at a specific point.
|
||||||
Editor.splitNodes(editor, { at: point })
|
Editor.splitNodes(editor, { at: point })
|
||||||
|
|
||||||
// Add a mark to all the text in a range.
|
// Add a quote format to all the block nodes in the selection.
|
||||||
Editor.addMarks(editor, mark, { at: range })
|
Editor.setNodes(editor, { type: 'quote' })
|
||||||
```
|
```
|
||||||
|
|
||||||
The editor-specific helpers are the ones you'll use most often when working with Slate editors, so it pays to become very familiar with them.
|
The editor-specific helpers are the ones you'll use most often when working with Slate editors, so it pays to become very familiar with them.
|
@@ -13,8 +13,7 @@ const withImages = editor => {
|
|||||||
editor.exec = command => {
|
editor.exec = command => {
|
||||||
if (command.type === 'insert_image') {
|
if (command.type === 'insert_image') {
|
||||||
const { url } = command
|
const { url } = command
|
||||||
const text = { text: '', marks: [] }
|
const element = { type: 'image', url, children: [{ text: '' }] }
|
||||||
const element = { type: 'image', url, children: [text] }
|
|
||||||
Editor.insertNodes(editor)
|
Editor.insertNodes(editor)
|
||||||
} else {
|
} else {
|
||||||
exec(command)
|
exec(command)
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
One of the best parts of Slate is that it's built with React, so it fits right into your existing application. It doesn't re-invent its own view layer that you have to learn. It tries to keep everything as React-y as possible.
|
One of the best parts of Slate is that it's built with React, so it fits right into your existing application. It doesn't re-invent its own view layer that you have to learn. It tries to keep everything as React-y as possible.
|
||||||
|
|
||||||
To that end, Slate gives you control over the rendering behavior of your custom elements and custom marks in your rich-text domain.
|
To that end, Slate gives you control over the rendering behavior of your custom nodes and properties in your rich-text domain.
|
||||||
|
|
||||||
You can define these behaviors by passing "render props" to the top-level `<Editable>` component.
|
You can define these behaviors by passing "render props" to the top-level `<Editable>` component.
|
||||||
|
|
||||||
@@ -54,24 +54,58 @@ const renderElement = useCallback(props => {
|
|||||||
}, [])
|
}, [])
|
||||||
```
|
```
|
||||||
|
|
||||||
The same thing goes for a custom `renderMark` prop:
|
## Leaves
|
||||||
|
|
||||||
|
When text-level formatting is rendered, the characters are grouped into "leaves" of text that each contain the same formatting applied to them.
|
||||||
|
|
||||||
|
To customize the rendering of each leaf, you use a custom `renderLeaf` prop:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
const renderMark = useCallback(({ attributes, children, mark }) => {
|
const renderLeaf = useCallback(({ attributes, children, leaf }) => {
|
||||||
switch (mark.type) {
|
return (
|
||||||
case 'bold':
|
<span
|
||||||
return <strong {...attributes}>{children}</strong>
|
{...attributes}
|
||||||
case 'italic':
|
style={{
|
||||||
return <em {...attributes}>{children}</em>
|
fontWeight: leaf.bold ? 'bold' : 'normal',
|
||||||
}
|
fontStyle: leaf.italic ? 'italic' : 'normal',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}, [])
|
}, [])
|
||||||
```
|
```
|
||||||
|
|
||||||
> 🤖 Be aware though that marks aren't guaranteed to be "contiguous". Which means even though a **word** is bolded, it's not guaranteed to render as a single `<strong>` element. If some of its characters are also italic, it might be split up into multiple elements—one `<strong>wo</strong>` and one `<em><strong>rd</strong></em>`.
|
Notice though how we've handled it slightly differently than `renderElement`. Since text formatting texts to be fairly simple, we've opted to ditch the `switch` statement and just toggle on/off a few styles instead. (But there's nothing preventing you from using custom components if you'd like!)
|
||||||
|
|
||||||
|
One disadvantage of text-level formatting is that you cannot guarantee that any given format is "contiguous"—meaning that it stays as a single leaf. This limitation with respect to leaves is similar to the DOM, where this is invalid:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<em>t<strong>e</em>x</strong>t
|
||||||
|
```
|
||||||
|
|
||||||
|
Because the elements in the above example do not properly close themselves they are invalid. Instead, you would write the above HTML as follows:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<em>t</em><strong><em>e</em>x</strong>t
|
||||||
|
```
|
||||||
|
|
||||||
|
If you happened to add another overlapping section of `<strike>` to that text, you might have to rearrange the closing tags yet again. Rendering leaves in Slate is similar—you can't guarantee that even though a word has one type of formatting applied to it that that leaf will be contiguous, because it depends on how it overlaps with other formatting.
|
||||||
|
|
||||||
|
Of course, this leaf stuff sounds pretty complex. But, you do not have to think about it much, as long as you use text-level formatting and element-level formatting for their intended purposes:
|
||||||
|
|
||||||
|
- Text properties are for **non-contiguous**, character-level formatting.
|
||||||
|
- Element properties are for **contiguous**, semantic elements in the document.
|
||||||
|
|
||||||
|
## Decorations
|
||||||
|
|
||||||
|
Decorations are another type of text-level formatting. They are similar to regular old custom properties, except each one applies to a `Range` of the document instead of being associated with a given text node.
|
||||||
|
|
||||||
|
However, decorations are computed at **render-time** based on the content itself. This is helpful for dynamic formatting like syntax highlighting or search keywords, where changes to the content (or some external data) has the potential to change the formatting.
|
||||||
|
|
||||||
## Toolbars, Menus, Overlays, and more!
|
## Toolbars, Menus, Overlays, and more!
|
||||||
|
|
||||||
In addition to controlling the rendering of elements and marks inside Slate, you can also retrieve the current editor context from inside other components using the `useSlate` hook.
|
In addition to controlling the rendering of nodes inside Slate, you can also retrieve the current editor context from inside other components using the `useSlate` hook.
|
||||||
|
|
||||||
That way other components can execute commands, query the editor state, or anything else.
|
That way other components can execute commands, query the editor state, or anything else.
|
||||||
|
|
@@ -68,9 +68,13 @@ We now use the `beforeinput` event almost exclusively. Instead of having relying
|
|||||||
|
|
||||||
The core history logic has now finally been extracted into a standalone plugin. This makes it much easier for people to implement their own custom history behaviors. And it ensures that plugins have enough control to augment the editor in complex ways, because the history requires it.
|
The core history logic has now finally been extracted into a standalone plugin. This makes it much easier for people to implement their own custom history behaviors. And it ensures that plugins have enough control to augment the editor in complex ways, because the history requires it.
|
||||||
|
|
||||||
|
### Mark-less
|
||||||
|
|
||||||
|
Marks have been removed the Slate data model. Now that we have the ability to define custom properties right on the nodes themselves, you can model marks as custom properties of text nodes. For example bold can be modelled simply as a `bold: true` property.
|
||||||
|
|
||||||
### Annotation-less
|
### Annotation-less
|
||||||
|
|
||||||
Similarly, annotations have been removed from Slate's core. They can be fully implemented now in userland by defining custom operations and rendering annotated ranges using decorations. But most cases should be using plain marks or plain decorations anyways. There were not that many use cases that benefitted from annotations.
|
Similarly, annotations have been removed from Slate's core. They can be fully implemented now in userland by defining custom operations and rendering annotated ranges using decorations. But most cases should be using custom text node properties or decorations anyways. There were not that many use cases that benefitted from annotations.
|
||||||
|
|
||||||
## Reductions
|
## Reductions
|
||||||
|
|
||||||
@@ -79,23 +83,23 @@ One of the goals was to dramatically simplify a lot of the logic in Slate to mak
|
|||||||
To give you a sense for the change in total lines of code:
|
To give you a sense for the change in total lines of code:
|
||||||
|
|
||||||
```
|
```
|
||||||
slate 8,436 -> 4,038 (48%)
|
slate 8,436 -> 3,724 (48%)
|
||||||
slate-react 3,905 -> 715 (18%)
|
slate-react 3,905 -> 627 (18%)
|
||||||
|
|
||||||
slate-base64-serializer 38 -> 0
|
slate-base64-serializer 38 -> 0
|
||||||
slate-dev-benchmark 340 -> 0
|
slate-dev-benchmark 340 -> 0
|
||||||
slate-dev-environment 102 -> 0
|
slate-dev-environment 102 -> 0
|
||||||
slate-dev-test-utils 44 -> 0
|
slate-dev-test-utils 44 -> 0
|
||||||
slate-history 0 -> 201
|
slate-history 0 -> 209
|
||||||
slate-hotkeys 62 -> 0
|
slate-hotkeys 62 -> 0
|
||||||
slate-html-serializer 253 -> 0
|
slate-html-serializer 253 -> 0
|
||||||
slate-hyperscript 447 -> 410
|
slate-hyperscript 447 -> 345
|
||||||
slate-plain-serializer 56 -> 0
|
slate-plain-serializer 56 -> 0
|
||||||
slate-prop-types 62 -> 0
|
slate-prop-types 62 -> 0
|
||||||
slate-react-placeholder 62 -> 0
|
slate-react-placeholder 62 -> 0
|
||||||
slate-schema 0 -> 504
|
slate-schema 0 -> 439
|
||||||
|
|
||||||
total 13,807 -> 5,868 (43%)
|
total 13,807 -> 5,344 (39%)
|
||||||
```
|
```
|
||||||
|
|
||||||
It's quite a big difference! And that doesn't even include the dependencies that were shed in the process too.
|
It's quite a big difference! And that doesn't even include the dependencies that were shed in the process too.
|
||||||
|
@@ -93,7 +93,6 @@ const defaultValue = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'A line of text in a paragraph.',
|
text: 'A line of text in a paragraph.',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -41,7 +41,7 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
And now, we'll edit the `onKeyDown` handler to make it so that when you press `control-B`, it will add a "bold" mark to the currently selected text:
|
And now, we'll edit the `onKeyDown` handler to make it so that when you press `control-B`, it will add a `bold` format to the currently selected text:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const App = () => {
|
const App = () => {
|
||||||
@@ -78,10 +78,16 @@ const App = () => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// When "B" is pressed, add a bold mark to the text.
|
// When "B" is pressed, bold the text in the selection.
|
||||||
case 'b': {
|
case 'b': {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
Editor.addMarks(editor, { type: 'bold' })
|
Editor.setNodes(
|
||||||
|
editor,
|
||||||
|
{ bold: true },
|
||||||
|
// Apply it to text nodes, and split the text node up if the
|
||||||
|
// selection is overlapping only part of it.
|
||||||
|
{ match: 'text', split: true }
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,18 +100,25 @@ const App = () => {
|
|||||||
|
|
||||||
Okay, so we've got the hotkey handler setup... but! If you happen to now try selecting text and hitting `Ctrl-B`, you won't notice any change. That's because we haven't told Slate how to render a "bold" mark.
|
Okay, so we've got the hotkey handler setup... but! If you happen to now try selecting text and hitting `Ctrl-B`, you won't notice any change. That's because we haven't told Slate how to render a "bold" mark.
|
||||||
|
|
||||||
For every mark type you want to add to your schema, you need to give Slate a "renderer" for that mark, just like elements. So let's define our `bold` mark:
|
For every format you want to add to your schema, Slate will break up the text content into "leaves", and you need to tell Slate how to read it, just like for elements. So let's define a `Leaf` component:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Define a React component to render bold text with.
|
// Define a React component to render leaves with bold text.
|
||||||
const BoldMark = props => {
|
const Leaf = props => {
|
||||||
return <strong {...props.attributes}>{props.children}</strong>
|
return (
|
||||||
|
<span
|
||||||
|
{...props.attributes}
|
||||||
|
style={{ fontWeight: leaf.bold ? 'bold' : 'normal' }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Pretty familiar, right?
|
Pretty familiar, right?
|
||||||
|
|
||||||
And now, let's tell Slate about that mark. To do that, we'll pass in the `renderMark` prop to our editor. Also, let's allow our mark to be toggled by changing `addMark` to `toggleMark`.
|
And now, let's tell Slate about that leaf. To do that, we'll pass in the `renderLeaf` prop to our editor. Also, let's allow our formatting to be toggled by adding active-checking logic.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const App = () => {
|
const App = () => {
|
||||||
@@ -119,21 +132,17 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Define a mark rendering function that is memoized with `useCallback`.
|
// Define a leaf rendering function that is memoized with `useCallback`.
|
||||||
const renderMark = useCallback(props => {
|
const renderLeaf = useCallback(props => {
|
||||||
switch (props.mark.type) {
|
return <Leaf {...props} />
|
||||||
case 'bold': {
|
|
||||||
return <BoldMark {...props} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slate editor={editor} defaultValue={defaultValue}>
|
<Slate editor={editor} defaultValue={defaultValue}>
|
||||||
<Editable
|
<Editable
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
// Pass in the `renderMark` function.
|
// Pass in the `renderLeaf` function.
|
||||||
renderMark={renderMark}
|
renderLeaf={renderLeaf}
|
||||||
onKeyDown={event => {
|
onKeyDown={event => {
|
||||||
if (!event.ctrlKey) {
|
if (!event.ctrlKey) {
|
||||||
return
|
return
|
||||||
@@ -154,7 +163,11 @@ const App = () => {
|
|||||||
|
|
||||||
case 'b': {
|
case 'b': {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
Editor.addMarks(editor, { type: 'bold' })
|
Editor.setNodes(
|
||||||
|
editor,
|
||||||
|
{ bold: true },
|
||||||
|
{ match: 'text', split: true }
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,8 +177,15 @@ const App = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const BoldMark = props => {
|
const Leaf = props => {
|
||||||
return <strong {...props.attributes}>{props.children}</strong>
|
return (
|
||||||
|
<span
|
||||||
|
{...props.attributes}
|
||||||
|
style={{ fontWeight: leaf.bold ? 'bold' : 'normal' }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -22,19 +22,15 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const renderMark = useCallback(props => {
|
const renderLeaf = useCallback(props => {
|
||||||
switch (props.mark.type) {
|
return <Leaf {...props} />
|
||||||
case 'bold': {
|
|
||||||
return <BoldMark {...props} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slate editor={editor} defaultValue={defaultValue}>
|
<Slate editor={editor} defaultValue={defaultValue}>
|
||||||
<Editable
|
<Editable
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderMark={renderMark}
|
renderLeaf={renderLeaf}
|
||||||
onKeyDown={event => {
|
onKeyDown={event => {
|
||||||
if (!event.ctrlKey) {
|
if (!event.ctrlKey) {
|
||||||
return
|
return
|
||||||
@@ -55,7 +51,11 @@ const App = () => {
|
|||||||
|
|
||||||
case 'b': {
|
case 'b': {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
Editor.addMarks(editor, { type: 'bold' })
|
Editor.setNodes(
|
||||||
|
editor,
|
||||||
|
{ bold: true },
|
||||||
|
{ match: 'text', split: true }
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
It has the concept of "code blocks" and "bold marks". But these things are all defined in one-off cases inside the `onKeyDown` handler. If you wanted to reuse that logic elsewhere you'd need to extract it.
|
It has the concept of "code blocks" and "bold formatting". But these things are all defined in one-off cases inside the `onKeyDown` handler. If you wanted to reuse that logic elsewhere you'd need to extract it.
|
||||||
|
|
||||||
We can instead implement these domain-specific concepts by extending the `editor` object:
|
We can instead implement these domain-specific concepts by extending the `editor` object:
|
||||||
|
|
||||||
@@ -88,19 +88,15 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const renderMark = useCallback(props => {
|
const renderLeaf = useCallback(props => {
|
||||||
switch (props.mark.type) {
|
return <Leaf {...props} />
|
||||||
case 'bold': {
|
|
||||||
return <BoldMark {...props} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slate editor={editor} defaultValue={defaultValue}>
|
<Slate editor={editor} defaultValue={defaultValue}>
|
||||||
<Editable
|
<Editable
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderMark={renderMark}
|
renderLeaf={renderLeaf}
|
||||||
onKeyDown={event => {
|
onKeyDown={event => {
|
||||||
if (!event.ctrlKey) {
|
if (!event.ctrlKey) {
|
||||||
return
|
return
|
||||||
@@ -121,7 +117,11 @@ const App = () => {
|
|||||||
|
|
||||||
case 'b': {
|
case 'b': {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
Editor.addMarks(editor, [{ type: 'bold' }])
|
Editor.setNodes(
|
||||||
|
editor,
|
||||||
|
{ bold: true },
|
||||||
|
{ match: 'text', split: true }
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,22 +141,19 @@ const withCustom = editor => {
|
|||||||
const { exec } = editor
|
const { exec } = editor
|
||||||
|
|
||||||
editor.exec = command => {
|
editor.exec = command => {
|
||||||
// Define a command to toggle the bold mark formatting.
|
// Define a command to toggle the bold formatting.
|
||||||
if (command.type === 'toggle_bold_mark') {
|
if (command.type === 'toggle_bold_mark') {
|
||||||
const isActive = CustomEditor.isBoldMarkActive(editor)
|
const isActive = CustomEditor.isBoldMarkActive(editor)
|
||||||
// Delegate to the existing `add_mark` and `remove_mark` commands, so that
|
Editor.setNodes(
|
||||||
// other plugins can override them if they need to still.
|
editor,
|
||||||
editor.exec({
|
{ bold: isActive ? null : true },
|
||||||
type: isActive ? 'remove_mark' : 'add_mark',
|
{ match: 'text', split: true }
|
||||||
mark: { type: 'bold' },
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define a command to toggle the code block formatting.
|
// Define a command to toggle the code block formatting.
|
||||||
else if (command.type === 'toggle_code_block') {
|
else if (command.type === 'toggle_code_block') {
|
||||||
const isActive = CustomEditor.isCodeBlockActive(editor)
|
const isActive = CustomEditor.isCodeBlockActive(editor)
|
||||||
// There is no `set_nodes` command, so we can transform the editor
|
|
||||||
// directly using the helper instead.
|
|
||||||
Editor.setNodes(
|
Editor.setNodes(
|
||||||
editor,
|
editor,
|
||||||
{ type: isActive ? null : 'code' },
|
{ type: isActive ? null : 'code' },
|
||||||
@@ -173,24 +170,24 @@ const withCustom = editor => {
|
|||||||
return editor
|
return editor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define our own custom set of helpers for common queries.
|
// Define our own custom set of helpers for active-checking queries.
|
||||||
const CustomEditor = {
|
const CustomEditor = {
|
||||||
isBoldMarkActive(editor) {
|
isBoldMarkActive(editor) {
|
||||||
const [mark] = Editor.marks(editor, {
|
const [match] = Editor.nodes(editor, {
|
||||||
match: { type: 'bold' },
|
match: { bold: true },
|
||||||
mode: 'universal',
|
mode: 'universal',
|
||||||
})
|
})
|
||||||
|
|
||||||
return !!mark
|
return !!match
|
||||||
},
|
},
|
||||||
|
|
||||||
isCodeBlockActive(editor) {
|
isCodeBlockActive(editor) {
|
||||||
const [node] = Editor.nodes(editor, {
|
const [match] = Editor.nodes(editor, {
|
||||||
match: { type: 'code' },
|
match: { type: 'code' },
|
||||||
mode: 'highest',
|
mode: 'highest',
|
||||||
})
|
})
|
||||||
|
|
||||||
return !!node
|
return !!match
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,19 +202,15 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const renderMark = useCallback(props => {
|
const renderLeaf = useCallback(props => {
|
||||||
switch (props.mark.type) {
|
return <Leaf {...props} />
|
||||||
case 'bold': {
|
|
||||||
return <BoldMark {...props} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slate editor={editor} defaultValue={defaultValue}>
|
<Slate editor={editor} defaultValue={defaultValue}>
|
||||||
<Editable
|
<Editable
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderMark={renderMark}
|
renderLeaf={renderLeaf}
|
||||||
// Replace the `onKeyDown` logic with our new commands.
|
// Replace the `onKeyDown` logic with our new commands.
|
||||||
onKeyDown={event => {
|
onKeyDown={event => {
|
||||||
if (!event.ctrlKey) {
|
if (!event.ctrlKey) {
|
||||||
@@ -258,12 +251,8 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const renderMark = useCallback(props => {
|
const renderLeaf = useCallback(props => {
|
||||||
switch (props.mark.type) {
|
return <Leaf {...props} />
|
||||||
case 'bold': {
|
|
||||||
return <BoldMark {...props} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -290,7 +279,7 @@ const App = () => {
|
|||||||
<Editable
|
<Editable
|
||||||
editor={editor}
|
editor={editor}
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderMark={renderMark}
|
renderLeaf={renderLeaf}
|
||||||
onKeyDown={event => {
|
onKeyDown={event => {
|
||||||
if (!event.ctrlKey) {
|
if (!event.ctrlKey) {
|
||||||
return
|
return
|
||||||
|
@@ -16,7 +16,6 @@ const defaultValue = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'A line of text in a paragraph.',
|
text: 'A line of text in a paragraph.',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -44,7 +43,6 @@ const defaultValue = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'A line of text in a paragraph.',
|
text: 'A line of text in a paragraph.',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -80,7 +78,6 @@ const defaultValue = existingValue || [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'A line of text in a paragraph.',
|
text: 'A line of text in a paragraph.',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -129,7 +126,7 @@ const deserialize = string => {
|
|||||||
// Return a value array of children derived by splitting the string.
|
// Return a value array of children derived by splitting the string.
|
||||||
return string.split('\n').map(line => {
|
return string.split('\n').map(line => {
|
||||||
return {
|
return {
|
||||||
children: [{ text: line, marks: [] }],
|
children: [{ text: line }],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
"fix": "yarn fix:prettier && yarn fix:eslint",
|
"fix": "yarn fix:prettier && yarn fix:eslint",
|
||||||
"fix:eslint": "yarn lint:eslint --fix",
|
"fix:eslint": "yarn lint:eslint --fix",
|
||||||
"fix:prettier": "yarn lint:prettier --write",
|
"fix:prettier": "yarn lint:prettier --write",
|
||||||
"lint": "yarn lint:eslint && yarn lint:prettier",
|
"lint": "yarn lint:eslint",
|
||||||
"lint:eslint": "eslint \"./{packages,site}/**/*.{js,jsx,ts,tsx}\"",
|
"lint:eslint": "eslint \"./{packages,site}/**/*.{js,jsx,ts,tsx}\"",
|
||||||
"lint:prettier": "prettier --list-different \"**/*.{css,md,js,jsx,json,ts,tsx}\"",
|
"lint:prettier": "prettier --list-different \"**/*.{css,md,js,jsx,json,ts,tsx}\"",
|
||||||
"open": "open http://localhost:3000",
|
"open": "open http://localhost:3000",
|
||||||
|
@@ -1,22 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from '../..'
|
|
||||||
|
|
||||||
export const run = editor => {
|
|
||||||
editor.exec({ type: 'add_mark', mark: { key: 'a' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<block>
|
|
||||||
o<anchor />
|
|
||||||
ne
|
|
||||||
</block>
|
|
||||||
<block>
|
|
||||||
tw
|
|
||||||
<focus />o
|
|
||||||
</block>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = input
|
|
@@ -1,20 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from '../..'
|
|
||||||
|
|
||||||
export const run = editor => {
|
|
||||||
editor.exec({ type: 'add_mark', mark: { key: 'a' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<block>
|
|
||||||
<mark key="b">
|
|
||||||
w<anchor />o
|
|
||||||
</mark>
|
|
||||||
r<focus />d
|
|
||||||
</block>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = input
|
|
@@ -1,20 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from '../..'
|
|
||||||
|
|
||||||
export const run = editor => {
|
|
||||||
editor.exec({ type: 'add_mark', mark: { key: 'a' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<block>
|
|
||||||
<mark key="a">
|
|
||||||
w<anchor />o
|
|
||||||
</mark>
|
|
||||||
r<focus />d
|
|
||||||
</block>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = input
|
|
@@ -1,20 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from '../..'
|
|
||||||
|
|
||||||
export const run = editor => {
|
|
||||||
editor.exec({ type: 'add_mark', mark: { key: 'a' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<block>
|
|
||||||
<anchor />
|
|
||||||
wo
|
|
||||||
<focus />
|
|
||||||
rd
|
|
||||||
</block>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = input
|
|
@@ -1,27 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { Editor } from 'slate'
|
|
||||||
import { jsx } from '../..'
|
|
||||||
|
|
||||||
export const run = editor => {
|
|
||||||
editor.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<block>
|
|
||||||
<mark key="a">
|
|
||||||
on
|
|
||||||
<anchor />e
|
|
||||||
</mark>
|
|
||||||
<mark key="c">
|
|
||||||
tw
|
|
||||||
<focus />o
|
|
||||||
</mark>
|
|
||||||
</block>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = input
|
|
||||||
|
|
||||||
export const skip = true
|
|
@@ -1,21 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from '../..'
|
|
||||||
|
|
||||||
export const run = editor => {
|
|
||||||
editor.exec({ type: 'remove_mark', mark: { key: true } })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<block>
|
|
||||||
<mark key>
|
|
||||||
<anchor />
|
|
||||||
one
|
|
||||||
<focus />
|
|
||||||
</mark>
|
|
||||||
</block>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = input
|
|
@@ -1,9 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Element,
|
Element,
|
||||||
Descendant,
|
Descendant,
|
||||||
Mark,
|
|
||||||
Node,
|
Node,
|
||||||
Path,
|
|
||||||
Range,
|
Range,
|
||||||
Text,
|
Text,
|
||||||
Editor,
|
Editor,
|
||||||
@@ -37,7 +35,7 @@ const resolveDescendants = (children: any[]): Descendant[] => {
|
|||||||
const prev = nodes[nodes.length - 1]
|
const prev = nodes[nodes.length - 1]
|
||||||
|
|
||||||
if (typeof child === 'string') {
|
if (typeof child === 'string') {
|
||||||
const text = { text: child, marks: [] }
|
const text = { text: child }
|
||||||
STRINGS.add(text)
|
STRINGS.add(text)
|
||||||
child = text
|
child = text
|
||||||
}
|
}
|
||||||
@@ -49,8 +47,7 @@ const resolveDescendants = (children: any[]): Descendant[] => {
|
|||||||
Text.isText(prev) &&
|
Text.isText(prev) &&
|
||||||
STRINGS.has(prev) &&
|
STRINGS.has(prev) &&
|
||||||
STRINGS.has(c) &&
|
STRINGS.has(c) &&
|
||||||
c.marks.every(m => Mark.exists(m, prev.marks)) &&
|
Text.equals(prev, c, { loose: true })
|
||||||
prev.marks.every(m => Mark.exists(m, c.marks))
|
|
||||||
) {
|
) {
|
||||||
prev.text += c.text
|
prev.text += c.text
|
||||||
} else {
|
} else {
|
||||||
@@ -143,43 +140,6 @@ export function createFragment(
|
|||||||
return resolveDescendants(children)
|
return resolveDescendants(children)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a `Text` object with a mark applied.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function createMark(
|
|
||||||
tagName: string,
|
|
||||||
attributes: { [key: string]: any },
|
|
||||||
children: any[]
|
|
||||||
): Text {
|
|
||||||
const mark = { ...attributes }
|
|
||||||
const nodes = resolveDescendants(children)
|
|
||||||
|
|
||||||
if (nodes.length > 1) {
|
|
||||||
throw new Error(
|
|
||||||
`The <mark> hyperscript tag must only contain a single node's worth of children.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodes.length === 0) {
|
|
||||||
return { text: '', marks: [mark] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const [node] = nodes
|
|
||||||
|
|
||||||
if (!Text.isText(node)) {
|
|
||||||
throw new Error(
|
|
||||||
`The <mark> hyperscript tag must only contain text content as children.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Mark.exists(mark, node.marks)) {
|
|
||||||
node.marks.push(mark)
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a `Selection` object.
|
* Create a `Selection` object.
|
||||||
*/
|
*/
|
||||||
@@ -237,7 +197,7 @@ export function createText(
|
|||||||
let [node] = nodes
|
let [node] = nodes
|
||||||
|
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
node = { text: '', marks: [] }
|
node = { text: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Text.isText(node)) {
|
if (!Text.isText(node)) {
|
||||||
@@ -245,8 +205,8 @@ export function createText(
|
|||||||
The <text> hyperscript tag can only contain text content as children.`)
|
The <text> hyperscript tag can only contain text content as children.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// COMPAT: Re-create the node, because if they used the <text> tag we want to
|
// COMPAT: If they used the <text> tag we want to guarantee that it won't be
|
||||||
// guarantee that it won't be merge with other string children.
|
// merge with other string children.
|
||||||
STRINGS.delete(node)
|
STRINGS.delete(node)
|
||||||
|
|
||||||
Object.assign(node, attributes)
|
Object.assign(node, attributes)
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import isPlainObject from 'is-plain-object'
|
import isPlainObject from 'is-plain-object'
|
||||||
import { Element, Mark } from 'slate'
|
import { Element } from 'slate'
|
||||||
import {
|
import {
|
||||||
createAnchor,
|
createAnchor,
|
||||||
createCursor,
|
createCursor,
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
createElement,
|
createElement,
|
||||||
createFocus,
|
createFocus,
|
||||||
createFragment,
|
createFragment,
|
||||||
createMark,
|
|
||||||
createSelection,
|
createSelection,
|
||||||
createText,
|
createText,
|
||||||
} from './creators'
|
} from './creators'
|
||||||
@@ -23,7 +22,6 @@ const DEFAULT_CREATORS = {
|
|||||||
element: createElement,
|
element: createElement,
|
||||||
focus: createFocus,
|
focus: createFocus,
|
||||||
fragment: createFragment,
|
fragment: createFragment,
|
||||||
mark: createMark,
|
|
||||||
selection: createSelection,
|
selection: createSelection,
|
||||||
text: createText,
|
text: createText,
|
||||||
}
|
}
|
||||||
@@ -54,16 +52,13 @@ const createHyperscript = (
|
|||||||
options: {
|
options: {
|
||||||
creators?: HyperscriptCreators
|
creators?: HyperscriptCreators
|
||||||
elements?: HyperscriptShorthands
|
elements?: HyperscriptShorthands
|
||||||
marks?: HyperscriptShorthands
|
|
||||||
} = {}
|
} = {}
|
||||||
) => {
|
) => {
|
||||||
const { elements = {}, marks = {} } = options
|
const { elements = {} } = options
|
||||||
const elementCreators = normalizeElements(elements)
|
const elementCreators = normalizeElements(elements)
|
||||||
const markCreators = normalizeMarks(marks)
|
|
||||||
const creators = {
|
const creators = {
|
||||||
...DEFAULT_CREATORS,
|
...DEFAULT_CREATORS,
|
||||||
...elementCreators,
|
...elementCreators,
|
||||||
...markCreators,
|
|
||||||
...options.creators,
|
...options.creators,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,32 +127,4 @@ const normalizeElements = (elements: HyperscriptShorthands) => {
|
|||||||
return creators
|
return creators
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a dictionary of mark shorthands into creator functions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const normalizeMarks = (marks: HyperscriptShorthands) => {
|
|
||||||
const creators: HyperscriptCreators<Mark> = {}
|
|
||||||
|
|
||||||
for (const tagName in marks) {
|
|
||||||
const props = marks[tagName]
|
|
||||||
|
|
||||||
if (typeof props !== 'object') {
|
|
||||||
throw new Error(
|
|
||||||
`Properties specified for a hyperscript shorthand should be an object, but for the custom mark <${tagName}> tag you passed: ${props}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
creators[tagName] = (
|
|
||||||
tagName: string,
|
|
||||||
attributes: { [key: string]: any },
|
|
||||||
children: any[]
|
|
||||||
) => {
|
|
||||||
return createMark('mark', { ...props, ...attributes }, children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return creators
|
|
||||||
}
|
|
||||||
|
|
||||||
export { createHyperscript, HyperscriptCreators, HyperscriptShorthands }
|
export { createHyperscript, HyperscriptCreators, HyperscriptShorthands }
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Mark, Node, Path, Text } from 'slate'
|
import { Node, Path, Text } from 'slate'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A weak map to hold anchor tokens.
|
* A weak map to hold anchor tokens.
|
||||||
@@ -23,23 +23,17 @@ export class Token {}
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export class AnchorToken extends Token {
|
export class AnchorToken extends Token {
|
||||||
focused: boolean
|
|
||||||
marks: Mark[] | null
|
|
||||||
offset?: number
|
offset?: number
|
||||||
path?: Path
|
path?: Path
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
props: {
|
props: {
|
||||||
focused?: boolean
|
|
||||||
marks?: Mark[] | null
|
|
||||||
offset?: number
|
offset?: number
|
||||||
path?: Path
|
path?: Path
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
const { focused = true, marks = null, offset, path } = props
|
const { offset, path } = props
|
||||||
this.focused = focused
|
|
||||||
this.marks = marks
|
|
||||||
this.offset = offset
|
this.offset = offset
|
||||||
this.path = path
|
this.path = path
|
||||||
}
|
}
|
||||||
@@ -50,23 +44,17 @@ export class AnchorToken extends Token {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export class FocusToken extends Token {
|
export class FocusToken extends Token {
|
||||||
focused: boolean
|
|
||||||
marks: Mark[] | null
|
|
||||||
offset?: number
|
offset?: number
|
||||||
path?: Path
|
path?: Path
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
props: {
|
props: {
|
||||||
focused?: boolean
|
|
||||||
marks?: Mark[] | null
|
|
||||||
offset?: number
|
offset?: number
|
||||||
path?: Path
|
path?: Path
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
const { focused = true, marks = null, offset, path } = props
|
const { offset, path } = props
|
||||||
this.focused = focused
|
|
||||||
this.marks = marks
|
|
||||||
this.offset = offset
|
this.offset = offset
|
||||||
this.path = path
|
this.path = path
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -23,7 +23,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: '',
|
text: '',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -31,7 +30,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: '',
|
text: '',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -21,7 +21,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'one',
|
text: 'one',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -29,7 +28,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'two',
|
text: 'two',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -21,7 +21,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'one',
|
text: 'one',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -29,7 +28,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'two',
|
text: 'two',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -21,7 +21,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'one',
|
text: 'one',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -29,7 +28,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'two',
|
text: 'two',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -16,7 +16,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: '',
|
text: '',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -17,7 +17,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'one',
|
text: 'one',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -17,7 +17,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'one',
|
text: 'one',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -21,7 +21,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -22,7 +22,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -21,7 +21,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -17,7 +17,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'one',
|
text: 'one',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -1,34 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<element>
|
|
||||||
<cursor focused={false} />
|
|
||||||
</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = {
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
text: '',
|
|
||||||
marks: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selection: {
|
|
||||||
anchor: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
focus: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
@@ -1,40 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<element>
|
|
||||||
<mark>one</mark>
|
|
||||||
<cursor />
|
|
||||||
two
|
|
||||||
</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = {
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
text: 'one',
|
|
||||||
marks: [{}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'two',
|
|
||||||
marks: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selection: {
|
|
||||||
anchor: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 3,
|
|
||||||
},
|
|
||||||
focus: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
@@ -1,42 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<element>
|
|
||||||
<mark>
|
|
||||||
one
|
|
||||||
<cursor />
|
|
||||||
</mark>
|
|
||||||
two
|
|
||||||
</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = {
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
text: 'one',
|
|
||||||
marks: [{}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'two',
|
|
||||||
marks: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selection: {
|
|
||||||
anchor: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 3,
|
|
||||||
},
|
|
||||||
focus: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
@@ -1,42 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<element>
|
|
||||||
<mark>
|
|
||||||
o<cursor />
|
|
||||||
ne
|
|
||||||
</mark>
|
|
||||||
two
|
|
||||||
</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = {
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
text: 'one',
|
|
||||||
marks: [{}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'two',
|
|
||||||
marks: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selection: {
|
|
||||||
anchor: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 1,
|
|
||||||
},
|
|
||||||
focus: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
@@ -1,42 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<element>
|
|
||||||
<mark>
|
|
||||||
<cursor />
|
|
||||||
one
|
|
||||||
</mark>
|
|
||||||
two
|
|
||||||
</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = {
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
text: 'one',
|
|
||||||
marks: [{}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'two',
|
|
||||||
marks: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selection: {
|
|
||||||
anchor: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
focus: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
@@ -1,34 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<element>
|
|
||||||
<cursor marks={[]} />
|
|
||||||
</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = {
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
text: '',
|
|
||||||
marks: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selection: {
|
|
||||||
anchor: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
focus: {
|
|
||||||
path: [0, 0],
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
@@ -18,7 +18,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: '',
|
text: '',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -15,7 +15,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -8,7 +8,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: '',
|
text: '',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,6 @@ export const output = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -7,6 +7,5 @@ export const input = <fragment>word</fragment>
|
|||||||
export const output = [
|
export const output = [
|
||||||
{
|
{
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@@ -1,16 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { createHyperscript } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
const jsx = createHyperscript({
|
|
||||||
marks: {
|
|
||||||
b: { type: 'bold' },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const input = <b>word</b>
|
|
||||||
|
|
||||||
export const output = {
|
|
||||||
text: 'word',
|
|
||||||
marks: [{ type: 'bold' }],
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<mark type="a">
|
|
||||||
<mark type="b">word</mark>
|
|
||||||
</mark>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = {
|
|
||||||
text: 'word',
|
|
||||||
marks: [{ type: 'b' }, { type: 'a' }],
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const input = <mark>word</mark>
|
|
||||||
|
|
||||||
export const output = {
|
|
||||||
text: 'word',
|
|
||||||
marks: [{}],
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<mark>
|
|
||||||
<text>word</text>
|
|
||||||
</mark>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = {
|
|
||||||
text: 'word',
|
|
||||||
marks: [{}],
|
|
||||||
}
|
|
@@ -18,7 +18,6 @@ export const output = {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
import { jsx } from 'slate-hyperscript'
|
||||||
|
|
||||||
export const input = <text />
|
export const input = <text a />
|
||||||
|
|
||||||
export const output = {
|
export const output = {
|
||||||
text: '',
|
text: '',
|
||||||
marks: [],
|
a: true,
|
||||||
}
|
}
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
import { jsx } from 'slate-hyperscript'
|
||||||
|
|
||||||
export const input = <text>word</text>
|
export const input = <text a>word</text>
|
||||||
|
|
||||||
export const output = {
|
export const output = {
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
a: true,
|
||||||
}
|
}
|
||||||
|
@@ -3,12 +3,13 @@
|
|||||||
import { jsx } from 'slate-hyperscript'
|
import { jsx } from 'slate-hyperscript'
|
||||||
|
|
||||||
export const input = (
|
export const input = (
|
||||||
<text>
|
<text b>
|
||||||
<text>word</text>
|
<text a>word</text>
|
||||||
</text>
|
</text>
|
||||||
)
|
)
|
||||||
|
|
||||||
export const output = {
|
export const output = {
|
||||||
text: 'word',
|
text: 'word',
|
||||||
marks: [],
|
a: true,
|
||||||
|
b: true,
|
||||||
}
|
}
|
||||||
|
@@ -6,11 +6,7 @@ import TextComponent from './text'
|
|||||||
import { ReactEditor } from '..'
|
import { ReactEditor } from '..'
|
||||||
import { useEditor } from '../hooks/use-editor'
|
import { useEditor } from '../hooks/use-editor'
|
||||||
import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps'
|
import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps'
|
||||||
import {
|
import { RenderElementProps, RenderLeafProps } from './editable'
|
||||||
RenderDecorationProps,
|
|
||||||
RenderElementProps,
|
|
||||||
RenderMarkProps,
|
|
||||||
} from './editable'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Children.
|
* Children.
|
||||||
@@ -20,18 +16,16 @@ const Children = (props: {
|
|||||||
decorate: (entry: NodeEntry) => Range[]
|
decorate: (entry: NodeEntry) => Range[]
|
||||||
decorations: Range[]
|
decorations: Range[]
|
||||||
node: Ancestor
|
node: Ancestor
|
||||||
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
|
|
||||||
renderElement?: (props: RenderElementProps) => JSX.Element
|
renderElement?: (props: RenderElementProps) => JSX.Element
|
||||||
renderMark?: (props: RenderMarkProps) => JSX.Element
|
renderLeaf?: (props: RenderLeafProps) => JSX.Element
|
||||||
selection: Range | null
|
selection: Range | null
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
decorate,
|
decorate,
|
||||||
decorations,
|
decorations,
|
||||||
node,
|
node,
|
||||||
renderDecoration,
|
|
||||||
renderElement,
|
renderElement,
|
||||||
renderMark,
|
renderLeaf,
|
||||||
selection,
|
selection,
|
||||||
} = props
|
} = props
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
@@ -65,9 +59,8 @@ const Children = (props: {
|
|||||||
decorations={ds}
|
decorations={ds}
|
||||||
element={n}
|
element={n}
|
||||||
key={key.id}
|
key={key.id}
|
||||||
renderDecoration={renderDecoration}
|
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderMark={renderMark}
|
renderLeaf={renderLeaf}
|
||||||
selection={sel}
|
selection={sel}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -78,8 +71,7 @@ const Children = (props: {
|
|||||||
key={key.id}
|
key={key.id}
|
||||||
isLast={isLeafBlock && i === node.children.length}
|
isLast={isLeafBlock && i === node.children.length}
|
||||||
parent={node}
|
parent={node}
|
||||||
renderDecoration={renderDecoration}
|
renderLeaf={renderLeaf}
|
||||||
renderMark={renderMark}
|
|
||||||
text={n}
|
text={n}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@@ -5,7 +5,7 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useCallback,
|
useCallback,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { Editor, Element, NodeEntry, Node, Range, Text, Mark } from 'slate'
|
import { Editor, Element, NodeEntry, Node, Range, Text } from 'slate'
|
||||||
import debounce from 'debounce'
|
import debounce from 'debounce'
|
||||||
import scrollIntoView from 'scroll-into-view-if-needed'
|
import scrollIntoView from 'scroll-into-view-if-needed'
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ import { IS_FIREFOX, IS_SAFARI } from '../utils/environment'
|
|||||||
import { ReactEditor } from '..'
|
import { ReactEditor } from '..'
|
||||||
import { ReadOnlyContext } from '../hooks/use-read-only'
|
import { ReadOnlyContext } from '../hooks/use-read-only'
|
||||||
import { useSlate } from '../hooks/use-slate'
|
import { useSlate } from '../hooks/use-slate'
|
||||||
import { Leaf } from '../utils/leaf'
|
|
||||||
import {
|
import {
|
||||||
DOMElement,
|
DOMElement,
|
||||||
DOMNode,
|
DOMNode,
|
||||||
@@ -34,20 +33,6 @@ import {
|
|||||||
PLACEHOLDER_SYMBOL,
|
PLACEHOLDER_SYMBOL,
|
||||||
} from '../utils/weak-maps'
|
} from '../utils/weak-maps'
|
||||||
|
|
||||||
/**
|
|
||||||
* `RenderDecorationProps` are passed to the `renderDecoration` handler.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface RenderDecorationProps {
|
|
||||||
children: any
|
|
||||||
decoration: Range
|
|
||||||
leaf: Leaf
|
|
||||||
text: Text
|
|
||||||
attributes: {
|
|
||||||
'data-slate-decoration': true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `RenderElementProps` are passed to the `renderElement` handler.
|
* `RenderElementProps` are passed to the `renderElement` handler.
|
||||||
*/
|
*/
|
||||||
@@ -56,8 +41,8 @@ export interface RenderElementProps {
|
|||||||
children: any
|
children: any
|
||||||
element: Element
|
element: Element
|
||||||
attributes: {
|
attributes: {
|
||||||
'data-slate-inline'?: true
|
|
||||||
'data-slate-node': 'element'
|
'data-slate-node': 'element'
|
||||||
|
'data-slate-inline'?: true
|
||||||
'data-slate-void'?: true
|
'data-slate-void'?: true
|
||||||
dir?: 'rtl'
|
dir?: 'rtl'
|
||||||
ref: any
|
ref: any
|
||||||
@@ -65,16 +50,15 @@ export interface RenderElementProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `RenderMarkProps` are passed to the `renderMark` handler.
|
* `RenderLeafProps` are passed to the `renderLeaf` handler.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface RenderMarkProps {
|
export interface RenderLeafProps {
|
||||||
children: any
|
children: any
|
||||||
mark: Mark
|
leaf: Text
|
||||||
leaf: Leaf
|
|
||||||
text: Text
|
text: Text
|
||||||
attributes: {
|
attributes: {
|
||||||
'data-slate-mark': true
|
'data-slate-leaf': true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,21 +74,19 @@ export const Editable = (
|
|||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
role?: string
|
role?: string
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
|
|
||||||
renderElement?: (props: RenderElementProps) => JSX.Element
|
renderElement?: (props: RenderElementProps) => JSX.Element
|
||||||
renderMark?: (props: RenderMarkProps) => JSX.Element
|
renderLeaf?: (props: RenderLeafProps) => JSX.Element
|
||||||
} & React.TextareaHTMLAttributes<HTMLDivElement>
|
} & React.TextareaHTMLAttributes<HTMLDivElement>
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
|
autoFocus,
|
||||||
decorate = defaultDecorate,
|
decorate = defaultDecorate,
|
||||||
|
onDOMBeforeInput: propsOnDOMBeforeInput,
|
||||||
placeholder,
|
placeholder,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
renderDecoration,
|
|
||||||
renderElement,
|
renderElement,
|
||||||
renderMark,
|
renderLeaf,
|
||||||
autoFocus,
|
|
||||||
style = {},
|
style = {},
|
||||||
onDOMBeforeInput: propsOnDOMBeforeInput,
|
|
||||||
...attributes
|
...attributes
|
||||||
} = props
|
} = props
|
||||||
const editor = useSlate()
|
const editor = useSlate()
|
||||||
@@ -906,9 +888,8 @@ export const Editable = (
|
|||||||
decorate={decorate}
|
decorate={decorate}
|
||||||
decorations={decorations}
|
decorations={decorations}
|
||||||
node={editor}
|
node={editor}
|
||||||
renderDecoration={renderDecoration}
|
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderMark={renderMark}
|
renderLeaf={renderLeaf}
|
||||||
selection={editor.selection}
|
selection={editor.selection}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -13,12 +13,7 @@ import {
|
|||||||
NODE_TO_INDEX,
|
NODE_TO_INDEX,
|
||||||
KEY_TO_ELEMENT,
|
KEY_TO_ELEMENT,
|
||||||
} from '../utils/weak-maps'
|
} from '../utils/weak-maps'
|
||||||
import {
|
import { RenderElementProps, RenderLeafProps } from './editable'
|
||||||
RenderDecorationProps,
|
|
||||||
RenderElementProps,
|
|
||||||
RenderMarkProps,
|
|
||||||
} from './editable'
|
|
||||||
import { isRangeListEqual } from '../utils/leaf'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Element.
|
* Element.
|
||||||
@@ -28,18 +23,16 @@ const Element = (props: {
|
|||||||
decorate: (entry: NodeEntry) => Range[]
|
decorate: (entry: NodeEntry) => Range[]
|
||||||
decorations: Range[]
|
decorations: Range[]
|
||||||
element: SlateElement
|
element: SlateElement
|
||||||
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
|
|
||||||
renderElement?: (props: RenderElementProps) => JSX.Element
|
renderElement?: (props: RenderElementProps) => JSX.Element
|
||||||
renderMark?: (props: RenderMarkProps) => JSX.Element
|
renderLeaf?: (props: RenderLeafProps) => JSX.Element
|
||||||
selection: Range | null
|
selection: Range | null
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
decorate,
|
decorate,
|
||||||
decorations,
|
decorations,
|
||||||
element,
|
element,
|
||||||
renderDecoration,
|
|
||||||
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
|
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
|
||||||
renderMark,
|
renderLeaf,
|
||||||
selection,
|
selection,
|
||||||
} = props
|
} = props
|
||||||
const ref = useRef<HTMLElement>(null)
|
const ref = useRef<HTMLElement>(null)
|
||||||
@@ -53,9 +46,8 @@ const Element = (props: {
|
|||||||
decorate={decorate}
|
decorate={decorate}
|
||||||
decorations={decorations}
|
decorations={decorations}
|
||||||
node={element}
|
node={element}
|
||||||
renderDecoration={renderDecoration}
|
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderMark={renderMark}
|
renderLeaf={renderLeaf}
|
||||||
selection={selection}
|
selection={selection}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -141,9 +133,8 @@ const MemoizedElement = React.memo(Element, (prev, next) => {
|
|||||||
return (
|
return (
|
||||||
prev.decorate === next.decorate &&
|
prev.decorate === next.decorate &&
|
||||||
prev.element === next.element &&
|
prev.element === next.element &&
|
||||||
prev.renderDecoration === next.renderDecoration &&
|
|
||||||
prev.renderElement === next.renderElement &&
|
prev.renderElement === next.renderElement &&
|
||||||
prev.renderMark === next.renderMark &&
|
prev.renderLeaf === next.renderLeaf &&
|
||||||
isRangeListEqual(prev.decorations, next.decorations) &&
|
isRangeListEqual(prev.decorations, next.decorations) &&
|
||||||
(prev.selection === next.selection ||
|
(prev.selection === next.selection ||
|
||||||
(!!prev.selection &&
|
(!!prev.selection &&
|
||||||
@@ -167,4 +158,29 @@ export const DefaultElement = (props: RenderElementProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a list of ranges is equal to another.
|
||||||
|
*
|
||||||
|
* PERF: this requires the two lists to also have the ranges inside them in the
|
||||||
|
* same order, but this is an okay constraint for us since decorations are
|
||||||
|
* kept in order, and the odd case where they aren't is okay to re-render for.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const isRangeListEqual = (list: Range[], another: Range[]): boolean => {
|
||||||
|
if (list.length !== another.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
const range = list[i]
|
||||||
|
const other = another[i]
|
||||||
|
|
||||||
|
if (!Range.equals(range, other)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
export default MemoizedElement
|
export default MemoizedElement
|
||||||
|
@@ -2,9 +2,8 @@ import React from 'react'
|
|||||||
import { Text, Element } from 'slate'
|
import { Text, Element } from 'slate'
|
||||||
|
|
||||||
import String from './string'
|
import String from './string'
|
||||||
import { Leaf as SlateLeaf } from '../utils/leaf'
|
|
||||||
import { PLACEHOLDER_SYMBOL } from '../utils/weak-maps'
|
import { PLACEHOLDER_SYMBOL } from '../utils/weak-maps'
|
||||||
import { RenderDecorationProps, RenderMarkProps } from './editable'
|
import { RenderLeafProps } from './editable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Individual leaves in a text node with unique formatting.
|
* Individual leaves in a text node with unique formatting.
|
||||||
@@ -12,10 +11,9 @@ import { RenderDecorationProps, RenderMarkProps } from './editable'
|
|||||||
|
|
||||||
const Leaf = (props: {
|
const Leaf = (props: {
|
||||||
isLast: boolean
|
isLast: boolean
|
||||||
leaf: SlateLeaf
|
leaf: Text
|
||||||
parent: Element
|
parent: Element
|
||||||
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
|
renderLeaf?: (props: RenderLeafProps) => JSX.Element
|
||||||
renderMark?: (props: RenderMarkProps) => JSX.Element
|
|
||||||
text: Text
|
text: Text
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@@ -23,117 +21,64 @@ const Leaf = (props: {
|
|||||||
isLast,
|
isLast,
|
||||||
text,
|
text,
|
||||||
parent,
|
parent,
|
||||||
renderDecoration = (props: RenderDecorationProps) => (
|
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
|
||||||
<DefaultDecoration {...props} />
|
|
||||||
),
|
|
||||||
renderMark = (props: RenderMarkProps) => <DefaultMark {...props} />,
|
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
let children = (
|
let children = (
|
||||||
<String isLast={isLast} leaf={leaf} parent={parent} text={text} />
|
<String isLast={isLast} leaf={leaf} parent={parent} text={text} />
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (leaf[PLACEHOLDER_SYMBOL]) {
|
||||||
|
children = (
|
||||||
|
<React.Fragment>
|
||||||
|
<span
|
||||||
|
contentEditable={false}
|
||||||
|
style={{
|
||||||
|
pointerEvents: 'none',
|
||||||
|
display: 'inline-block',
|
||||||
|
verticalAlign: 'text-top',
|
||||||
|
width: '0',
|
||||||
|
maxWidth: '100%',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
opacity: '0.333',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{leaf.placeholder}
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// COMPAT: Having the `data-` attributes on these leaf elements ensures that
|
// COMPAT: Having the `data-` attributes on these leaf elements ensures that
|
||||||
// in certain misbehaving browsers they aren't weirdly cloned/destroyed by
|
// in certain misbehaving browsers they aren't weirdly cloned/destroyed by
|
||||||
// contenteditable behaviors. (2019/05/08)
|
// contenteditable behaviors. (2019/05/08)
|
||||||
for (const mark of leaf.marks) {
|
const attributes: {
|
||||||
const ret = renderMark({
|
'data-slate-leaf': true
|
||||||
children,
|
} = {
|
||||||
leaf,
|
'data-slate-leaf': true,
|
||||||
mark,
|
|
||||||
text,
|
|
||||||
attributes: {
|
|
||||||
'data-slate-mark': true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (ret) {
|
|
||||||
children = ret
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const decoration of leaf.decorations) {
|
return renderLeaf({ attributes, children, leaf, text })
|
||||||
const p = {
|
|
||||||
children,
|
|
||||||
decoration,
|
|
||||||
leaf,
|
|
||||||
text,
|
|
||||||
attributes: {
|
|
||||||
'data-slate-decoration': true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PLACEHOLDER_SYMBOL in decoration) {
|
|
||||||
// @ts-ignore
|
|
||||||
children = <PlaceholderDecoration {...p} />
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
const ret = renderDecoration(p)
|
|
||||||
|
|
||||||
if (ret) {
|
|
||||||
children = ret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span data-slate-leaf>{children}</span>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
|
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
|
||||||
return (
|
return (
|
||||||
next.parent === prev.parent &&
|
next.parent === prev.parent &&
|
||||||
next.isLast === prev.isLast &&
|
next.isLast === prev.isLast &&
|
||||||
next.renderDecoration === prev.renderDecoration &&
|
next.renderLeaf === prev.renderLeaf &&
|
||||||
next.renderMark === prev.renderMark &&
|
|
||||||
next.text === prev.text &&
|
next.text === prev.text &&
|
||||||
SlateLeaf.equals(next.leaf, prev.leaf)
|
Text.matches(next.leaf, prev.leaf)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default custom decoration renderer.
|
* The default custom leaf renderer.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const DefaultDecoration = (props: RenderDecorationProps) => {
|
export const DefaultLeaf = (props: RenderLeafProps) => {
|
||||||
const { attributes, children } = props
|
const { attributes, children } = props
|
||||||
return <span {...attributes}>{children}</span>
|
return <span {...attributes}>{children}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The default custom mark renderer.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const DefaultMark = (props: RenderMarkProps) => {
|
|
||||||
const { attributes, children } = props
|
|
||||||
return <span {...attributes}>{children}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A custom decoration for the default placeholder behavior.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const PlaceholderDecoration = (props: RenderDecorationProps) => {
|
|
||||||
const { decoration, attributes, children } = props
|
|
||||||
const { placeholder } = decoration
|
|
||||||
return (
|
|
||||||
<span {...attributes}>
|
|
||||||
<span
|
|
||||||
contentEditable={false}
|
|
||||||
style={{
|
|
||||||
pointerEvents: 'none',
|
|
||||||
display: 'inline-block',
|
|
||||||
verticalAlign: 'text-top',
|
|
||||||
width: '0',
|
|
||||||
maxWidth: '100%',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
opacity: '0.333',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{placeholder}
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MemoizedLeaf
|
export default MemoizedLeaf
|
||||||
|
@@ -2,7 +2,6 @@ import React from 'react'
|
|||||||
import { Editor, Text, Path, Element, Node } from 'slate'
|
import { Editor, Text, Path, Element, Node } from 'slate'
|
||||||
|
|
||||||
import { ReactEditor, useEditor } from '..'
|
import { ReactEditor, useEditor } from '..'
|
||||||
import { Leaf } from '../utils/leaf'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Leaf content strings.
|
* Leaf content strings.
|
||||||
@@ -10,7 +9,7 @@ import { Leaf } from '../utils/leaf'
|
|||||||
|
|
||||||
const String = (props: {
|
const String = (props: {
|
||||||
isLast: boolean
|
isLast: boolean
|
||||||
leaf: Leaf
|
leaf: Text
|
||||||
parent: Element
|
parent: Element
|
||||||
text: Text
|
text: Text
|
||||||
}) => {
|
}) => {
|
||||||
|
@@ -2,9 +2,8 @@ import React, { useLayoutEffect, useRef } from 'react'
|
|||||||
import { Range, Element, Text as SlateText } from 'slate'
|
import { Range, Element, Text as SlateText } from 'slate'
|
||||||
|
|
||||||
import Leaf from './leaf'
|
import Leaf from './leaf'
|
||||||
import { Leaf as SlateLeaf } from '../utils/leaf'
|
|
||||||
import { ReactEditor, useEditor } from '..'
|
import { ReactEditor, useEditor } from '..'
|
||||||
import { RenderDecorationProps, RenderMarkProps } from './editable'
|
import { RenderLeafProps } from './editable'
|
||||||
import {
|
import {
|
||||||
KEY_TO_ELEMENT,
|
KEY_TO_ELEMENT,
|
||||||
NODE_TO_ELEMENT,
|
NODE_TO_ELEMENT,
|
||||||
@@ -19,18 +18,10 @@ const Text = (props: {
|
|||||||
decorations: Range[]
|
decorations: Range[]
|
||||||
isLast: boolean
|
isLast: boolean
|
||||||
parent: Element
|
parent: Element
|
||||||
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
|
renderLeaf?: (props: RenderLeafProps) => JSX.Element
|
||||||
renderMark?: (props: RenderMarkProps) => JSX.Element
|
|
||||||
text: SlateText
|
text: SlateText
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { decorations, isLast, parent, renderLeaf, text } = props
|
||||||
decorations,
|
|
||||||
isLast,
|
|
||||||
parent,
|
|
||||||
renderDecoration,
|
|
||||||
renderMark,
|
|
||||||
text,
|
|
||||||
} = props
|
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const ref = useRef<HTMLSpanElement>(null)
|
const ref = useRef<HTMLSpanElement>(null)
|
||||||
const leaves = getLeaves(text, decorations)
|
const leaves = getLeaves(text, decorations)
|
||||||
@@ -47,8 +38,7 @@ const Text = (props: {
|
|||||||
leaf={leaf}
|
leaf={leaf}
|
||||||
text={text}
|
text={text}
|
||||||
parent={parent}
|
parent={parent}
|
||||||
renderDecoration={renderDecoration}
|
renderLeaf={renderLeaf}
|
||||||
renderMark={renderMark}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -76,12 +66,12 @@ const Text = (props: {
|
|||||||
* Get the leaves for a text node given decorations.
|
* Get the leaves for a text node given decorations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
|
const getLeaves = (node: SlateText, decorations: Range[]): SlateText[] => {
|
||||||
const { text, marks } = node
|
let leaves: SlateText[] = [{ ...node }]
|
||||||
let leaves: SlateLeaf[] = [{ text, marks, decorations: [] }]
|
|
||||||
|
|
||||||
const compile = (range: Range, key?: string) => {
|
for (const dec of decorations) {
|
||||||
const [start, end] = Range.edges(range)
|
const { anchor, focus, ...rest } = dec
|
||||||
|
const [start, end] = Range.edges(dec)
|
||||||
const next = []
|
const next = []
|
||||||
let o = 0
|
let o = 0
|
||||||
|
|
||||||
@@ -92,7 +82,7 @@ const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
|
|||||||
|
|
||||||
// If the range encompases the entire leaf, add the range.
|
// If the range encompases the entire leaf, add the range.
|
||||||
if (start.offset <= offset && end.offset >= offset + length) {
|
if (start.offset <= offset && end.offset >= offset + length) {
|
||||||
leaf.decorations.push(range)
|
Object.assign(leaf, rest)
|
||||||
next.push(leaf)
|
next.push(leaf)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -115,14 +105,18 @@ const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
|
|||||||
let after
|
let after
|
||||||
|
|
||||||
if (end.offset < offset + length) {
|
if (end.offset < offset + length) {
|
||||||
;[middle, after] = SlateLeaf.split(middle, end.offset - offset)
|
const off = end.offset - offset
|
||||||
|
after = { ...middle, text: middle.text.slice(off) }
|
||||||
|
middle = { ...middle, text: middle.text.slice(0, off) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start.offset > offset) {
|
if (start.offset > offset) {
|
||||||
;[before, middle] = SlateLeaf.split(middle, start.offset - offset)
|
const off = start.offset - offset
|
||||||
|
before = { ...middle, text: middle.text.slice(0, off) }
|
||||||
|
middle = { ...middle, text: middle.text.slice(off) }
|
||||||
}
|
}
|
||||||
|
|
||||||
middle.decorations.push(range)
|
Object.assign(middle, rest)
|
||||||
|
|
||||||
if (before) {
|
if (before) {
|
||||||
next.push(before)
|
next.push(before)
|
||||||
@@ -138,28 +132,16 @@ const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
|
|||||||
leaves = next
|
leaves = next
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const range of decorations) {
|
|
||||||
compile(range)
|
|
||||||
}
|
|
||||||
|
|
||||||
return leaves
|
return leaves
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemoizedText = React.memo(Text, (prev, next) => {
|
const MemoizedText = React.memo(Text, (prev, next) => {
|
||||||
if (
|
return (
|
||||||
next.parent === prev.parent &&
|
next.parent === prev.parent &&
|
||||||
next.isLast === prev.isLast &&
|
next.isLast === prev.isLast &&
|
||||||
next.renderDecoration === prev.renderDecoration &&
|
next.renderLeaf === prev.renderLeaf &&
|
||||||
next.renderMark === prev.renderMark &&
|
|
||||||
next.text === prev.text
|
next.text === prev.text
|
||||||
) {
|
)
|
||||||
return SlateLeaf.equals(
|
|
||||||
{ ...next.text, decorations: next.decorations },
|
|
||||||
{ ...prev.text, decorations: prev.decorations }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default MemoizedText
|
export default MemoizedText
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
export * from './components/editable'
|
export * from './components/editable'
|
||||||
export { DefaultElement } from './components/element'
|
export { DefaultElement } from './components/element'
|
||||||
export { DefaultMark, DefaultDecoration } from './components/leaf'
|
export { DefaultLeaf } from './components/leaf'
|
||||||
export * from './hooks/use-editor'
|
export * from './hooks/use-editor'
|
||||||
export * from './hooks/use-focused'
|
export * from './hooks/use-focused'
|
||||||
export * from './hooks/use-read-only'
|
export * from './hooks/use-read-only'
|
||||||
|
@@ -1,113 +0,0 @@
|
|||||||
import isPlainObject from 'is-plain-object'
|
|
||||||
import { Range, Mark } from 'slate'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `Leaf` interface represents the individual leaves inside a text node,
|
|
||||||
* once decorations have been applied.
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface Leaf {
|
|
||||||
decorations: Range[]
|
|
||||||
marks: Mark[]
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Leaf {
|
|
||||||
/**
|
|
||||||
* Check if two leaves are equal.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const equals = (leaf: Leaf, another: Leaf): boolean => {
|
|
||||||
return (
|
|
||||||
leaf.text === another.text &&
|
|
||||||
leaf.decorations.length === another.decorations.length &&
|
|
||||||
leaf.marks.length === another.marks.length &&
|
|
||||||
leaf.marks.every(m => Mark.exists(m, another.marks)) &&
|
|
||||||
another.marks.every(m => Mark.exists(m, leaf.marks)) &&
|
|
||||||
isRangeListEqual(leaf.decorations, another.decorations)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a value is a `Leaf` object.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const isLeaf = (value: any): value is Leaf => {
|
|
||||||
return (
|
|
||||||
isPlainObject(value) &&
|
|
||||||
typeof value.text === 'string' &&
|
|
||||||
Mark.isMarkSet(value.marks) &&
|
|
||||||
Range.isRangeList(value.decorations)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Split a leaf into two at an offset.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const split = (leaf: Leaf, offset: number): [Leaf, Leaf] => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
text: leaf.text.slice(0, offset),
|
|
||||||
marks: leaf.marks,
|
|
||||||
decorations: [...leaf.decorations],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: leaf.text.slice(offset),
|
|
||||||
marks: leaf.marks,
|
|
||||||
decorations: [...leaf.decorations],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a list of ranges is equal to another.
|
|
||||||
*
|
|
||||||
* PERF: this requires the two lists to also have the ranges inside them in the
|
|
||||||
* same order, but this is an okay constraint for us since decorations are
|
|
||||||
* kept in order, and the odd case where they aren't is okay to re-render for.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const isRangeListEqual = (list: Range[], another: Range[]): boolean => {
|
|
||||||
if (list.length !== another.length) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < list.length; i++) {
|
|
||||||
const range = list[i]
|
|
||||||
const other = another[i]
|
|
||||||
|
|
||||||
if (!Range.equals(range, other)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a map of ranges is equal to another.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const isRangeMapEqual = (
|
|
||||||
map: Record<string, Range>,
|
|
||||||
another: Record<string, Range>
|
|
||||||
): boolean => {
|
|
||||||
if (Object.keys(map).length !== Object.keys(another).length) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in map) {
|
|
||||||
const range = map[key]
|
|
||||||
const other = another[key]
|
|
||||||
|
|
||||||
if (!Range.equals(range, other)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Leaf, isRangeListEqual, isRangeMapEqual }
|
|
@@ -1,4 +1,4 @@
|
|||||||
import { Node, Ancestor, Editor } from 'slate'
|
import { Node, Ancestor, Editor, Text } from 'slate'
|
||||||
|
|
||||||
import { Key } from './key'
|
import { Key } from './key'
|
||||||
|
|
||||||
@@ -16,10 +16,11 @@ export const NODE_TO_PARENT: WeakMap<Node, Ancestor> = new WeakMap()
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const EDITOR_TO_ELEMENT: WeakMap<Editor, HTMLElement> = new WeakMap()
|
export const EDITOR_TO_ELEMENT: WeakMap<Editor, HTMLElement> = new WeakMap()
|
||||||
export const NODE_TO_ELEMENT: WeakMap<Node, HTMLElement> = new WeakMap()
|
export const EDITOR_TO_PLACEHOLDER: WeakMap<Editor, string> = new WeakMap()
|
||||||
export const ELEMENT_TO_NODE: WeakMap<HTMLElement, Node> = new WeakMap()
|
export const ELEMENT_TO_NODE: WeakMap<HTMLElement, Node> = new WeakMap()
|
||||||
export const NODE_TO_KEY: WeakMap<Node, Key> = new WeakMap()
|
|
||||||
export const KEY_TO_ELEMENT: WeakMap<Key, HTMLElement> = new WeakMap()
|
export const KEY_TO_ELEMENT: WeakMap<Key, HTMLElement> = new WeakMap()
|
||||||
|
export const NODE_TO_ELEMENT: WeakMap<Node, HTMLElement> = new WeakMap()
|
||||||
|
export const NODE_TO_KEY: WeakMap<Node, Key> = new WeakMap()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Weak maps for storing editor-related state.
|
* Weak maps for storing editor-related state.
|
||||||
@@ -30,8 +31,4 @@ export const IS_FOCUSED: WeakMap<Editor, boolean> = new WeakMap()
|
|||||||
export const IS_DRAGGING: WeakMap<Editor, boolean> = new WeakMap()
|
export const IS_DRAGGING: WeakMap<Editor, boolean> = new WeakMap()
|
||||||
export const IS_CLICKING: WeakMap<Editor, boolean> = new WeakMap()
|
export const IS_CLICKING: WeakMap<Editor, boolean> = new WeakMap()
|
||||||
|
|
||||||
/**
|
export const PLACEHOLDER_SYMBOL = (Symbol('placeholder') as unknown) as string
|
||||||
* Symbols.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const PLACEHOLDER_SYMBOL = Symbol('placeholder')
|
|
||||||
|
@@ -16,11 +16,8 @@ export const withReact = (editor: Editor): Editor => {
|
|||||||
const matches: [Path, Key][] = []
|
const matches: [Path, Key][] = []
|
||||||
|
|
||||||
switch (op.type) {
|
switch (op.type) {
|
||||||
case 'add_mark':
|
|
||||||
case 'insert_text':
|
case 'insert_text':
|
||||||
case 'remove_mark':
|
|
||||||
case 'remove_text':
|
case 'remove_text':
|
||||||
case 'set_mark':
|
|
||||||
case 'set_node': {
|
case 'set_node': {
|
||||||
for (const [node, path] of Editor.levels(editor, { at: op.path })) {
|
for (const [node, path] of Editor.levels(editor, { at: op.path })) {
|
||||||
const key = ReactEditor.findKey(editor, node)
|
const key = ReactEditor.findKey(editor, node)
|
||||||
|
@@ -2,47 +2,14 @@ import {
|
|||||||
NodeEntry,
|
NodeEntry,
|
||||||
Node,
|
Node,
|
||||||
Text,
|
Text,
|
||||||
Mark,
|
|
||||||
Editor,
|
Editor,
|
||||||
MarkEntry,
|
|
||||||
AncestorEntry,
|
AncestorEntry,
|
||||||
Descendant,
|
Descendant,
|
||||||
DescendantEntry,
|
DescendantEntry,
|
||||||
} from 'slate'
|
} from 'slate'
|
||||||
|
|
||||||
import { MarkError, NodeError } from './errors'
|
import { NodeError } from './errors'
|
||||||
import { NodeRule, MarkRule, ChildValidation } from './rules'
|
import { NodeRule, ChildValidation } from './rules'
|
||||||
|
|
||||||
/**
|
|
||||||
* Check a mark object.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const checkMark = (
|
|
||||||
editor: Editor,
|
|
||||||
entry: MarkEntry,
|
|
||||||
rule: MarkRule
|
|
||||||
): MarkError | undefined => {
|
|
||||||
const { validate: v } = rule
|
|
||||||
const [mark, index, node, path] = entry
|
|
||||||
|
|
||||||
if ('properties' in v) {
|
|
||||||
for (const k in v.properties) {
|
|
||||||
const p = v.properties[k]
|
|
||||||
const value = mark[k]
|
|
||||||
|
|
||||||
if ((typeof p === 'function' && !p(value)) || p !== value) {
|
|
||||||
return {
|
|
||||||
code: 'mark_property_invalid',
|
|
||||||
mark,
|
|
||||||
index,
|
|
||||||
node,
|
|
||||||
path,
|
|
||||||
property: k,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check a node object.
|
* Check a node object.
|
||||||
@@ -70,15 +37,6 @@ export const checkNode = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('marks' in v && v.marks != null) {
|
|
||||||
for (const entry of Node.marks(node)) {
|
|
||||||
if (!Editor.isMarkMatch(editor, entry, v.marks)) {
|
|
||||||
const [mark, index, n, p] = entry
|
|
||||||
return { code: 'mark_invalid', node: n, path: p, mark, index }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('text' in v && v.text != null) {
|
if ('text' in v && v.text != null) {
|
||||||
const text = Node.text(node)
|
const text = Node.text(node)
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Ancestor, Descendant, Range, Mark, Node, Path, Text } from 'slate'
|
import { Ancestor, Descendant, Node, Path } from 'slate'
|
||||||
|
|
||||||
export interface ChildInvalidError {
|
export interface ChildInvalidError {
|
||||||
code: 'child_invalid'
|
code: 'child_invalid'
|
||||||
@@ -56,23 +56,6 @@ export interface NodeTextInvalidError {
|
|||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkInvalidError {
|
|
||||||
code: 'mark_invalid'
|
|
||||||
node: Text
|
|
||||||
path: Path
|
|
||||||
mark: Mark
|
|
||||||
index: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MarkPropertyInvalidError {
|
|
||||||
code: 'mark_property_invalid'
|
|
||||||
mark: Mark
|
|
||||||
index: number
|
|
||||||
node: Text
|
|
||||||
path: Path
|
|
||||||
property: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ParentInvalidError {
|
export interface ParentInvalidError {
|
||||||
code: 'parent_invalid'
|
code: 'parent_invalid'
|
||||||
node: Ancestor
|
node: Ancestor
|
||||||
@@ -86,19 +69,16 @@ export interface PreviousSiblingInvalidError {
|
|||||||
path: Path
|
path: Path
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MarkError = MarkPropertyInvalidError
|
|
||||||
|
|
||||||
export type NodeError =
|
export type NodeError =
|
||||||
| ChildInvalidError
|
| ChildInvalidError
|
||||||
| ChildMaxInvalidError
|
| ChildMaxInvalidError
|
||||||
| ChildMinInvalidError
|
| ChildMinInvalidError
|
||||||
| FirstChildInvalidError
|
| FirstChildInvalidError
|
||||||
| LastChildInvalidError
|
| LastChildInvalidError
|
||||||
| MarkInvalidError
|
|
||||||
| NextSiblingInvalidError
|
| NextSiblingInvalidError
|
||||||
| NodePropertyInvalidError
|
| NodePropertyInvalidError
|
||||||
| NodeTextInvalidError
|
| NodeTextInvalidError
|
||||||
| ParentInvalidError
|
| ParentInvalidError
|
||||||
| PreviousSiblingInvalidError
|
| PreviousSiblingInvalidError
|
||||||
|
|
||||||
export type SchemaError = MarkError | NodeError
|
export type SchemaError = NodeError
|
||||||
|
@@ -1,16 +1,5 @@
|
|||||||
import { Editor, NodeMatch, MarkMatch } from 'slate'
|
import { Editor, NodeMatch } from 'slate'
|
||||||
import { NodeError, MarkError } from './errors'
|
import { NodeError } from './errors'
|
||||||
|
|
||||||
export interface MarkValidation {
|
|
||||||
properties?: Record<string, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MarkRule {
|
|
||||||
for: 'mark'
|
|
||||||
match: MarkMatch
|
|
||||||
validate: MarkValidation
|
|
||||||
normalize: (editor: Editor, error: MarkError) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChildValidation {
|
export interface ChildValidation {
|
||||||
match?: NodeMatch
|
match?: NodeMatch
|
||||||
@@ -22,7 +11,6 @@ export interface NodeValidation {
|
|||||||
children?: ChildValidation[]
|
children?: ChildValidation[]
|
||||||
first?: NodeMatch
|
first?: NodeMatch
|
||||||
last?: NodeMatch
|
last?: NodeMatch
|
||||||
marks?: MarkMatch
|
|
||||||
next?: NodeMatch
|
next?: NodeMatch
|
||||||
parent?: NodeMatch
|
parent?: NodeMatch
|
||||||
previous?: NodeMatch
|
previous?: NodeMatch
|
||||||
@@ -37,4 +25,4 @@ export interface NodeRule {
|
|||||||
normalize: (editor: Editor, error: NodeError) => void
|
normalize: (editor: Editor, error: NodeError) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SchemaRule = MarkRule | NodeRule
|
export type SchemaRule = NodeRule
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Editor, Text, NodeEntry } from 'slate'
|
import { Editor, Text, NodeEntry } from 'slate'
|
||||||
|
|
||||||
import { NodeRule, SchemaRule, MarkRule } from './rules'
|
import { NodeRule, SchemaRule } from './rules'
|
||||||
import { NodeError } from './errors'
|
import { NodeError } from './errors'
|
||||||
import { checkNode, checkAncestor } from './checkers'
|
import { checkNode, checkAncestor } from './checkers'
|
||||||
|
|
||||||
@@ -14,23 +14,16 @@ export const withSchema = (
|
|||||||
rules: SchemaRule[] = []
|
rules: SchemaRule[] = []
|
||||||
): Editor => {
|
): Editor => {
|
||||||
const { normalizeNode } = editor
|
const { normalizeNode } = editor
|
||||||
const markRules: MarkRule[] = []
|
const nodeRules: NodeRule[] = rules
|
||||||
const nodeRules: NodeRule[] = []
|
|
||||||
const parentRules: NodeRule[] = []
|
const parentRules: NodeRule[] = []
|
||||||
|
|
||||||
for (const rule of rules) {
|
for (const rule of rules) {
|
||||||
if (rule.for === 'mark') {
|
if (
|
||||||
markRules.push(rule)
|
'parent' in rule.validate ||
|
||||||
} else {
|
'next' in rule.validate ||
|
||||||
nodeRules.push(rule)
|
'previous' in rule.validate
|
||||||
|
) {
|
||||||
if (
|
parentRules.push(rule)
|
||||||
'parent' in rule.validate ||
|
|
||||||
'next' in rule.validate ||
|
|
||||||
'previous' in rule.validate
|
|
||||||
) {
|
|
||||||
parentRules.push(rule)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,12 +129,6 @@ export const withSchema = (
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'mark_invalid': {
|
|
||||||
const { mark, path } = error
|
|
||||||
Editor.removeMarks(editor, [mark], { at: path })
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'parent_invalid': {
|
case 'parent_invalid': {
|
||||||
const { path, index } = error
|
const { path, index } = error
|
||||||
const childPath = path.concat(index)
|
const childPath = path.concat(index)
|
||||||
|
@@ -1,27 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const schema = [
|
|
||||||
{
|
|
||||||
for: 'node',
|
|
||||||
match: 'element',
|
|
||||||
validate: {
|
|
||||||
marks: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<element>
|
|
||||||
<mark a>text</mark>
|
|
||||||
</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = (
|
|
||||||
<editor>
|
|
||||||
<element>text</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
@@ -1,27 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const schema = [
|
|
||||||
{
|
|
||||||
for: 'node',
|
|
||||||
match: 'element',
|
|
||||||
validate: {
|
|
||||||
marks: [{ a: true }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<element>
|
|
||||||
<mark b>text</mark>
|
|
||||||
</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = (
|
|
||||||
<editor>
|
|
||||||
<element>text</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
@@ -1,27 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const schema = [
|
|
||||||
{
|
|
||||||
for: 'node',
|
|
||||||
match: 'element',
|
|
||||||
validate: {
|
|
||||||
marks: [{ a: true }, { b: true }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<element>
|
|
||||||
<mark c>text</mark>
|
|
||||||
</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = (
|
|
||||||
<editor>
|
|
||||||
<element>text</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
@@ -1,23 +0,0 @@
|
|||||||
/** @jsx jsx */
|
|
||||||
|
|
||||||
import { jsx } from 'slate-hyperscript'
|
|
||||||
|
|
||||||
export const schema = [
|
|
||||||
{
|
|
||||||
for: 'node',
|
|
||||||
match: 'element',
|
|
||||||
validate: {
|
|
||||||
marks: [{ a: true }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const input = (
|
|
||||||
<editor>
|
|
||||||
<element>
|
|
||||||
<mark a>text</mark>
|
|
||||||
</element>
|
|
||||||
</editor>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const output = input
|
|
@@ -88,11 +88,6 @@ export const createEditor = (): Editor => {
|
|||||||
|
|
||||||
if (Command.isCoreCommand(command)) {
|
if (Command.isCoreCommand(command)) {
|
||||||
switch (command.type) {
|
switch (command.type) {
|
||||||
case 'add_mark': {
|
|
||||||
Editor.addMarks(editor, command.mark)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'delete_backward': {
|
case 'delete_backward': {
|
||||||
if (selection && Range.isCollapsed(selection)) {
|
if (selection && Range.isCollapsed(selection)) {
|
||||||
Editor.delete(editor, { unit: command.unit, reverse: true })
|
Editor.delete(editor, { unit: command.unit, reverse: true })
|
||||||
@@ -136,11 +131,6 @@ export const createEditor = (): Editor => {
|
|||||||
Editor.insertText(editor, command.text)
|
Editor.insertText(editor, command.text)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'remove_mark': {
|
|
||||||
Editor.removeMarks(editor, [command.mark])
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -154,7 +144,7 @@ export const createEditor = (): Editor => {
|
|||||||
|
|
||||||
// Ensure that block and inline nodes have at least one text child.
|
// Ensure that block and inline nodes have at least one text child.
|
||||||
if (Element.isElement(node) && node.children.length === 0) {
|
if (Element.isElement(node) && node.children.length === 0) {
|
||||||
const child = { text: '', marks: [] }
|
const child = { text: '' }
|
||||||
Editor.insertNodes(editor, child, { at: path.concat(0) })
|
Editor.insertNodes(editor, child, { at: path.concat(0) })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -194,23 +184,23 @@ export const createEditor = (): Editor => {
|
|||||||
// Ensure that inline nodes are surrounded by text nodes.
|
// Ensure that inline nodes are surrounded by text nodes.
|
||||||
if (editor.isInline(child)) {
|
if (editor.isInline(child)) {
|
||||||
if (prev == null || !Text.isText(prev)) {
|
if (prev == null || !Text.isText(prev)) {
|
||||||
const newChild = { text: '', marks: [] }
|
const newChild = { text: '' }
|
||||||
Editor.insertNodes(editor, newChild, { at: path.concat(n) })
|
Editor.insertNodes(editor, newChild, { at: path.concat(n) })
|
||||||
n++
|
n++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLast) {
|
if (isLast) {
|
||||||
const newChild = { text: '', marks: [] }
|
const newChild = { text: '' }
|
||||||
Editor.insertNodes(editor, newChild, { at: path.concat(n + 1) })
|
Editor.insertNodes(editor, newChild, { at: path.concat(n + 1) })
|
||||||
n++
|
n++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Merge adjacent text nodes that are empty or have matching marks.
|
// Merge adjacent text nodes that are empty or match.
|
||||||
if (prev != null && Text.isText(prev)) {
|
if (prev != null && Text.isText(prev)) {
|
||||||
if (Text.matches(child, prev)) {
|
if (Text.equals(child, prev, { loose: true })) {
|
||||||
Editor.mergeNodes(editor, { at: path.concat(n) })
|
Editor.mergeNodes(editor, { at: path.concat(n) })
|
||||||
n--
|
n--
|
||||||
continue
|
continue
|
||||||
@@ -238,11 +228,8 @@ export const createEditor = (): Editor => {
|
|||||||
|
|
||||||
const getDirtyPaths = (op: Operation) => {
|
const getDirtyPaths = (op: Operation) => {
|
||||||
switch (op.type) {
|
switch (op.type) {
|
||||||
case 'add_mark':
|
|
||||||
case 'insert_text':
|
case 'insert_text':
|
||||||
case 'remove_mark':
|
|
||||||
case 'remove_text':
|
case 'remove_text':
|
||||||
case 'set_mark':
|
|
||||||
case 'set_node': {
|
case 'set_node': {
|
||||||
const { path } = op
|
const { path } = op
|
||||||
return Path.levels(path)
|
return Path.levels(path)
|
||||||
|
@@ -3,7 +3,6 @@ export * from './interfaces/command'
|
|||||||
export * from './interfaces/editor'
|
export * from './interfaces/editor'
|
||||||
export * from './interfaces/element'
|
export * from './interfaces/element'
|
||||||
export * from './interfaces/location'
|
export * from './interfaces/location'
|
||||||
export * from './interfaces/mark'
|
|
||||||
export * from './interfaces/node'
|
export * from './interfaces/node'
|
||||||
export * from './interfaces/operation'
|
export * from './interfaces/operation'
|
||||||
export * from './interfaces/path'
|
export * from './interfaces/path'
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import isPlainObject from 'is-plain-object'
|
import isPlainObject from 'is-plain-object'
|
||||||
import { Mark, Node, Range } from '..'
|
import { Node } from '..'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `Command` objects represent an action that a user is taking on the editor.
|
* `Command` objects represent an action that a user is taking on the editor.
|
||||||
@@ -20,32 +20,18 @@ export const Command = {
|
|||||||
return isPlainObject(value) && typeof value.type === 'string'
|
return isPlainObject(value) && typeof value.type === 'string'
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a value is an `AddMarkCommand` object.
|
|
||||||
*/
|
|
||||||
|
|
||||||
isAddMarkCommand(value: any): value is AddMarkCommand {
|
|
||||||
return (
|
|
||||||
Command.isCommand(value) &&
|
|
||||||
value.type === 'add_mark' &&
|
|
||||||
Mark.isMark(value.mark)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value is a `CoreCommand` object.
|
* Check if a value is a `CoreCommand` object.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
isCoreCommand(value: any): value is CoreCommand {
|
isCoreCommand(value: any): value is CoreCommand {
|
||||||
return (
|
return (
|
||||||
Command.isAddMarkCommand(value) ||
|
|
||||||
Command.isDeleteBackwardCommand(value) ||
|
Command.isDeleteBackwardCommand(value) ||
|
||||||
Command.isDeleteForwardCommand(value) ||
|
Command.isDeleteForwardCommand(value) ||
|
||||||
Command.isDeleteFragmentCommand(value) ||
|
Command.isDeleteFragmentCommand(value) ||
|
||||||
Command.isInsertTextCommand(value) ||
|
Command.isInsertTextCommand(value) ||
|
||||||
Command.isInsertFragmentCommand(value) ||
|
Command.isInsertFragmentCommand(value) ||
|
||||||
Command.isInsertBreakCommand(value) ||
|
Command.isInsertBreakCommand(value)
|
||||||
Command.isRemoveMarkCommand(value)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -124,27 +110,6 @@ export const Command = {
|
|||||||
typeof value.text === 'string'
|
typeof value.text === 'string'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a value is a `RemoveMarkCommand` object.
|
|
||||||
*/
|
|
||||||
|
|
||||||
isRemoveMarkCommand(value: any): value is RemoveMarkCommand {
|
|
||||||
return (
|
|
||||||
Command.isCommand(value) &&
|
|
||||||
value.type === 'remove_mark' &&
|
|
||||||
Mark.isMark(value.mark)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `AddMarkCommand` adds a mark to the current selection.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface AddMarkCommand {
|
|
||||||
type: 'add_mark'
|
|
||||||
mark: Mark
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,22 +175,12 @@ export interface InsertTextCommand {
|
|||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The `RemoveMarkCommand` removes a mark in the current selection.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface RemoveMarkCommand {
|
|
||||||
type: 'remove_mark'
|
|
||||||
mark: Mark
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `CoreCommand` union is a set of all of the commands that are recognized
|
* The `CoreCommand` union is a set of all of the commands that are recognized
|
||||||
* by Slate's "core" out of the box.
|
* by Slate's "core" out of the box.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type CoreCommand =
|
export type CoreCommand =
|
||||||
| AddMarkCommand
|
|
||||||
| DeleteBackwardCommand
|
| DeleteBackwardCommand
|
||||||
| DeleteForwardCommand
|
| DeleteForwardCommand
|
||||||
| DeleteFragmentCommand
|
| DeleteFragmentCommand
|
||||||
@@ -233,4 +188,3 @@ export type CoreCommand =
|
|||||||
| InsertFragmentCommand
|
| InsertFragmentCommand
|
||||||
| InsertNodeCommand
|
| InsertNodeCommand
|
||||||
| InsertTextCommand
|
| InsertTextCommand
|
||||||
| RemoveMarkCommand
|
|
||||||
|
@@ -4,8 +4,6 @@ import { ElementQueries } from './queries/element'
|
|||||||
import { GeneralTransforms } from './transforms/general'
|
import { GeneralTransforms } from './transforms/general'
|
||||||
import { GeneralQueries } from './queries/general'
|
import { GeneralQueries } from './queries/general'
|
||||||
import { LocationQueries } from './queries/location'
|
import { LocationQueries } from './queries/location'
|
||||||
import { MarkQueries } from './queries/mark'
|
|
||||||
import { MarkTransforms } from './transforms/mark'
|
|
||||||
import { NodeTransforms } from './transforms/node'
|
import { NodeTransforms } from './transforms/node'
|
||||||
import { NodeQueries } from './queries/node'
|
import { NodeQueries } from './queries/node'
|
||||||
import { RangeQueries } from './queries/range'
|
import { RangeQueries } from './queries/range'
|
||||||
@@ -35,8 +33,6 @@ export const Editor = {
|
|||||||
...GeneralQueries,
|
...GeneralQueries,
|
||||||
...GeneralTransforms,
|
...GeneralTransforms,
|
||||||
...LocationQueries,
|
...LocationQueries,
|
||||||
...MarkQueries,
|
|
||||||
...MarkTransforms,
|
|
||||||
...NodeQueries,
|
...NodeQueries,
|
||||||
...NodeTransforms,
|
...NodeTransforms,
|
||||||
...RangeQueries,
|
...RangeQueries,
|
||||||
|
@@ -9,8 +9,6 @@ import {
|
|||||||
Element,
|
Element,
|
||||||
ElementEntry,
|
ElementEntry,
|
||||||
Location,
|
Location,
|
||||||
Mark,
|
|
||||||
MarkEntry,
|
|
||||||
Node,
|
Node,
|
||||||
NodeEntry,
|
NodeEntry,
|
||||||
NodeMatch,
|
NodeMatch,
|
||||||
@@ -21,46 +19,8 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextEntry,
|
TextEntry,
|
||||||
} from '../../..'
|
} from '../../..'
|
||||||
import { MarkMatch } from '../../mark'
|
|
||||||
|
|
||||||
export const LocationQueries = {
|
export const LocationQueries = {
|
||||||
/**
|
|
||||||
* Get the marks that are "active" at a location. These are the
|
|
||||||
* marks that will be added to any text that is inserted.
|
|
||||||
*
|
|
||||||
* The `union: true` option can be passed to create a union of marks across
|
|
||||||
* the text nodes in the selection, instead of creating an intersection, which
|
|
||||||
* is the default.
|
|
||||||
*
|
|
||||||
* Note: to obey common rich text behavior, if the selection is collapsed at
|
|
||||||
* the start of a text node and there are previous text nodes in the same
|
|
||||||
* block, it will carry those marks forward from the previous text node. This
|
|
||||||
* allows for continuation of marks from previous words.
|
|
||||||
*/
|
|
||||||
|
|
||||||
activeMarks(
|
|
||||||
editor: Editor,
|
|
||||||
options: {
|
|
||||||
at?: Location
|
|
||||||
union?: boolean
|
|
||||||
hanging?: boolean
|
|
||||||
} = {}
|
|
||||||
): Mark[] {
|
|
||||||
warning(
|
|
||||||
false,
|
|
||||||
'The `Editor.activeMarks` helper is deprecated, use `Editor.marks` instead.'
|
|
||||||
)
|
|
||||||
|
|
||||||
return Array.from(
|
|
||||||
Editor.marks(editor, {
|
|
||||||
at: options.at,
|
|
||||||
mode: options.union ? 'distinct' : 'universal',
|
|
||||||
continuing: true,
|
|
||||||
}),
|
|
||||||
([m]) => m
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the point after a location.
|
* Get the point after a location.
|
||||||
*/
|
*/
|
||||||
@@ -305,116 +265,6 @@ export const LocationQueries = {
|
|||||||
yield* levels
|
yield* levels
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterate through all of the text nodes in the Editor.
|
|
||||||
*/
|
|
||||||
|
|
||||||
*marks(
|
|
||||||
editor: Editor,
|
|
||||||
options: {
|
|
||||||
at?: Location
|
|
||||||
match?: MarkMatch
|
|
||||||
mode?: 'all' | 'first' | 'distinct' | 'universal'
|
|
||||||
reverse?: boolean
|
|
||||||
continuing?: boolean
|
|
||||||
} = {}
|
|
||||||
): Iterable<MarkEntry> {
|
|
||||||
const { match, mode = 'all', reverse = false, continuing = false } = options
|
|
||||||
let { at = editor.selection } = options
|
|
||||||
|
|
||||||
if (!at) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the range is collapsed at the start of a text node, it should continue
|
|
||||||
// the marks from the previous text node in the same block.
|
|
||||||
if (
|
|
||||||
continuing &&
|
|
||||||
Range.isRange(at) &&
|
|
||||||
Range.isCollapsed(at) &&
|
|
||||||
at.anchor.offset === 0
|
|
||||||
) {
|
|
||||||
const { anchor } = at
|
|
||||||
const prev = Editor.previous(editor, anchor, 'text')
|
|
||||||
|
|
||||||
if (prev && Path.isSibling(anchor.path, prev[1])) {
|
|
||||||
const [, prevPath] = prev
|
|
||||||
at = Editor.range(editor, prevPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const universalMarks: Mark[] = []
|
|
||||||
const distinctMarks: Mark[] = []
|
|
||||||
const universalEntries: MarkEntry[] = []
|
|
||||||
let first = true
|
|
||||||
|
|
||||||
for (const entry of Editor.texts(editor, { reverse, at })) {
|
|
||||||
const [node, path] = entry
|
|
||||||
|
|
||||||
if (mode === 'universal') {
|
|
||||||
if (first) {
|
|
||||||
for (let i = 0; i < node.marks.length; i++) {
|
|
||||||
const mark = node.marks[i]
|
|
||||||
const markEntry: MarkEntry = [mark, i, node, path]
|
|
||||||
|
|
||||||
if (match == null || Editor.isMarkMatch(editor, markEntry, match)) {
|
|
||||||
universalMarks.push(mark)
|
|
||||||
universalEntries.push(markEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
first = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// PERF: If we're in universal mode and the eligible marks hits zero
|
|
||||||
// it can never increase again, so we can exit early.
|
|
||||||
if (universalMarks.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = universalMarks.length - 1; i >= 0; i--) {
|
|
||||||
const existing = universalMarks[i]
|
|
||||||
|
|
||||||
if (!Mark.exists(existing, node.marks)) {
|
|
||||||
universalMarks.splice(i, 1)
|
|
||||||
universalEntries.splice(i, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let index = 0; index < node.marks.length; index++) {
|
|
||||||
const mark = node.marks[index]
|
|
||||||
const markEntry: MarkEntry = [mark, index, node, path]
|
|
||||||
|
|
||||||
if (match != null && !Editor.isMarkMatch(editor, markEntry, match)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'distinct') {
|
|
||||||
if (Mark.exists(mark, distinctMarks)) {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
distinctMarks.push(mark)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
yield markEntry
|
|
||||||
|
|
||||||
// After matching a mark, if we're in first mode skip to the next text.
|
|
||||||
if (mode === 'first') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In universal mode, the marks are collected while iterating and we can
|
|
||||||
// only be certain of which are universal when we've finished.
|
|
||||||
if (mode === 'universal') {
|
|
||||||
yield* universalEntries
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the first matching node in a single branch of the document.
|
* Get the first matching node in a single branch of the document.
|
||||||
*/
|
*/
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
import { Editor, Mark, MarkEntry, MarkMatch } from '../../..'
|
|
||||||
|
|
||||||
export const MarkQueries = {
|
|
||||||
/**
|
|
||||||
* Check if a mark entry is a match.
|
|
||||||
*/
|
|
||||||
|
|
||||||
isMarkMatch(editor: Editor, entry: MarkEntry, match: MarkMatch): boolean {
|
|
||||||
if (Array.isArray(match)) {
|
|
||||||
return match.some(m => Editor.isMarkMatch(editor, entry, m))
|
|
||||||
} else if (typeof match === 'function') {
|
|
||||||
return match(entry)
|
|
||||||
} else {
|
|
||||||
return Mark.matches(entry[0], match)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
@@ -2,7 +2,6 @@ import { createDraft, finishDraft, isDraft } from 'immer'
|
|||||||
import {
|
import {
|
||||||
Node,
|
Node,
|
||||||
Editor,
|
Editor,
|
||||||
Mark,
|
|
||||||
Range,
|
Range,
|
||||||
Point,
|
Point,
|
||||||
Text,
|
Text,
|
||||||
@@ -68,17 +67,6 @@ export const GeneralTransforms = {
|
|||||||
let selection = editor.selection && createDraft(editor.selection)
|
let selection = editor.selection && createDraft(editor.selection)
|
||||||
|
|
||||||
switch (op.type) {
|
switch (op.type) {
|
||||||
case 'add_mark': {
|
|
||||||
const { path, mark } = op
|
|
||||||
const node = Node.leaf(editor, path)
|
|
||||||
|
|
||||||
if (!Mark.exists(mark, node.marks)) {
|
|
||||||
node.marks.push(mark)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'insert_node': {
|
case 'insert_node': {
|
||||||
const { path, node } = op
|
const { path, node } = op
|
||||||
const parent = Node.parent(editor, path)
|
const parent = Node.parent(editor, path)
|
||||||
@@ -174,20 +162,6 @@ export const GeneralTransforms = {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'remove_mark': {
|
|
||||||
const { path, mark } = op
|
|
||||||
const node = Node.leaf(editor, path)
|
|
||||||
|
|
||||||
for (let i = 0; i < node.marks.length; i++) {
|
|
||||||
if (Mark.matches(node.marks[i], mark)) {
|
|
||||||
node.marks.splice(i, 1)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'remove_node': {
|
case 'remove_node': {
|
||||||
const { path } = op
|
const { path } = op
|
||||||
const index = path[path.length - 1]
|
const index = path[path.length - 1]
|
||||||
@@ -238,20 +212,6 @@ export const GeneralTransforms = {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'set_mark': {
|
|
||||||
const { path, properties, newProperties } = op
|
|
||||||
const node = Node.leaf(editor, path)
|
|
||||||
|
|
||||||
for (const mark of node.marks) {
|
|
||||||
if (Mark.matches(mark, properties)) {
|
|
||||||
Object.assign(mark, newProperties)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'set_node': {
|
case 'set_node': {
|
||||||
const { path, newProperties } = op
|
const { path, newProperties } = op
|
||||||
|
|
||||||
@@ -260,7 +220,21 @@ export const GeneralTransforms = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const node = Node.get(editor, path)
|
const node = Node.get(editor, path)
|
||||||
Object.assign(node, newProperties)
|
|
||||||
|
for (const key in newProperties) {
|
||||||
|
if (key === 'children' || key === 'text') {
|
||||||
|
throw new Error(`Cannot set the "${key}" property of nodes!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = newProperties[key]
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
delete node[key]
|
||||||
|
} else {
|
||||||
|
node[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,140 +0,0 @@
|
|||||||
import { Editor, Mark, Location, Range } from '../../..'
|
|
||||||
|
|
||||||
export const MarkTransforms = {
|
|
||||||
/**
|
|
||||||
* Add a set of marks to the text nodes at a location.
|
|
||||||
*/
|
|
||||||
|
|
||||||
addMarks(
|
|
||||||
editor: Editor,
|
|
||||||
mark: Mark | Mark[],
|
|
||||||
options: {
|
|
||||||
at?: Location
|
|
||||||
hanging?: boolean
|
|
||||||
} = {}
|
|
||||||
) {
|
|
||||||
Editor.withoutNormalizing(editor, () => {
|
|
||||||
const at = splitLocation(editor, options)
|
|
||||||
|
|
||||||
if (!at) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// De-dupe the marks being added to ensure the set is unique.
|
|
||||||
const marks = Array.isArray(mark) ? mark : [mark]
|
|
||||||
const set: Mark[] = []
|
|
||||||
|
|
||||||
for (const m of marks) {
|
|
||||||
if (!Mark.exists(m, set)) {
|
|
||||||
set.push(m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [node, path] of Editor.texts(editor, { at })) {
|
|
||||||
for (const m of set) {
|
|
||||||
if (!Mark.exists(m, node.marks)) {
|
|
||||||
editor.apply({ type: 'add_mark', path, mark: m })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
removeMarks(
|
|
||||||
editor: Editor,
|
|
||||||
mark: Mark | Mark[],
|
|
||||||
options: {
|
|
||||||
at?: Location
|
|
||||||
hanging?: boolean
|
|
||||||
} = {}
|
|
||||||
) {
|
|
||||||
Editor.withoutNormalizing(editor, () => {
|
|
||||||
const at = splitLocation(editor, options)
|
|
||||||
|
|
||||||
if (at) {
|
|
||||||
const marks = Array.isArray(mark) ? mark : [mark]
|
|
||||||
for (const [m, i, node, path] of Editor.marks(editor, { at })) {
|
|
||||||
if (Mark.exists(m, marks)) {
|
|
||||||
editor.apply({ type: 'remove_mark', path, mark: m })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
setMarks(
|
|
||||||
editor: Editor,
|
|
||||||
mark: Mark | Mark[],
|
|
||||||
props: Partial<Mark>,
|
|
||||||
options: {
|
|
||||||
at?: Location
|
|
||||||
hanging?: boolean
|
|
||||||
} = {}
|
|
||||||
) {
|
|
||||||
Editor.withoutNormalizing(editor, () => {
|
|
||||||
const at = splitLocation(editor, options)
|
|
||||||
|
|
||||||
if (at) {
|
|
||||||
const marks = Array.isArray(mark) ? mark : [mark]
|
|
||||||
for (const [m, i, node, path] of Editor.marks(editor, { at })) {
|
|
||||||
if (Mark.exists(m, marks)) {
|
|
||||||
const newProps = {}
|
|
||||||
|
|
||||||
for (const k in props) {
|
|
||||||
if (props[k] !== m[k]) {
|
|
||||||
newProps[k] = props[k]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(newProps).length > 0) {
|
|
||||||
editor.apply({
|
|
||||||
type: 'set_mark',
|
|
||||||
path,
|
|
||||||
properties: m,
|
|
||||||
newProperties: newProps,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Split the text nodes at a range's edges to prepare for adding/removing marks.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const splitLocation = (
|
|
||||||
editor: Editor,
|
|
||||||
options: {
|
|
||||||
at?: Location
|
|
||||||
hanging?: boolean
|
|
||||||
} = {}
|
|
||||||
): Location | undefined => {
|
|
||||||
let { at = editor.selection, hanging = false } = options
|
|
||||||
|
|
||||||
if (!at) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Range.isRange(at)) {
|
|
||||||
if (!hanging) {
|
|
||||||
at = Editor.unhangRange(editor, at)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rangeRef = Editor.rangeRef(editor, at, { affinity: 'inward' })
|
|
||||||
const [start, end] = Range.edges(at)
|
|
||||||
Editor.splitNodes(editor, { at: end, match: 'text' })
|
|
||||||
Editor.splitNodes(editor, { at: start, match: 'text' })
|
|
||||||
const range = rangeRef.unref()!
|
|
||||||
|
|
||||||
if (options.at == null) {
|
|
||||||
Editor.select(editor, range)
|
|
||||||
}
|
|
||||||
|
|
||||||
return range
|
|
||||||
}
|
|
||||||
|
|
||||||
return at
|
|
||||||
}
|
|
@@ -270,7 +270,7 @@ export const NodeTransforms = {
|
|||||||
// Ensure that the nodes are equivalent, and figure out what the position
|
// Ensure that the nodes are equivalent, and figure out what the position
|
||||||
// and extra properties of the merge will be.
|
// and extra properties of the merge will be.
|
||||||
if (Text.isText(node) && Text.isText(prevNode)) {
|
if (Text.isText(node) && Text.isText(prevNode)) {
|
||||||
const { text, marks, ...rest } = node
|
const { text, ...rest } = node
|
||||||
position = prevNode.text.length
|
position = prevNode.text.length
|
||||||
properties = rest as Partial<Text>
|
properties = rest as Partial<Text>
|
||||||
} else if (Element.isElement(node) && Element.isElement(prevNode)) {
|
} else if (Element.isElement(node) && Element.isElement(prevNode)) {
|
||||||
@@ -413,7 +413,7 @@ export const NodeTransforms = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set new properties on the nodes ...
|
* Set new properties on the nodes at a location.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
setNodes(
|
setNodes(
|
||||||
@@ -422,12 +422,14 @@ export const NodeTransforms = {
|
|||||||
options: {
|
options: {
|
||||||
at?: Location
|
at?: Location
|
||||||
match?: NodeMatch
|
match?: NodeMatch
|
||||||
|
mode?: 'all' | 'highest'
|
||||||
hanging?: boolean
|
hanging?: boolean
|
||||||
|
split?: boolean
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
Editor.withoutNormalizing(editor, () => {
|
Editor.withoutNormalizing(editor, () => {
|
||||||
let { match, at = editor.selection } = options
|
let { match, at = editor.selection } = options
|
||||||
const { hanging = false } = options
|
const { hanging = false, mode = 'highest', split = false } = options
|
||||||
|
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
if (Path.isPath(at)) {
|
if (Path.isPath(at)) {
|
||||||
@@ -446,21 +448,29 @@ export const NodeTransforms = {
|
|||||||
at = Editor.unhangRange(editor, at)
|
at = Editor.unhangRange(editor, at)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [node, path] of Editor.nodes(editor, {
|
if (split && Range.isRange(at)) {
|
||||||
at,
|
const rangeRef = Editor.rangeRef(editor, at, { affinity: 'inward' })
|
||||||
match,
|
const [start, end] = Range.edges(at)
|
||||||
mode: 'highest',
|
Editor.splitNodes(editor, { at: end, match })
|
||||||
})) {
|
Editor.splitNodes(editor, { at: start, match })
|
||||||
|
at = rangeRef.unref()!
|
||||||
|
|
||||||
|
if (options.at == null) {
|
||||||
|
Editor.select(editor, at)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [node, path] of Editor.nodes(editor, { at, match, mode })) {
|
||||||
const properties: Partial<Node> = {}
|
const properties: Partial<Node> = {}
|
||||||
const newProperties: Partial<Node> = {}
|
const newProperties: Partial<Node> = {}
|
||||||
|
|
||||||
|
// You can't set properties on the editor node.
|
||||||
|
if (path.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for (const k in props) {
|
for (const k in props) {
|
||||||
if (
|
if (k === 'children' || k === 'text') {
|
||||||
k === 'marks' ||
|
|
||||||
k === 'children' ||
|
|
||||||
k === 'selection' ||
|
|
||||||
k === 'text'
|
|
||||||
) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,7 +550,7 @@ export const NodeTransforms = {
|
|||||||
let after = Editor.after(editor, voidPath)
|
let after = Editor.after(editor, voidPath)
|
||||||
|
|
||||||
if (!after) {
|
if (!after) {
|
||||||
const text = { text: '', marks: [] }
|
const text = { text: '' }
|
||||||
const afterPath = Path.next(voidPath)
|
const afterPath = Path.next(voidPath)
|
||||||
Editor.insertNodes(editor, text, { at: afterPath })
|
Editor.insertNodes(editor, text, { at: afterPath })
|
||||||
after = Editor.point(editor, afterPath)!
|
after = Editor.point(editor, afterPath)!
|
||||||
@@ -581,7 +591,7 @@ export const NodeTransforms = {
|
|||||||
|
|
||||||
if (always || !beforeRef || !Editor.isEdge(editor, point, path)) {
|
if (always || !beforeRef || !Editor.isEdge(editor, point, path)) {
|
||||||
split = true
|
split = true
|
||||||
const { text, marks, children, ...properties } = node
|
const { text, children, ...properties } = node
|
||||||
editor.apply({
|
editor.apply({
|
||||||
type: 'split_node',
|
type: 'split_node',
|
||||||
path,
|
path,
|
||||||
|
@@ -1,68 +0,0 @@
|
|||||||
import isPlainObject from 'is-plain-object'
|
|
||||||
import { Path, Text } from '..'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `Mark` objects represent formatting that is applied to text in a Slate
|
|
||||||
* document. They appear in leaf text nodes in the document.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface Mark {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Mark = {
|
|
||||||
/**
|
|
||||||
* Check if a mark exists in a set of marks.
|
|
||||||
*/
|
|
||||||
|
|
||||||
exists(mark: Mark, marks: Mark[]): boolean {
|
|
||||||
return !!marks.find(f => Mark.matches(f, mark))
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a value implements the `Mark` interface.
|
|
||||||
*/
|
|
||||||
|
|
||||||
isMark(value: any): value is Mark {
|
|
||||||
return isPlainObject(value)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a value is an array of `Mark` objects.
|
|
||||||
*/
|
|
||||||
|
|
||||||
isMarkSet(value: any): value is Mark[] {
|
|
||||||
return Array.isArray(value) && (value.length === 0 || Mark.isMark(value[0]))
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a mark matches set of properties.
|
|
||||||
*/
|
|
||||||
|
|
||||||
matches(mark: Mark, props: Partial<Mark>): boolean {
|
|
||||||
for (const key in props) {
|
|
||||||
if (mark[key] !== props[key]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `MarkEntry` tuples are returned when iterating through the marks in a text
|
|
||||||
* node. They include the index of the mark in the text node's marks array, as
|
|
||||||
* well as the text node and its path in the root node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type MarkEntry = [Mark, number, Text, Path]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `MarkMatch` values are used as shorthands for matching mark objects.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type MarkMatch =
|
|
||||||
| Partial<Mark>
|
|
||||||
| ((entry: MarkEntry) => boolean)
|
|
||||||
| MarkMatch[]
|
|
@@ -1,14 +1,5 @@
|
|||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import {
|
import { Editor, Element, ElementEntry, Path, Range, Text, TextEntry } from '..'
|
||||||
Editor,
|
|
||||||
Element,
|
|
||||||
ElementEntry,
|
|
||||||
MarkEntry,
|
|
||||||
Path,
|
|
||||||
Range,
|
|
||||||
Text,
|
|
||||||
TextEntry,
|
|
||||||
} from '..'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `Node` union type represents all of the different types of nodes that
|
* The `Node` union type represents all of the different types of nodes that
|
||||||
@@ -372,27 +363,6 @@ export const Node = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an iterable of all the marks in all of the text nodes in a root node.
|
|
||||||
*/
|
|
||||||
|
|
||||||
*marks(
|
|
||||||
root: Node,
|
|
||||||
options: {
|
|
||||||
from?: Path
|
|
||||||
to?: Path
|
|
||||||
reverse?: boolean
|
|
||||||
pass?: (node: NodeEntry) => boolean
|
|
||||||
} = {}
|
|
||||||
): Iterable<MarkEntry> {
|
|
||||||
for (const [node, path] of Node.texts(root, options)) {
|
|
||||||
for (let i = 0; i < node.marks.length; i++) {
|
|
||||||
const mark = node.marks[i]
|
|
||||||
yield [mark, i, node, path]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an iterable of all the node entries of a root node. Each entry is
|
* Return an iterable of all the node entries of a root node. Each entry is
|
||||||
* returned as a `[Node, Path]` tuple, with the path referring to the node's
|
* returned as a `[Node, Path]` tuple, with the path referring to the node's
|
||||||
|
@@ -1,13 +1,6 @@
|
|||||||
import { Mark, Node, Path, Range } from '..'
|
import { Node, Path, Range } from '..'
|
||||||
import isPlainObject from 'is-plain-object'
|
import isPlainObject from 'is-plain-object'
|
||||||
|
|
||||||
type AddMarkOperation = {
|
|
||||||
type: 'add_mark'
|
|
||||||
path: Path
|
|
||||||
mark: Mark
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
type InsertNodeOperation = {
|
type InsertNodeOperation = {
|
||||||
type: 'insert_node'
|
type: 'insert_node'
|
||||||
path: Path
|
path: Path
|
||||||
@@ -39,13 +32,6 @@ type MoveNodeOperation = {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoveMarkOperation = {
|
|
||||||
type: 'remove_mark'
|
|
||||||
path: Path
|
|
||||||
mark: Mark
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemoveNodeOperation = {
|
type RemoveNodeOperation = {
|
||||||
type: 'remove_node'
|
type: 'remove_node'
|
||||||
path: Path
|
path: Path
|
||||||
@@ -61,14 +47,6 @@ type RemoveTextOperation = {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
type SetMarkOperation = {
|
|
||||||
type: 'set_mark'
|
|
||||||
path: Path
|
|
||||||
properties: Partial<Mark>
|
|
||||||
newProperties: Partial<Mark>
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetNodeOperation = {
|
type SetNodeOperation = {
|
||||||
type: 'set_node'
|
type: 'set_node'
|
||||||
path: Path
|
path: Path
|
||||||
@@ -113,11 +91,7 @@ type SplitNodeOperation = {
|
|||||||
* collaboration, and other features.
|
* collaboration, and other features.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type Operation =
|
type Operation = NodeOperation | SelectionOperation | TextOperation
|
||||||
| NodeOperation
|
|
||||||
| MarkOperation
|
|
||||||
| SelectionOperation
|
|
||||||
| TextOperation
|
|
||||||
|
|
||||||
type NodeOperation =
|
type NodeOperation =
|
||||||
| InsertNodeOperation
|
| InsertNodeOperation
|
||||||
@@ -127,8 +101,6 @@ type NodeOperation =
|
|||||||
| SetNodeOperation
|
| SetNodeOperation
|
||||||
| SplitNodeOperation
|
| SplitNodeOperation
|
||||||
|
|
||||||
type MarkOperation = AddMarkOperation | RemoveMarkOperation | SetMarkOperation
|
|
||||||
|
|
||||||
type SelectionOperation = SetSelectionOperation
|
type SelectionOperation = SetSelectionOperation
|
||||||
|
|
||||||
type TextOperation = InsertTextOperation | RemoveTextOperation
|
type TextOperation = InsertTextOperation | RemoveTextOperation
|
||||||
@@ -142,14 +114,6 @@ const Operation = {
|
|||||||
return Operation.isOperation(value) && value.type.endsWith('_node')
|
return Operation.isOperation(value) && value.type.endsWith('_node')
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Check of a value is a `MarkOperation` object.
|
|
||||||
*/
|
|
||||||
|
|
||||||
isMarkOperation(value: any): value is MarkOperation {
|
|
||||||
return Operation.isOperation(value) && value.type.endsWith('_mark')
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check of a value is an `Operation` object.
|
* Check of a value is an `Operation` object.
|
||||||
*/
|
*/
|
||||||
@@ -160,10 +124,6 @@ const Operation = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case 'add_mark': {
|
|
||||||
return Path.isPath(value.path) && Mark.isMark(value.mark)
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'insert_node': {
|
case 'insert_node': {
|
||||||
return Path.isPath(value.path) && Node.isNode(value.node)
|
return Path.isPath(value.path) && Node.isNode(value.node)
|
||||||
}
|
}
|
||||||
@@ -189,10 +149,6 @@ const Operation = {
|
|||||||
return Path.isPath(value.path) && Path.isPath(value.newPath)
|
return Path.isPath(value.path) && Path.isPath(value.newPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'remove_mark': {
|
|
||||||
return Path.isPath(value.path) && Mark.isMark(value.mark)
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'remove_node': {
|
case 'remove_node': {
|
||||||
return Path.isPath(value.path) && Node.isNode(value.node)
|
return Path.isPath(value.path) && Node.isNode(value.node)
|
||||||
}
|
}
|
||||||
@@ -205,14 +161,6 @@ const Operation = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'set_mark': {
|
|
||||||
return (
|
|
||||||
Path.isPath(value.path) &&
|
|
||||||
isPlainObject(value.properties) &&
|
|
||||||
isPlainObject(value.newProperties)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'set_node': {
|
case 'set_node': {
|
||||||
return (
|
return (
|
||||||
Path.isPath(value.path) &&
|
Path.isPath(value.path) &&
|
||||||
@@ -285,10 +233,6 @@ const Operation = {
|
|||||||
|
|
||||||
inverse(op: Operation): Operation {
|
inverse(op: Operation): Operation {
|
||||||
switch (op.type) {
|
switch (op.type) {
|
||||||
case 'add_mark': {
|
|
||||||
return { ...op, type: 'remove_mark' }
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'insert_node': {
|
case 'insert_node': {
|
||||||
return { ...op, type: 'remove_node' }
|
return { ...op, type: 'remove_node' }
|
||||||
}
|
}
|
||||||
@@ -317,10 +261,6 @@ const Operation = {
|
|||||||
return { ...op, path: inversePath, newPath: inverseNewPath }
|
return { ...op, path: inversePath, newPath: inverseNewPath }
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'remove_mark': {
|
|
||||||
return { ...op, type: 'add_mark' }
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'remove_node': {
|
case 'remove_node': {
|
||||||
return { ...op, type: 'insert_node' }
|
return { ...op, type: 'insert_node' }
|
||||||
}
|
}
|
||||||
@@ -329,7 +269,6 @@ const Operation = {
|
|||||||
return { ...op, type: 'insert_text' }
|
return { ...op, type: 'insert_text' }
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'set_mark':
|
|
||||||
case 'set_node': {
|
case 'set_node': {
|
||||||
const { properties, newProperties } = op
|
const { properties, newProperties } = op
|
||||||
return { ...op, properties: newProperties, newProperties: properties }
|
return { ...op, properties: newProperties, newProperties: properties }
|
||||||
@@ -363,15 +302,12 @@ const Operation = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AddMarkOperation,
|
|
||||||
InsertNodeOperation,
|
InsertNodeOperation,
|
||||||
InsertTextOperation,
|
InsertTextOperation,
|
||||||
MergeNodeOperation,
|
MergeNodeOperation,
|
||||||
MoveNodeOperation,
|
MoveNodeOperation,
|
||||||
RemoveMarkOperation,
|
|
||||||
RemoveNodeOperation,
|
RemoveNodeOperation,
|
||||||
RemoveTextOperation,
|
RemoveTextOperation,
|
||||||
SetMarkOperation,
|
|
||||||
SetNodeOperation,
|
SetNodeOperation,
|
||||||
SetSelectionOperation,
|
SetSelectionOperation,
|
||||||
SplitNodeOperation,
|
SplitNodeOperation,
|
||||||
|
@@ -1,29 +1,58 @@
|
|||||||
import isPlainObject from 'is-plain-object'
|
import isPlainObject from 'is-plain-object'
|
||||||
import { Mark, Path } from '..'
|
import { Path } from '..'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `Text` objects represent the nodes that contain the actual text content of a
|
* `Text` objects represent the nodes that contain the actual text content of a
|
||||||
* Slate document along with any formatting marks. They are always leaf nodes in
|
* Slate document along with any formatting properties. They are always leaf
|
||||||
* the document tree as they cannot contain any children.
|
* nodes in the document tree as they cannot contain any children.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Text {
|
export interface Text {
|
||||||
text: string
|
text: string
|
||||||
marks: Mark[]
|
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Text = {
|
export const Text = {
|
||||||
|
/**
|
||||||
|
* Check if two text nodes are equal.
|
||||||
|
*/
|
||||||
|
|
||||||
|
equals(
|
||||||
|
text: Text,
|
||||||
|
another: Text,
|
||||||
|
options: { loose?: boolean } = {}
|
||||||
|
): boolean {
|
||||||
|
const { loose = false } = options
|
||||||
|
|
||||||
|
for (const key in text) {
|
||||||
|
if (loose && key === 'text') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text[key] !== another[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in another) {
|
||||||
|
if (loose && key === 'text') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text[key] !== another[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a value implements the `Text` interface.
|
* Check if a value implements the `Text` interface.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
isText(value: any): value is Text {
|
isText(value: any): value is Text {
|
||||||
return (
|
return isPlainObject(value) && typeof value.text === 'string'
|
||||||
isPlainObject(value) &&
|
|
||||||
typeof value.text === 'string' &&
|
|
||||||
Array.isArray(value.marks)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,8 +67,7 @@ export const Text = {
|
|||||||
* Check if an text matches set of properties.
|
* Check if an text matches set of properties.
|
||||||
*
|
*
|
||||||
* Note: this is for matching custom properties, and it does not ensure that
|
* Note: this is for matching custom properties, and it does not ensure that
|
||||||
* the `text` property are two nodes equal. However, if `marks` are passed it
|
* the `text` property are two nodes equal.
|
||||||
* will ensure that the set of marks is exactly equal.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
matches(text: Text, props: Partial<Text>): boolean {
|
matches(text: Text, props: Partial<Text>): boolean {
|
||||||
@@ -48,30 +76,6 @@ export const Text = {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'marks' && props.marks != null) {
|
|
||||||
const existing = text.marks
|
|
||||||
const { marks } = props
|
|
||||||
|
|
||||||
// PERF: If the lengths aren't the same, we know it's not a match.
|
|
||||||
if (existing.length !== marks.length) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const m of existing) {
|
|
||||||
if (!Mark.exists(m, marks)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const m of marks) {
|
|
||||||
if (!Mark.exists(m, existing)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text[key] !== props[key]) {
|
if (text[key] !== props[key]) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ import { Element } from 'slate'
|
|||||||
|
|
||||||
export const input = {
|
export const input = {
|
||||||
text: '',
|
text: '',
|
||||||
marks: [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const test = value => {
|
export const test = value => {
|
||||||
|
@@ -3,7 +3,6 @@ import { Element } from 'slate'
|
|||||||
export const input = [
|
export const input = [
|
||||||
{
|
{
|
||||||
text: '',
|
text: '',
|
||||||
marks: [],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@@ -1,12 +0,0 @@
|
|||||||
import { Mark } from 'slate'
|
|
||||||
|
|
||||||
export const input = {
|
|
||||||
element: { children: [], type: 'bold', other: true },
|
|
||||||
props: { type: 'bold' },
|
|
||||||
}
|
|
||||||
|
|
||||||
export const test = ({ element, props }) => {
|
|
||||||
return Mark.matches(element, props)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const output = true
|
|
@@ -1,12 +0,0 @@
|
|||||||
import { Mark } from 'slate'
|
|
||||||
|
|
||||||
export const input = {
|
|
||||||
element: { children: [], type: 'bold' },
|
|
||||||
props: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const test = ({ mark, props }) => {
|
|
||||||
return Mark.matches(mark, props)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const output = true
|
|
@@ -1,12 +0,0 @@
|
|||||||
import { Mark } from 'slate'
|
|
||||||
|
|
||||||
export const input = {
|
|
||||||
mark: { type: 'bold' },
|
|
||||||
marks: [{ type: 'bold' }],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const test = ({ mark, marks }) => {
|
|
||||||
return Mark.exists(mark, marks)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const output = true
|
|
@@ -1,12 +0,0 @@
|
|||||||
import { Mark } from 'slate'
|
|
||||||
|
|
||||||
export const input = {
|
|
||||||
mark: { type: 'bold' },
|
|
||||||
marks: [{ type: 'italic' }],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const test = ({ mark, marks }) => {
|
|
||||||
return Mark.exists(mark, marks)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const output = false
|
|
@@ -1,12 +0,0 @@
|
|||||||
import { Mark } from 'slate'
|
|
||||||
|
|
||||||
export const input = {
|
|
||||||
mark: { type: 'bold' },
|
|
||||||
marks: [{ type: 'bold', other: true }],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const test = ({ mark, marks }) => {
|
|
||||||
return Mark.exists(mark, marks)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const output = true
|
|
@@ -1,12 +0,0 @@
|
|||||||
import { Mark } from 'slate'
|
|
||||||
|
|
||||||
export const input = {
|
|
||||||
mark: {},
|
|
||||||
marks: [{}],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const test = ({ mark, marks }) => {
|
|
||||||
return Mark.exists(mark, marks)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const output = true
|
|
@@ -1,12 +0,0 @@
|
|||||||
import { Mark } from 'slate'
|
|
||||||
|
|
||||||
export const input = {
|
|
||||||
mark: {},
|
|
||||||
marks: [{ type: 'bold' }],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const test = ({ mark, marks }) => {
|
|
||||||
return Mark.exists(mark, marks)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const output = true
|
|
@@ -1,12 +0,0 @@
|
|||||||
import { Mark } from 'slate'
|
|
||||||
|
|
||||||
export const input = {
|
|
||||||
mark: {},
|
|
||||||
marks: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const test = ({ mark, marks }) => {
|
|
||||||
return Mark.exists(mark, marks)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const output = false
|
|
@@ -1,9 +0,0 @@
|
|||||||
import { Mark } from 'slate'
|
|
||||||
|
|
||||||
export const input = true
|
|
||||||
|
|
||||||
export const test = value => {
|
|
||||||
return Mark.isMark(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const output = false
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user