mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-11 17:53:59 +02:00
Merge branch 'master' of github.com:ianstormtaylor/slate
This commit is contained in:
@@ -538,8 +538,8 @@ Wrap the given node in a [`Inline`](./inline.md) node that match `properties`. F
|
|||||||
|
|
||||||
### `wrapNodeByKey/Path`
|
### `wrapNodeByKey/Path`
|
||||||
|
|
||||||
`wraNodeByKey(key: String, parent: Node) => Editor` <br/>
|
`wrapNodeByKey(key: String, parent: Node) => Editor` <br/>
|
||||||
`wraNodeByPath(path: List, parent: Node) => Editor` <br/>
|
`wrapNodeByPath(path: List, parent: Node) => Editor` <br/>
|
||||||
|
|
||||||
Wrap the node with the specified key with the parent [`Node`](./node.md). This will clear all children of the parent.
|
Wrap the node with the specified key with the parent [`Node`](./node.md). This will clear all children of the parent.
|
||||||
|
|
||||||
|
@@ -11,6 +11,8 @@ A text node in a Slate [`Document`](./document.md). Text nodes are always the bo
|
|||||||
```js
|
```js
|
||||||
Text({
|
Text({
|
||||||
key: String,
|
key: String,
|
||||||
|
text: String,
|
||||||
|
marks: Immutable.List<Mark>,
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -20,20 +22,24 @@ Text({
|
|||||||
|
|
||||||
A unique identifier for the node.
|
A unique identifier for the node.
|
||||||
|
|
||||||
|
### `text`
|
||||||
|
|
||||||
|
`String`
|
||||||
|
|
||||||
|
The text contents of this node.
|
||||||
|
|
||||||
|
### `marks`
|
||||||
|
|
||||||
|
`Immutable.List<Mark>,`
|
||||||
|
|
||||||
|
A list of marks for this node.
|
||||||
|
|
||||||
### `object`
|
### `object`
|
||||||
|
|
||||||
`String`
|
`String`
|
||||||
|
|
||||||
An immutable string value of `'text'` for easily separating this node from [`Inline`](./inline.md) or [`Block`](./block.md) nodes.
|
An immutable string value of `'text'` for easily separating this node from [`Inline`](./inline.md) or [`Block`](./block.md) nodes.
|
||||||
|
|
||||||
## Computed Properties
|
|
||||||
|
|
||||||
### `text`
|
|
||||||
|
|
||||||
`String`
|
|
||||||
|
|
||||||
A concatenated string of all of the characters in the text node.
|
|
||||||
|
|
||||||
## Static Methods
|
## Static Methods
|
||||||
|
|
||||||
### `Text.create`
|
### `Text.create`
|
||||||
|
@@ -89,18 +89,18 @@ class App extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
// Pass in the `renderNode` prop...
|
// Pass in the `renderBlock` prop...
|
||||||
<Editor
|
<Editor
|
||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
renderNode={this.renderNode}
|
renderBlock={this.renderBlock}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a `renderNode` method to render a `CodeNode` for code blocks.
|
// Add a `renderBlock` method to render a `CodeNode` for code blocks.
|
||||||
renderNode = (props, editor, next) => {
|
renderBlock = (props, editor, next) => {
|
||||||
switch (props.node.type) {
|
switch (props.node.type) {
|
||||||
case 'code':
|
case 'code':
|
||||||
return <CodeNode {...props} />
|
return <CodeNode {...props} />
|
||||||
@@ -148,12 +148,12 @@ class App extends React.Component {
|
|||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
renderNode={this.renderNode}
|
renderBlock={this.renderBlock}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNode = (props, editor, next) => {
|
renderBlock = (props, editor, next) => {
|
||||||
switch (props.node.type) {
|
switch (props.node.type) {
|
||||||
case 'code':
|
case 'code':
|
||||||
return <CodeNode {...props} />
|
return <CodeNode {...props} />
|
||||||
@@ -206,12 +206,12 @@ class App extends React.Component {
|
|||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
renderNode={this.renderNode}
|
renderBlock={this.renderBlock}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNode = (props, editor, next) => {
|
renderBlock = (props, editor, next) => {
|
||||||
switch (props.node.type) {
|
switch (props.node.type) {
|
||||||
case 'code':
|
case 'code':
|
||||||
return <CodeNode {...props} />
|
return <CodeNode {...props} />
|
||||||
|
@@ -258,7 +258,7 @@ class App extends React.Component {
|
|||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
// Add the ability to render our nodes and marks...
|
// Add the ability to render our nodes and marks...
|
||||||
renderNode={this.renderNode}
|
renderBlock={this.renderNode}
|
||||||
renderMark={this.renderMark}
|
renderMark={this.renderMark}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@@ -27,6 +27,7 @@ import PlainText from './plain-text'
|
|||||||
import Plugins from './plugins'
|
import Plugins from './plugins'
|
||||||
import RTL from './rtl'
|
import RTL from './rtl'
|
||||||
import ReadOnly from './read-only'
|
import ReadOnly from './read-only'
|
||||||
|
import RestoreDOM from './restore-dom'
|
||||||
import RichText from './rich-text'
|
import RichText from './rich-text'
|
||||||
import SearchHighlighting from './search-highlighting'
|
import SearchHighlighting from './search-highlighting'
|
||||||
import Composition from './composition'
|
import Composition from './composition'
|
||||||
@@ -63,6 +64,7 @@ const EXAMPLES = [
|
|||||||
['Plain Text', PlainText, '/plain-text'],
|
['Plain Text', PlainText, '/plain-text'],
|
||||||
['Plugins', Plugins, '/plugins'],
|
['Plugins', Plugins, '/plugins'],
|
||||||
['Read-only', ReadOnly, '/read-only'],
|
['Read-only', ReadOnly, '/read-only'],
|
||||||
|
['Restore DOM', RestoreDOM, '/restore-dom'],
|
||||||
['Rich Text', RichText, '/rich-text'],
|
['Rich Text', RichText, '/rich-text'],
|
||||||
['RTL', RTL, '/rtl'],
|
['RTL', RTL, '/rtl'],
|
||||||
['Search Highlighting', SearchHighlighting, '/search-highlighting'],
|
['Search Highlighting', SearchHighlighting, '/search-highlighting'],
|
||||||
|
@@ -19,6 +19,52 @@ export const Button = React.forwardRef(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const EditorValue = React.forwardRef(
|
||||||
|
({ className, value, ...props }, ref) => {
|
||||||
|
const textLines = value.document.nodes
|
||||||
|
.map(node => node.text)
|
||||||
|
.toArray()
|
||||||
|
.join('\n')
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
className,
|
||||||
|
css`
|
||||||
|
margin: 30px -20px 0;
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 5px 20px;
|
||||||
|
color: #404040;
|
||||||
|
border-top: 2px solid #eeeeee;
|
||||||
|
background: #f8f8f8;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Slate's value as text
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
color: #404040;
|
||||||
|
font: 12px monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
padding: 10px 20px;
|
||||||
|
div {
|
||||||
|
margin: 0 0 0.5em;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{textLines}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export const Icon = React.forwardRef(({ className, ...props }, ref) => (
|
export const Icon = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<span
|
<span
|
||||||
{...props}
|
{...props}
|
||||||
@@ -34,6 +80,23 @@ export const Icon = React.forwardRef(({ className, ...props }, ref) => (
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|
||||||
|
export const Instruction = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
className={cx(
|
||||||
|
className,
|
||||||
|
css`
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin: 0 -20px 10px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #f8f8e8;
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
export const Menu = React.forwardRef(({ className, ...props }, ref) => (
|
export const Menu = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
|
@@ -8,7 +8,7 @@ import splitJoin from './split-join.js'
|
|||||||
import insert from './insert.js'
|
import insert from './insert.js'
|
||||||
import special from './special.js'
|
import special from './special.js'
|
||||||
import { isKeyHotkey } from 'is-hotkey'
|
import { isKeyHotkey } from 'is-hotkey'
|
||||||
import { Button, Icon, Toolbar } from '../components'
|
import { Button, EditorValue, Icon, Instruction, Toolbar } from '../components'
|
||||||
import { ANDROID_API_VERSION } from 'slate-dev-environment'
|
import { ANDROID_API_VERSION } from 'slate-dev-environment'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,23 +25,11 @@ const DEFAULT_NODE = 'paragraph'
|
|||||||
* @type {Component}
|
* @type {Component}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Instruction = props => (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
className={css`
|
|
||||||
white-space: pre-wrap;
|
|
||||||
margin: -1em -1em 1em;
|
|
||||||
padding: 0.5em;
|
|
||||||
background: #eee;
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
const Tabs = props => (
|
const Tabs = props => (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
className={css`
|
className={css`
|
||||||
margin-bottom: 0.5em;
|
margin: -10px -10px 0;
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -52,11 +40,13 @@ const Tab = ({ active, ...props }) => (
|
|||||||
className={css`
|
className={css`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: black;
|
font-size: 14px;
|
||||||
background: ${active ? '#AAA' : '#DDD'};
|
color: ${active ? 'black' : '#808080'};
|
||||||
padding: 0.25em 0.5em;
|
background: ${active ? '#f8f8e8' : '#fff'};
|
||||||
border-radius: 0.25em;
|
padding: 10px;
|
||||||
margin-right: 0.25em;
|
margin-right: 0.25em;
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-top-right-radius: 5px;
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -73,45 +63,6 @@ const Version = props => (
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const EditorText = props => (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
className={css`
|
|
||||||
color: #808080;
|
|
||||||
background: #f0f0f0;
|
|
||||||
font: 12px monospace;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
margin: 1em -1em;
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
div {
|
|
||||||
margin: 0 0 0.5em;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
const EditorTextCaption = props => (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
className={css`
|
|
||||||
color: white;
|
|
||||||
background: #808080;
|
|
||||||
padding: 0.5em;
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract lines of text from `Value`
|
|
||||||
*
|
|
||||||
* @return {String[]}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getTextLines(value) {
|
|
||||||
return value.document.nodes.map(node => node.text).toArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subpages which are each a smoke test.
|
* Subpages which are each a smoke test.
|
||||||
*
|
*
|
||||||
@@ -210,29 +161,23 @@ class RichTextExample extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { text } = this.state
|
const { text } = this.state
|
||||||
if (text == null) return <Redirect to="/composition/split-join" />
|
if (text == null) return <Redirect to="/composition/split-join" />
|
||||||
const textLines = getTextLines(this.state.value)
|
// const textLines = getTextLines(this.state.value)
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Instruction>
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
{SUBPAGES.map(([name, Component, subpage]) => {
|
{SUBPAGES.map(([name, Component, subpage]) => {
|
||||||
const active = subpage === this.props.params.subpage
|
const active = subpage === this.props.params.subpage
|
||||||
return (
|
return (
|
||||||
<Tab
|
<Tab key={subpage} to={`/composition/${subpage}`} active={active}>
|
||||||
key={subpage}
|
|
||||||
to={`/composition/${subpage}`}
|
|
||||||
active={active}
|
|
||||||
>
|
|
||||||
{name}
|
{name}
|
||||||
</Tab>
|
</Tab>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<Version>
|
<Version>
|
||||||
{ANDROID_API_VERSION
|
{ANDROID_API_VERSION ? `Android API ${ANDROID_API_VERSION}` : null}
|
||||||
? `Android API ${ANDROID_API_VERSION}`
|
|
||||||
: null}
|
|
||||||
</Version>
|
</Version>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
<Instruction>
|
||||||
<div>{this.state.text}</div>
|
<div>{this.state.text}</div>
|
||||||
</Instruction>
|
</Instruction>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
@@ -257,12 +202,7 @@ class RichTextExample extends React.Component {
|
|||||||
renderBlock={this.renderBlock}
|
renderBlock={this.renderBlock}
|
||||||
renderMark={this.renderMark}
|
renderMark={this.renderMark}
|
||||||
/>
|
/>
|
||||||
<EditorText>
|
<EditorValue value={this.state.value} />
|
||||||
<EditorTextCaption>Text in Slate's `Value`</EditorTextCaption>
|
|
||||||
{textLines.map((line, index) => (
|
|
||||||
<div key={index}>{line.length > 0 ? line : ' '}</div>
|
|
||||||
))}
|
|
||||||
</EditorText>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +1,23 @@
|
|||||||
import { p, bold } from './util'
|
import { p, text, bold } from './util'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
text: `Follow the instructions on each line exactly`,
|
text: `Follow the instructions on each line exactly`,
|
||||||
document: {
|
document: {
|
||||||
nodes: [
|
nodes: [
|
||||||
p(bold('Type "it is". cursor to "i|t" then hit enter.')),
|
p(bold('Type "it is". cursor to "i|t" then hit enter.')),
|
||||||
p(''),
|
p(text('')),
|
||||||
p(
|
p(
|
||||||
bold(
|
bold(
|
||||||
'Cursor to "mid|dle" then press space, backspace, space, backspace. Should say "middle".'
|
'Cursor to "mid|dle" then press space, backspace, space, backspace. Should say "middle".'
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
p('The middle word.'),
|
p(text('The middle word.')),
|
||||||
p(
|
p(
|
||||||
bold(
|
bold(
|
||||||
'Cursor in line below. Wait for caps on keyboard to show up. If not try again. Type "It me. No." and it should not mangle on the last period.'
|
'Cursor in line below. Wait for caps on keyboard to show up. If not try again. Type "It me. No." and it should not mangle on the last period.'
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
p(''),
|
p(text('')),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -2,14 +2,14 @@ export function p(...leaves) {
|
|||||||
return {
|
return {
|
||||||
object: 'block',
|
object: 'block',
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
nodes: [{ object: 'text', leaves }],
|
nodes: leaves,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function text(textContent) {
|
export function text(textContent) {
|
||||||
return { text: textContent }
|
return { object: 'text', text: textContent }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bold(textContent) {
|
export function bold(textContent) {
|
||||||
return { text: textContent, marks: [{ type: 'bold' }] }
|
return { object: 'text', text: textContent, marks: [{ type: 'bold' }] }
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Editor, getEventRange, getEventTransfer } from 'slate-react'
|
import { Editor, getEventTransfer } from 'slate-react'
|
||||||
import { Block, Value } from 'slate'
|
import { Block, Value } from 'slate'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -181,7 +181,7 @@ class Images extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onDropOrPaste = (event, editor, next) => {
|
onDropOrPaste = (event, editor, next) => {
|
||||||
const target = getEventRange(event, editor)
|
const target = editor.findEventRange(event)
|
||||||
if (!target && event.type === 'drop') return next()
|
if (!target && event.type === 'drop') return next()
|
||||||
|
|
||||||
const transfer = getEventTransfer(event)
|
const transfer = getEventTransfer(event)
|
||||||
|
224
examples/restore-dom/index.js
Normal file
224
examples/restore-dom/index.js
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { Editor } from 'slate-react'
|
||||||
|
import { Value } from 'slate'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import initialValue from './value.json'
|
||||||
|
import { Button, EditorValue, Icon, Instruction, Toolbar } from '../components'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Restore DOM example.
|
||||||
|
*
|
||||||
|
* This shows the usage of the `restoreDOM` command to rebuild the editor from
|
||||||
|
* scratch causing all the nodes to force render even if there are no changes
|
||||||
|
* to the DOM.
|
||||||
|
*
|
||||||
|
* The `onClickHighlight` method changes the internal state but normally the
|
||||||
|
* change is not rendered because there is no change to Slate's internal
|
||||||
|
* `value`.
|
||||||
|
*
|
||||||
|
* RestoreDOM also blows away the old render which makes it safe if the DOM
|
||||||
|
* has been altered outside of React.
|
||||||
|
*
|
||||||
|
* @type {Component}
|
||||||
|
*/
|
||||||
|
|
||||||
|
class RestoreDOMExample extends React.Component {
|
||||||
|
/**
|
||||||
|
* Deserialize the initial editor value and set an initial highlight color.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
state = {
|
||||||
|
value: Value.fromJSON(initialValue),
|
||||||
|
bgcolor: '#ffeecc',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a reference to the `editor`.
|
||||||
|
*
|
||||||
|
* @param {Editor} editor
|
||||||
|
*/
|
||||||
|
|
||||||
|
ref = editor => {
|
||||||
|
this.editor = editor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render.
|
||||||
|
*
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Instruction>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Click a brush to change color in state. Use refresh button to
|
||||||
|
`restoreDOM` which renders changes.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Press `!` button to corrupt DOM by removing `bold`. Backspace from
|
||||||
|
start of `text` 5 times. Console will show error but Slate will
|
||||||
|
recover by restoring DOM.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</Instruction>
|
||||||
|
<Toolbar>
|
||||||
|
{this.renderHighlightButton('#ffeecc')}
|
||||||
|
{this.renderHighlightButton('#ffffcc')}
|
||||||
|
{this.renderHighlightButton('#ccffcc')}
|
||||||
|
{this.renderCorruptButton()}
|
||||||
|
{this.renderRestoreButton()}
|
||||||
|
</Toolbar>
|
||||||
|
<Editor
|
||||||
|
spellCheck
|
||||||
|
autoFocus
|
||||||
|
placeholder="Enter some text..."
|
||||||
|
ref={this.ref}
|
||||||
|
value={this.state.value}
|
||||||
|
onChange={this.onChange}
|
||||||
|
renderBlock={this.renderBlock}
|
||||||
|
renderMark={this.renderMark}
|
||||||
|
/>
|
||||||
|
<EditorValue value={this.state.value} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a highlight button
|
||||||
|
*
|
||||||
|
* @param {String} bgcolor
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderHighlightButton = bgcolor => {
|
||||||
|
const isActive = this.state.bgcolor === bgcolor
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
active={isActive}
|
||||||
|
onMouseDown={event => this.onClickHighlight(bgcolor)}
|
||||||
|
style={{ backgroundColor: bgcolor }}
|
||||||
|
>
|
||||||
|
<Icon>format_paint</Icon>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render restoreDOM button
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderRestoreButton = () => {
|
||||||
|
const { editor } = this
|
||||||
|
|
||||||
|
function restoreDOM() {
|
||||||
|
editor.restoreDOM()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onMouseDown={restoreDOM}>
|
||||||
|
<Icon>refresh</Icon>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a button to corrupt the DOM
|
||||||
|
*
|
||||||
|
*@return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderCorruptButton = () => {
|
||||||
|
/**
|
||||||
|
* Corrupt the DOM by forcibly deleting the first instance we can find
|
||||||
|
* of the `bold` text in the DOM.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function corrupt() {
|
||||||
|
const boldEl = window.document.querySelector('[data-bold]')
|
||||||
|
const el = boldEl.closest('[data-slate-object="text"]')
|
||||||
|
el.parentNode.removeChild(el)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button onMouseDown={corrupt}>
|
||||||
|
<Icon>error_outline</Icon>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight every block with a given background color
|
||||||
|
*
|
||||||
|
* @param {String} bgcolor
|
||||||
|
*/
|
||||||
|
|
||||||
|
onClickHighlight = bgcolor => {
|
||||||
|
this.setState({ bgcolor })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a Slate block.
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderBlock = (props, editor, next) => {
|
||||||
|
const { attributes, children, node } = props
|
||||||
|
const style = { backgroundColor: this.state.bgcolor }
|
||||||
|
|
||||||
|
switch (node.type) {
|
||||||
|
case 'paragraph':
|
||||||
|
return (
|
||||||
|
<p {...attributes} style={style}>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a Slate mark.
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderMark = (props, editor, next) => {
|
||||||
|
const { children, mark, attributes } = props
|
||||||
|
|
||||||
|
switch (mark.type) {
|
||||||
|
case 'bold':
|
||||||
|
// Added `data-bold` so we can find bold text with `querySelector`
|
||||||
|
return (
|
||||||
|
<strong {...attributes} data-bold>
|
||||||
|
{children}
|
||||||
|
</strong>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On change, save the new `value`.
|
||||||
|
*
|
||||||
|
* @param {Editor} editor
|
||||||
|
*/
|
||||||
|
|
||||||
|
onChange = ({ value }) => {
|
||||||
|
this.setState({ value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default RestoreDOMExample
|
65
examples/restore-dom/value.json
Normal file
65
examples/restore-dom/value.json
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"object": "value",
|
||||||
|
"document": {
|
||||||
|
"object": "document",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"object": "block",
|
||||||
|
"type": "paragraph",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"object": "text",
|
||||||
|
"text": "First block with "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "text",
|
||||||
|
"text": "bold",
|
||||||
|
"marks": [{ "type": "bold" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "text",
|
||||||
|
"text": " text in it"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "block",
|
||||||
|
"type": "paragraph",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"object": "text",
|
||||||
|
"text": "Second block with "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "text",
|
||||||
|
"text": "bold",
|
||||||
|
"marks": [{ "type": "bold" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "text",
|
||||||
|
"text": " text in it"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "block",
|
||||||
|
"type": "paragraph",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"object": "text",
|
||||||
|
"text": "Third block with "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "text",
|
||||||
|
"text": "bold",
|
||||||
|
"marks": [{ "type": "bold" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "text",
|
||||||
|
"text": " text in it"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@@ -53,6 +53,7 @@ class Content extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
autoCorrect: Types.bool.isRequired,
|
autoCorrect: Types.bool.isRequired,
|
||||||
className: Types.string,
|
className: Types.string,
|
||||||
|
contentKey: Types.number,
|
||||||
editor: Types.object.isRequired,
|
editor: Types.object.isRequired,
|
||||||
id: Types.string,
|
id: Types.string,
|
||||||
readOnly: Types.bool.isRequired,
|
readOnly: Types.bool.isRequired,
|
||||||
@@ -74,6 +75,22 @@ class Content extends React.Component {
|
|||||||
tagName: 'div',
|
tagName: 'div',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error boundary. If there is a render error, we increment `errorKey`
|
||||||
|
* which is part of the container `key` which forces a re-render from
|
||||||
|
* scratch.
|
||||||
|
*
|
||||||
|
* @param {Error} error
|
||||||
|
* @param {String} info
|
||||||
|
*/
|
||||||
|
|
||||||
|
componentDidCatch(error, info) {
|
||||||
|
debug('componentDidCatch', { error, info })
|
||||||
|
// The call to `setState` is required despite not setting a value.
|
||||||
|
// Without this call, React will not try to recreate the component tree.
|
||||||
|
this.setState({})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary values.
|
* Temporary values.
|
||||||
*
|
*
|
||||||
@@ -486,6 +503,7 @@ class Content extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
|
key={this.props.contentKey}
|
||||||
{...handlers}
|
{...handlers}
|
||||||
{...data}
|
{...data}
|
||||||
ref={this.ref}
|
ref={this.ref}
|
||||||
|
@@ -79,7 +79,7 @@ class Editor extends React.Component {
|
|||||||
* @type {Object}
|
* @type {Object}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
state = { value: this.props.defaultValue }
|
state = { value: this.props.defaultValue, contentKey: 0 }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary values.
|
* Temporary values.
|
||||||
@@ -151,6 +151,7 @@ class Editor extends React.Component {
|
|||||||
const { options, readOnly, value: valueFromProps } = this.props
|
const { options, readOnly, value: valueFromProps } = this.props
|
||||||
const { value: valueFromState } = this.state
|
const { value: valueFromState } = this.state
|
||||||
const value = valueFromProps || valueFromState
|
const value = valueFromProps || valueFromState
|
||||||
|
const { contentKey } = this.state
|
||||||
this.controller.setReadOnly(readOnly)
|
this.controller.setReadOnly(readOnly)
|
||||||
this.controller.setValue(value, options)
|
this.controller.setValue(value, options)
|
||||||
|
|
||||||
@@ -170,6 +171,7 @@ class Editor extends React.Component {
|
|||||||
ref={this.tmp.contentRef}
|
ref={this.tmp.contentRef}
|
||||||
autoCorrect={autoCorrect}
|
autoCorrect={autoCorrect}
|
||||||
className={className}
|
className={className}
|
||||||
|
contentKey={contentKey}
|
||||||
editor={this}
|
editor={this}
|
||||||
id={id}
|
id={id}
|
||||||
onEvent={(handler, event) => this.run(handler, event)}
|
onEvent={(handler, event) => this.run(handler, event)}
|
||||||
|
@@ -201,10 +201,11 @@ Leaf.propTypes = {
|
|||||||
|
|
||||||
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
|
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
|
||||||
return (
|
return (
|
||||||
|
next.block === prev.block &&
|
||||||
next.index === prev.index &&
|
next.index === prev.index &&
|
||||||
next.marks === prev.marks &&
|
next.marks === prev.marks &&
|
||||||
next.parent === prev.parent &&
|
next.parent === prev.parent &&
|
||||||
next.block === prev.block &&
|
next.text === prev.text &&
|
||||||
next.annotations.equals(prev.annotations) &&
|
next.annotations.equals(prev.annotations) &&
|
||||||
next.decorations.equals(prev.decorations)
|
next.decorations.equals(prev.decorations)
|
||||||
)
|
)
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import getSelectionFromDom from '../../utils/get-selection-from-dom'
|
|
||||||
import ElementSnapshot from './element-snapshot'
|
import ElementSnapshot from './element-snapshot'
|
||||||
import SELECTORS from '../../constants/selectors'
|
import SELECTORS from '../../constants/selectors'
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ export default class DomSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.snapshot = new ElementSnapshot(elements)
|
this.snapshot = new ElementSnapshot(elements)
|
||||||
this.selection = getSelectionFromDom(window, editor, domSelection)
|
this.selection = editor.findSelection(domSelection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -5,7 +5,6 @@ import pick from 'lodash/pick'
|
|||||||
import { ANDROID_API_VERSION } from 'slate-dev-environment'
|
import { ANDROID_API_VERSION } from 'slate-dev-environment'
|
||||||
import fixSelectionInZeroWidthBlock from './fix-selection-in-zero-width-block'
|
import fixSelectionInZeroWidthBlock from './fix-selection-in-zero-width-block'
|
||||||
import getSelectionFromDom from '../../utils/get-selection-from-dom'
|
import getSelectionFromDom from '../../utils/get-selection-from-dom'
|
||||||
import setTextFromDomNode from '../../utils/set-text-from-dom-node'
|
|
||||||
import isInputDataEnter from './is-input-data-enter'
|
import isInputDataEnter from './is-input-data-enter'
|
||||||
import isInputDataLastChar from './is-input-data-last-char'
|
import isInputDataLastChar from './is-input-data-last-char'
|
||||||
import DomSnapshot from './dom-snapshot'
|
import DomSnapshot from './dom-snapshot'
|
||||||
@@ -133,10 +132,10 @@ function AndroidPlugin() {
|
|||||||
function reconcile(window, editor, { from }) {
|
function reconcile(window, editor, { from }) {
|
||||||
debug.reconcile({ from })
|
debug.reconcile({ from })
|
||||||
const domSelection = window.getSelection()
|
const domSelection = window.getSelection()
|
||||||
const selection = getSelectionFromDom(window, editor, domSelection)
|
const selection = editor.findSelection(domSelection)
|
||||||
|
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
setTextFromDomNode(window, editor, node)
|
editor.reconcileDOMNode(node)
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.select(selection)
|
editor.select(selection)
|
||||||
|
111
packages/slate-react/src/plugins/debug/debug-batch-events.js
Normal file
111
packages/slate-react/src/plugins/debug/debug-batch-events.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import Debug from 'debug'
|
||||||
|
import EVENT_HANDLERS from '../../constants/event-handlers'
|
||||||
|
import stringifyEvent from './stringify-event'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
const INTERVAL = 2000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug events function.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const debug = Debug('slate:batch-events')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A plugin that sends short easy to digest debug info about each event to
|
||||||
|
* browser.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function DebugBatchEventsPlugin() {
|
||||||
|
/**
|
||||||
|
* When the batch started
|
||||||
|
*
|
||||||
|
* @type {Date}
|
||||||
|
*/
|
||||||
|
|
||||||
|
let startDate = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timeoutId used to cancel the timeout
|
||||||
|
*
|
||||||
|
* @type {Any}
|
||||||
|
*/
|
||||||
|
|
||||||
|
let timeoutId = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of events not yet dumped with `debug`
|
||||||
|
*
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const events = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send all events to debug
|
||||||
|
*
|
||||||
|
* Note: Formatted so it can easily be cut and pasted as text for analysis or
|
||||||
|
* documentation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function dumpEvents() {
|
||||||
|
debug(`\n${events.join('\n')}`)
|
||||||
|
events.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push an event on to the Array of events for debugging in a batch
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
|
||||||
|
function pushEvent(event) {
|
||||||
|
if (events.length === 0) {
|
||||||
|
startDate = new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = stringifyEvent(event)
|
||||||
|
const now = new Date()
|
||||||
|
events.push(`- ${now - startDate} - ${s}`)
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
timeoutId = setTimeout(dumpEvents, INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin Object
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const plugin = {}
|
||||||
|
|
||||||
|
for (const eventName of EVENT_HANDLERS) {
|
||||||
|
plugin[eventName] = function(event, editor, next) {
|
||||||
|
pushEvent(event)
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the plugin.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default DebugBatchEventsPlugin
|
52
packages/slate-react/src/plugins/debug/debug-events.js
Normal file
52
packages/slate-react/src/plugins/debug/debug-events.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import Debug from 'debug'
|
||||||
|
import EVENT_HANDLERS from '../../constants/event-handlers'
|
||||||
|
import stringifyEvent from './stringify-event'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug events function.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const debug = Debug('slate:events')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A plugin that sends short easy to digest debug info about each event to
|
||||||
|
* browser.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function DebugEventsPlugin() {
|
||||||
|
/**
|
||||||
|
* Plugin Object
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const plugin = {}
|
||||||
|
|
||||||
|
for (const eventName of EVENT_HANDLERS) {
|
||||||
|
plugin[eventName] = function(event, editor, next) {
|
||||||
|
const s = stringifyEvent(event)
|
||||||
|
debug(s)
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the plugin.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default DebugEventsPlugin
|
@@ -1,64 +0,0 @@
|
|||||||
import Debug from 'debug'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A plugin that adds the "before" browser-specific logic to the editor.
|
|
||||||
*
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function DebugPlugin(namespace) {
|
|
||||||
/**
|
|
||||||
* Debug.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const debug = Debug(namespace)
|
|
||||||
|
|
||||||
const events = [
|
|
||||||
'onBeforeInput',
|
|
||||||
'onBlur',
|
|
||||||
'onClick',
|
|
||||||
'onCompositionEnd',
|
|
||||||
'onCompositionStart',
|
|
||||||
'onCopy',
|
|
||||||
'onCut',
|
|
||||||
'onDragEnd',
|
|
||||||
'onDragEnter',
|
|
||||||
'onDragExit',
|
|
||||||
'onDragLeave',
|
|
||||||
'onDragOver',
|
|
||||||
'onDragStart',
|
|
||||||
'onDrop',
|
|
||||||
'onFocus',
|
|
||||||
'onInput',
|
|
||||||
'onKeyDown',
|
|
||||||
'onPaste',
|
|
||||||
'onSelect',
|
|
||||||
]
|
|
||||||
|
|
||||||
const plugin = {}
|
|
||||||
|
|
||||||
for (const eventName of events) {
|
|
||||||
plugin[eventName] = function(event, editor, next) {
|
|
||||||
debug(eventName, { event })
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the plugin.
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
|
|
||||||
return plugin
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export.
|
|
||||||
*
|
|
||||||
* @type {Function}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default DebugPlugin
|
|
21
packages/slate-react/src/plugins/debug/stringify-event.js
Normal file
21
packages/slate-react/src/plugins/debug/stringify-event.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Takes a React Synthetic Event or a DOM Event and turns it into a String that
|
||||||
|
* is easy to log. It's succinct and keeps info to a bare minimum.
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function stringifyEvent(event) {
|
||||||
|
const e = event.nativeEvent || event
|
||||||
|
|
||||||
|
switch (e.type) {
|
||||||
|
case 'keydown':
|
||||||
|
return `${e.type} ${JSON.stringify(e.key)}`
|
||||||
|
case 'input':
|
||||||
|
case 'beforeinput':
|
||||||
|
case 'textInput':
|
||||||
|
return `${e.type}:${e.inputType} ${JSON.stringify(e.data)}`
|
||||||
|
default:
|
||||||
|
return e.type
|
||||||
|
}
|
||||||
|
}
|
@@ -8,7 +8,6 @@ import { IS_IOS, IS_IE, IS_EDGE } from 'slate-dev-environment'
|
|||||||
import cloneFragment from '../../utils/clone-fragment'
|
import cloneFragment from '../../utils/clone-fragment'
|
||||||
import getEventTransfer from '../../utils/get-event-transfer'
|
import getEventTransfer from '../../utils/get-event-transfer'
|
||||||
import setEventTransfer from '../../utils/set-event-transfer'
|
import setEventTransfer from '../../utils/set-event-transfer'
|
||||||
import setTextFromDomNode from '../../utils/set-text-from-dom-node'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug.
|
* Debug.
|
||||||
@@ -432,7 +431,7 @@ function AfterPlugin(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { anchorNode } = domSelection
|
const { anchorNode } = domSelection
|
||||||
setTextFromDomNode(window, editor, anchorNode)
|
editor.reconcileDOMNode(anchorNode)
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
@@ -552,7 +551,7 @@ function AfterPlugin(options = {}) {
|
|||||||
|
|
||||||
if (Hotkeys.isExtendBackward(event)) {
|
if (Hotkeys.isExtendBackward(event)) {
|
||||||
const startText = document.getNode(start.path)
|
const startText = document.getNode(start.path)
|
||||||
const prevEntry = document.texts({
|
const [prevEntry] = document.texts({
|
||||||
path: start.path,
|
path: start.path,
|
||||||
direction: 'backward',
|
direction: 'backward',
|
||||||
})
|
})
|
||||||
|
@@ -271,7 +271,7 @@ function BeforePlugin() {
|
|||||||
// default, and calling `preventDefault` hides the cursor.
|
// default, and calling `preventDefault` hides the cursor.
|
||||||
const node = editor.findNode(event.target)
|
const node = editor.findNode(event.target)
|
||||||
|
|
||||||
if (editor.isVoid(node)) {
|
if (!node || editor.isVoid(node)) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
72
packages/slate-react/src/plugins/react/commands.js
Normal file
72
packages/slate-react/src/plugins/react/commands.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* A set of commands for the React plugin.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function CommandsPlugin() {
|
||||||
|
/**
|
||||||
|
* Takes a `node`, find the matching `domNode` and uses it to set the text
|
||||||
|
* in the `node`.
|
||||||
|
*
|
||||||
|
* @param {Editor} editor
|
||||||
|
* @param {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
function reconcileNode(editor, node) {
|
||||||
|
const { value } = editor
|
||||||
|
const { document, selection } = value
|
||||||
|
const path = document.getPath(node.key)
|
||||||
|
|
||||||
|
const domElement = editor.findDOMNode(path)
|
||||||
|
const block = document.getClosestBlock(path)
|
||||||
|
|
||||||
|
// Get text information
|
||||||
|
const { text } = node
|
||||||
|
let { textContent: domText } = domElement
|
||||||
|
|
||||||
|
const isLastNode = block.nodes.last() === node
|
||||||
|
const lastChar = domText.charAt(domText.length - 1)
|
||||||
|
|
||||||
|
// COMPAT: If this is the last leaf, and the DOM text ends in a new line,
|
||||||
|
// we will have added another new line in <Leaf>'s render method to account
|
||||||
|
// for browsers collapsing a single trailing new lines, so remove it.
|
||||||
|
if (isLastNode && lastChar === '\n') {
|
||||||
|
domText = domText.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the text is no different, abort.
|
||||||
|
if (text === domText) return
|
||||||
|
|
||||||
|
let entire = selection.moveAnchorTo(path, 0).moveFocusTo(path, text.length)
|
||||||
|
|
||||||
|
entire = document.resolveRange(entire)
|
||||||
|
|
||||||
|
// Change the current value to have the leaf's text replaced.
|
||||||
|
editor.insertTextAtRange(entire, domText, node.marks)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes text from the `domNode` and uses it to set the text in the matching
|
||||||
|
* `node` in Slate.
|
||||||
|
*
|
||||||
|
* @param {Editor} editor
|
||||||
|
* @param {DOMNode} domNode
|
||||||
|
*/
|
||||||
|
|
||||||
|
function reconcileDOMNode(editor, domNode) {
|
||||||
|
const domElement = domNode.parentElement.closest('[data-key]')
|
||||||
|
const node = editor.findNode(domElement)
|
||||||
|
editor.reconcileNode(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
commands: {
|
||||||
|
reconcileNode,
|
||||||
|
reconcileDOMNode,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandsPlugin
|
@@ -1,9 +1,14 @@
|
|||||||
|
import Debug from 'debug'
|
||||||
import PlaceholderPlugin from 'slate-react-placeholder'
|
import PlaceholderPlugin from 'slate-react-placeholder'
|
||||||
|
|
||||||
import EditorPropsPlugin from './editor-props'
|
import EditorPropsPlugin from './editor-props'
|
||||||
import RenderingPlugin from './rendering'
|
import RenderingPlugin from './rendering'
|
||||||
|
import CommandsPlugin from './commands'
|
||||||
import QueriesPlugin from './queries'
|
import QueriesPlugin from './queries'
|
||||||
import DOMPlugin from '../dom'
|
import DOMPlugin from '../dom'
|
||||||
|
import RestoreDOMPlugin from './restore-dom'
|
||||||
|
import DebugEventsPlugin from '../debug/debug-events'
|
||||||
|
import DebugBatchEventsPlugin from '../debug/debug-batch-events'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A plugin that adds the React-specific rendering logic to the editor.
|
* A plugin that adds the React-specific rendering logic to the editor.
|
||||||
@@ -14,13 +19,20 @@ import DOMPlugin from '../dom'
|
|||||||
|
|
||||||
function ReactPlugin(options = {}) {
|
function ReactPlugin(options = {}) {
|
||||||
const { placeholder = '', plugins = [] } = options
|
const { placeholder = '', plugins = [] } = options
|
||||||
|
const debugEventsPlugin = Debug.enabled('slate:events')
|
||||||
|
? DebugEventsPlugin(options)
|
||||||
|
: null
|
||||||
|
const debugBatchEventsPlugin = Debug.enabled('slate:batch-events')
|
||||||
|
? DebugBatchEventsPlugin(options)
|
||||||
|
: null
|
||||||
const renderingPlugin = RenderingPlugin(options)
|
const renderingPlugin = RenderingPlugin(options)
|
||||||
|
const commandsPlugin = CommandsPlugin(options)
|
||||||
const queriesPlugin = QueriesPlugin(options)
|
const queriesPlugin = QueriesPlugin(options)
|
||||||
const editorPropsPlugin = EditorPropsPlugin(options)
|
const editorPropsPlugin = EditorPropsPlugin(options)
|
||||||
const domPlugin = DOMPlugin({
|
const domPlugin = DOMPlugin({
|
||||||
plugins: [editorPropsPlugin, ...plugins],
|
plugins: [editorPropsPlugin, ...plugins],
|
||||||
})
|
})
|
||||||
|
const restoreDomPlugin = RestoreDOMPlugin()
|
||||||
const placeholderPlugin = PlaceholderPlugin({
|
const placeholderPlugin = PlaceholderPlugin({
|
||||||
placeholder,
|
placeholder,
|
||||||
when: (editor, node) =>
|
when: (editor, node) =>
|
||||||
@@ -30,7 +42,16 @@ function ReactPlugin(options = {}) {
|
|||||||
Array.from(node.texts()).length === 1,
|
Array.from(node.texts()).length === 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
return [domPlugin, placeholderPlugin, renderingPlugin, queriesPlugin]
|
return [
|
||||||
|
debugEventsPlugin,
|
||||||
|
debugBatchEventsPlugin,
|
||||||
|
domPlugin,
|
||||||
|
restoreDomPlugin,
|
||||||
|
placeholderPlugin,
|
||||||
|
renderingPlugin,
|
||||||
|
commandsPlugin,
|
||||||
|
queriesPlugin,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -23,6 +23,10 @@ function QueriesPlugin() {
|
|||||||
path = PathUtils.create(path)
|
path = PathUtils.create(path)
|
||||||
const content = editor.tmp.contentRef.current
|
const content = editor.tmp.contentRef.current
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (!path.size) {
|
if (!path.size) {
|
||||||
return content.ref.current || null
|
return content.ref.current || null
|
||||||
}
|
}
|
||||||
@@ -177,13 +181,13 @@ function QueriesPlugin() {
|
|||||||
: y - rect.top < rect.top + rect.height - y
|
: y - rect.top < rect.top + rect.height - y
|
||||||
|
|
||||||
const range = document.createRange()
|
const range = document.createRange()
|
||||||
const iterable = isPrevious ? 'previousTexts' : 'nextTexts'
|
|
||||||
const move = isPrevious ? 'moveToEndOfNode' : 'moveToStartOfNode'
|
const move = isPrevious ? 'moveToEndOfNode' : 'moveToStartOfNode'
|
||||||
const entry = document[iterable](path)
|
const entry = document[isPrevious ? 'getPreviousText' : 'getNextText'](
|
||||||
|
path
|
||||||
|
)
|
||||||
|
|
||||||
if (entry) {
|
if (entry) {
|
||||||
const [n] = entry
|
return range[move](entry)
|
||||||
return range[move](n)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@@ -230,13 +234,24 @@ function QueriesPlugin() {
|
|||||||
|
|
||||||
function findPath(editor, element) {
|
function findPath(editor, element) {
|
||||||
const content = editor.tmp.contentRef.current
|
const content = editor.tmp.contentRef.current
|
||||||
|
let nodeElement = element
|
||||||
|
|
||||||
if (element === content.ref.current) {
|
// If element does not have a key, it is likely a string or
|
||||||
|
// mark, return the closest parent Node that can be looked up.
|
||||||
|
if (!nodeElement.hasAttribute(DATA_ATTRS.KEY)) {
|
||||||
|
nodeElement = nodeElement.closest(SELECTORS.KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeElement || !nodeElement.getAttribute(DATA_ATTRS.KEY)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeElement === content.ref.current) {
|
||||||
return PathUtils.create([])
|
return PathUtils.create([])
|
||||||
}
|
}
|
||||||
|
|
||||||
const search = (instance, p) => {
|
const search = (instance, p) => {
|
||||||
if (element === instance) {
|
if (nodeElement === instance) {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +259,7 @@ function QueriesPlugin() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element === instance.ref.current) {
|
if (nodeElement === instance.ref.current) {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,11 +498,14 @@ function QueriesPlugin() {
|
|||||||
anchor.offset === anchorText.text.length
|
anchor.offset === anchorText.text.length
|
||||||
) {
|
) {
|
||||||
const block = document.getClosestBlock(anchor.path)
|
const block = document.getClosestBlock(anchor.path)
|
||||||
const [next] = block.texts({ path: anchor.path })
|
const depth = document.getDepth(block.key)
|
||||||
|
const relativePath = PathUtils.drop(anchor.path, depth)
|
||||||
|
const [next] = block.texts({ path: relativePath })
|
||||||
|
|
||||||
if (next) {
|
if (next) {
|
||||||
const [, nextPath] = next
|
const [, nextPath] = next
|
||||||
range = range.moveAnchorTo(nextPath, 0)
|
const absolutePath = anchor.path.slice(0, depth).concat(nextPath)
|
||||||
|
range = range.moveAnchorTo(absolutePath, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,11 +515,14 @@ function QueriesPlugin() {
|
|||||||
focus.offset === focusText.text.length
|
focus.offset === focusText.text.length
|
||||||
) {
|
) {
|
||||||
const block = document.getClosestBlock(focus.path)
|
const block = document.getClosestBlock(focus.path)
|
||||||
const [next] = block.texts({ path: focus.path })
|
const depth = document.getDepth(block.key)
|
||||||
|
const relativePath = PathUtils.drop(focus.path, depth)
|
||||||
|
const [next] = block.texts({ path: relativePath })
|
||||||
|
|
||||||
if (next) {
|
if (next) {
|
||||||
const [, nextPath] = next
|
const [, nextPath] = next
|
||||||
range = range.moveFocusTo(nextPath, 0)
|
const absolutePath = focus.path.slice(0, depth).concat(nextPath)
|
||||||
|
range = range.moveFocusTo(absolutePath, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
21
packages/slate-react/src/plugins/react/restore-dom.js
Normal file
21
packages/slate-react/src/plugins/react/restore-dom.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
function RestoreDOMPlugin() {
|
||||||
|
/**
|
||||||
|
* Makes sure that on the next Content `render` the DOM is restored.
|
||||||
|
* This gets us around issues where the DOM is in a different state than
|
||||||
|
* React's virtual DOM and would crash.
|
||||||
|
*
|
||||||
|
* @param {Editor} editor
|
||||||
|
*/
|
||||||
|
|
||||||
|
function restoreDOM(editor) {
|
||||||
|
editor.setState({ contentKey: editor.state.contentKey + 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
commands: {
|
||||||
|
restoreDOM,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RestoreDOMPlugin
|
@@ -1,4 +1,5 @@
|
|||||||
import warning from 'tiny-warning'
|
import warning from 'tiny-warning'
|
||||||
|
import { PathUtils } from 'slate'
|
||||||
|
|
||||||
import findRange from './find-range'
|
import findRange from './find-range'
|
||||||
|
|
||||||
@@ -59,11 +60,14 @@ export default function getSelectionFromDOM(window, editor, domSelection) {
|
|||||||
anchor.offset === anchorText.text.length
|
anchor.offset === anchorText.text.length
|
||||||
) {
|
) {
|
||||||
const block = document.getClosestBlock(anchor.path)
|
const block = document.getClosestBlock(anchor.path)
|
||||||
const [next] = block.texts({ path: anchor.path })
|
const depth = document.getDepth(block.key)
|
||||||
|
const relativePath = PathUtils.drop(anchor.path, depth)
|
||||||
|
const [next] = block.texts({ path: relativePath })
|
||||||
|
|
||||||
if (next) {
|
if (next) {
|
||||||
const [, nextPath] = next
|
const [, nextPath] = next
|
||||||
range = range.moveAnchorTo(nextPath, 0)
|
const absolutePath = anchor.path.slice(0, depth).concat(nextPath)
|
||||||
|
range = range.moveAnchorTo(absolutePath, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,11 +77,14 @@ export default function getSelectionFromDOM(window, editor, domSelection) {
|
|||||||
focus.offset === focusText.text.length
|
focus.offset === focusText.text.length
|
||||||
) {
|
) {
|
||||||
const block = document.getClosestBlock(focus.path)
|
const block = document.getClosestBlock(focus.path)
|
||||||
const [next] = block.texts({ path: focus.path })
|
const depth = document.getDepth(block.key)
|
||||||
|
const relativePath = PathUtils.drop(focus.path, depth)
|
||||||
|
const [next] = block.texts({ path: relativePath })
|
||||||
|
|
||||||
if (next) {
|
if (next) {
|
||||||
const [, nextPath] = next
|
const [, nextPath] = next
|
||||||
range = range.moveFocusTo(nextPath, 0)
|
const absolutePath = focus.path.slice(0, depth).concat(nextPath)
|
||||||
|
range = range.moveFocusTo(absolutePath, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,67 +0,0 @@
|
|||||||
import findPoint from './find-point'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* setTextFromDomNode lets us take a domNode and reconcile the text in the
|
|
||||||
* editor's Document such that it reflects the text in the DOM. This is the
|
|
||||||
* opposite of what the Editor usually does which takes the Editor's Document
|
|
||||||
* and React modifies the DOM to match. The purpose of this method is for
|
|
||||||
* composition changes where we don't know what changes the user made by
|
|
||||||
* looking at events. Instead we wait until the DOM is in a safe state, we
|
|
||||||
* read from it, and update the Editor's Document.
|
|
||||||
*
|
|
||||||
* @param {Window} window
|
|
||||||
* @param {Editor} editor
|
|
||||||
* @param {Node} domNode
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function setTextFromDomNode(window, editor, domNode) {
|
|
||||||
const point = findPoint(domNode, 0, editor)
|
|
||||||
if (!point) return
|
|
||||||
|
|
||||||
// Get the text node and leaf in question.
|
|
||||||
const { value } = editor
|
|
||||||
const { document, selection } = value
|
|
||||||
const node = document.getDescendant(point.path)
|
|
||||||
const block = document.getClosestBlock(point.path)
|
|
||||||
const leaves = node.getLeaves()
|
|
||||||
const lastText = block.getLastText()
|
|
||||||
const lastLeaf = leaves.last()
|
|
||||||
let start = 0
|
|
||||||
let end = 0
|
|
||||||
|
|
||||||
const leaf =
|
|
||||||
leaves.find(r => {
|
|
||||||
start = end
|
|
||||||
end += r.text.length
|
|
||||||
if (end > point.offset) return true
|
|
||||||
}) || lastLeaf
|
|
||||||
|
|
||||||
// Get the text information.
|
|
||||||
const { text } = leaf
|
|
||||||
let { textContent } = domNode
|
|
||||||
const isLastText = node === lastText
|
|
||||||
const isLastLeaf = leaf === lastLeaf
|
|
||||||
const lastChar = textContent.charAt(textContent.length - 1)
|
|
||||||
|
|
||||||
// COMPAT: If this is the last leaf, and the DOM text ends in a new line,
|
|
||||||
// we will have added another new line in <Leaf>'s render method to account
|
|
||||||
// for browsers collapsing a single trailing new lines, so remove it.
|
|
||||||
if (isLastText && isLastLeaf && lastChar === '\n') {
|
|
||||||
textContent = textContent.slice(0, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the text is no different, abort.
|
|
||||||
if (textContent === text) return
|
|
||||||
|
|
||||||
// Determine what the selection should be after changing the text.
|
|
||||||
// const delta = textContent.length - text.length
|
|
||||||
// const corrected = selection.moveToEnd().moveForward(delta)
|
|
||||||
let entire = selection
|
|
||||||
.moveAnchorTo(point.path, start)
|
|
||||||
.moveFocusTo(point.path, end)
|
|
||||||
|
|
||||||
entire = document.resolveRange(entire)
|
|
||||||
|
|
||||||
// Change the current value to have the leaf's text replaced.
|
|
||||||
editor.insertTextAtRange(entire, textContent, leaf.marks)
|
|
||||||
}
|
|
@@ -114,7 +114,8 @@ Commands.deleteAtRange = (editor, range) => {
|
|||||||
endOffset === 0 &&
|
endOffset === 0 &&
|
||||||
isStartVoid === false &&
|
isStartVoid === false &&
|
||||||
startKey === startBlock.getFirstText().key &&
|
startKey === startBlock.getFirstText().key &&
|
||||||
endKey === endBlock.getFirstText().key
|
endKey === endBlock.getFirstText().key &&
|
||||||
|
startKey !== endKey
|
||||||
|
|
||||||
// If it's a hanging selection, nudge it back to end in the previous text.
|
// If it's a hanging selection, nudge it back to end in the previous text.
|
||||||
if (isHanging && isEndVoid) {
|
if (isHanging && isEndVoid) {
|
||||||
@@ -661,15 +662,11 @@ Commands.insertBlockAtRange = (editor, range, block) => {
|
|||||||
const startInline = document.getClosestInline(startKey)
|
const startInline = document.getClosestInline(startKey)
|
||||||
const parent = document.getParent(startBlock.key)
|
const parent = document.getParent(startBlock.key)
|
||||||
const index = parent.nodes.indexOf(startBlock)
|
const index = parent.nodes.indexOf(startBlock)
|
||||||
|
const insertionMode = getInsertionMode(editor, range)
|
||||||
|
|
||||||
if (editor.isVoid(startBlock)) {
|
if (insertionMode === 'before') {
|
||||||
const extra = start.isAtEndOfNode(startBlock) ? 1 : 0
|
|
||||||
editor.insertNodeByKey(parent.key, index + extra, block)
|
|
||||||
} else if (!startInline && startBlock.text === '') {
|
|
||||||
editor.insertNodeByKey(parent.key, index + 1, block)
|
|
||||||
} else if (start.isAtStartOfNode(startBlock)) {
|
|
||||||
editor.insertNodeByKey(parent.key, index, block)
|
editor.insertNodeByKey(parent.key, index, block)
|
||||||
} else if (start.isAtEndOfNode(startBlock)) {
|
} else if (insertionMode === 'behind') {
|
||||||
editor.insertNodeByKey(parent.key, index + 1, block)
|
editor.insertNodeByKey(parent.key, index + 1, block)
|
||||||
} else {
|
} else {
|
||||||
if (startInline && editor.isVoid(startInline)) {
|
if (startInline && editor.isVoid(startInline)) {
|
||||||
@@ -693,6 +690,34 @@ Commands.insertBlockAtRange = (editor, range, block) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current block should be split or new block should be added before or behind it.
|
||||||
|
*
|
||||||
|
* @param {Editor} editor
|
||||||
|
* @param {Range} range
|
||||||
|
*/
|
||||||
|
|
||||||
|
const getInsertionMode = (editor, range) => {
|
||||||
|
const { value } = editor
|
||||||
|
const { document } = value
|
||||||
|
const { start } = range
|
||||||
|
const startKey = start.key
|
||||||
|
const startBlock = document.getClosestBlock(startKey)
|
||||||
|
const startInline = document.getClosestInline(startKey)
|
||||||
|
|
||||||
|
if (editor.isVoid(startBlock)) {
|
||||||
|
if (start.isAtEndOfNode(startBlock)) return 'behind'
|
||||||
|
else return 'before'
|
||||||
|
} else if (!startInline && startBlock.text === '') {
|
||||||
|
return 'behind'
|
||||||
|
} else if (start.isAtStartOfNode(startBlock)) {
|
||||||
|
return 'before'
|
||||||
|
} else if (start.isAtEndOfNode(startBlock)) {
|
||||||
|
return 'behind'
|
||||||
|
}
|
||||||
|
return 'split'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a `fragment` at a `range`.
|
* Insert a `fragment` at a `range`.
|
||||||
*
|
*
|
||||||
@@ -743,7 +768,12 @@ Commands.insertFragmentAtRange = (editor, range, fragment) => {
|
|||||||
insertionNode === fragment &&
|
insertionNode === fragment &&
|
||||||
(firstChild.hasBlockChildren() || lastChild.hasBlockChildren())
|
(firstChild.hasBlockChildren() || lastChild.hasBlockChildren())
|
||||||
) {
|
) {
|
||||||
fragment.nodes.reverse().forEach(node => {
|
// check if reversal is necessary or not
|
||||||
|
const insertionMode = getInsertionMode(editor, range)
|
||||||
|
const nodes =
|
||||||
|
insertionMode === 'before' ? fragment.nodes : fragment.nodes.reverse()
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
editor.insertBlockAtRange(range, node)
|
editor.insertBlockAtRange(range, node)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@@ -278,7 +278,19 @@ Commands.insertFragment = (editor, fragment) => {
|
|||||||
if (newText && (lastInline || isInserting)) {
|
if (newText && (lastInline || isInserting)) {
|
||||||
editor.moveToEndOfNode(newText)
|
editor.moveToEndOfNode(newText)
|
||||||
} else if (newText) {
|
} else if (newText) {
|
||||||
editor.moveToStartOfNode(newText).moveForward(lastBlock.text.length)
|
// The position within the last text node needs to be calculated. This is the length
|
||||||
|
// of all text nodes within the last block, but if the last block contains inline nodes,
|
||||||
|
// these have to be skipped.
|
||||||
|
const { nodes } = lastBlock
|
||||||
|
const lastIndex = nodes.findLastIndex(
|
||||||
|
node => node && node.object === 'inline'
|
||||||
|
)
|
||||||
|
const remainingTexts = nodes.takeLast(nodes.size - lastIndex - 1)
|
||||||
|
const remainingTextLength = remainingTexts.reduce(
|
||||||
|
(acc, val) => acc + val.text.length,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
editor.moveToStartOfNode(newText).moveForward(remainingTextLength)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,29 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
|
export default function(editor) {
|
||||||
|
editor.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
<cursor />
|
||||||
|
word
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
<cursor />
|
||||||
|
word
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
@@ -0,0 +1,35 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
|
export default function(editor) {
|
||||||
|
editor.insertFragment(
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
<text>one</text>
|
||||||
|
<inline type="link">Some inline stuff</inline>
|
||||||
|
<text>two</text>
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
A<cursor />B
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
Aone<inline type="link">Some inline stuff</inline>two<cursor />B
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
@@ -0,0 +1,40 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
|
export default function(editor) {
|
||||||
|
editor.insertFragment(
|
||||||
|
<document>
|
||||||
|
<quote>
|
||||||
|
<quote>one</quote>
|
||||||
|
<quote>two</quote>
|
||||||
|
</quote>
|
||||||
|
<paragraph>after quote</paragraph>
|
||||||
|
</document>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
word<cursor />
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>word</paragraph>
|
||||||
|
<quote>
|
||||||
|
<quote>one</quote>
|
||||||
|
<quote>two</quote>
|
||||||
|
</quote>
|
||||||
|
<paragraph>
|
||||||
|
after quote<cursor />
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
@@ -0,0 +1,40 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
|
export default function(editor) {
|
||||||
|
editor.insertFragment(
|
||||||
|
<document>
|
||||||
|
<quote>
|
||||||
|
<quote>one</quote>
|
||||||
|
<quote>two</quote>
|
||||||
|
</quote>
|
||||||
|
<paragraph>after quote</paragraph>
|
||||||
|
</document>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<paragraph>
|
||||||
|
<cursor />word
|
||||||
|
</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<quote>
|
||||||
|
<quote>one</quote>
|
||||||
|
<quote>two</quote>
|
||||||
|
</quote>
|
||||||
|
<paragraph>
|
||||||
|
after quote<cursor />
|
||||||
|
</paragraph>
|
||||||
|
<paragraph>word</paragraph>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
Reference in New Issue
Block a user