(
-
-)
-
const Tabs = props => (
)
@@ -52,11 +40,13 @@ const Tab = ({ active, ...props }) => (
className={css`
display: inline-block;
text-decoration: none;
- color: black;
- background: ${active ? '#AAA' : '#DDD'};
- padding: 0.25em 0.5em;
- border-radius: 0.25em;
+ font-size: 14px;
+ color: ${active ? 'black' : '#808080'};
+ background: ${active ? '#f8f8e8' : '#fff'};
+ padding: 10px;
margin-right: 0.25em;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
`}
/>
)
@@ -73,45 +63,6 @@ const Version = props => (
/>
)
-const EditorText = props => (
-
-)
-
-const EditorTextCaption = props => (
-
-)
-
-/**
- * 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.
*
@@ -210,29 +161,23 @@ class RichTextExample extends React.Component {
render() {
const { text } = this.state
if (text == null) return
- const textLines = getTextLines(this.state.value)
+ // const textLines = getTextLines(this.state.value)
return (
+
+ {SUBPAGES.map(([name, Component, subpage]) => {
+ const active = subpage === this.props.params.subpage
+ return (
+
+ {name}
+
+ )
+ })}
+
+ {ANDROID_API_VERSION ? `Android API ${ANDROID_API_VERSION}` : null}
+
+
-
- {SUBPAGES.map(([name, Component, subpage]) => {
- const active = subpage === this.props.params.subpage
- return (
-
- {name}
-
- )
- })}
-
- {ANDROID_API_VERSION
- ? `Android API ${ANDROID_API_VERSION}`
- : null}
-
-
{this.state.text}
@@ -257,12 +202,7 @@ class RichTextExample extends React.Component {
renderBlock={this.renderBlock}
renderMark={this.renderMark}
/>
-
- Text in Slate's `Value`
- {textLines.map((line, index) => (
- {line.length > 0 ? line : ' '}
- ))}
-
+
)
}
diff --git a/examples/composition/special.js b/examples/composition/special.js
index fab03c65d..c1edcf131 100644
--- a/examples/composition/special.js
+++ b/examples/composition/special.js
@@ -1,23 +1,23 @@
-import { p, bold } from './util'
+import { p, text, bold } from './util'
export default {
text: `Follow the instructions on each line exactly`,
document: {
nodes: [
p(bold('Type "it is". cursor to "i|t" then hit enter.')),
- p(''),
+ p(text('')),
p(
bold(
'Cursor to "mid|dle" then press space, backspace, space, backspace. Should say "middle".'
)
),
- p('The middle word.'),
+ p(text('The middle word.')),
p(
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.'
)
),
- p(''),
+ p(text('')),
],
},
}
diff --git a/examples/composition/util.js b/examples/composition/util.js
index 6facdb4f6..d81325e59 100644
--- a/examples/composition/util.js
+++ b/examples/composition/util.js
@@ -2,14 +2,14 @@ export function p(...leaves) {
return {
object: 'block',
type: 'paragraph',
- nodes: [{ object: 'text', leaves }],
+ nodes: leaves,
}
}
export function text(textContent) {
- return { text: textContent }
+ return { object: 'text', text: textContent }
}
export function bold(textContent) {
- return { text: textContent, marks: [{ type: 'bold' }] }
+ return { object: 'text', text: textContent, marks: [{ type: 'bold' }] }
}
diff --git a/examples/images/index.js b/examples/images/index.js
index ba005b441..bee13970e 100644
--- a/examples/images/index.js
+++ b/examples/images/index.js
@@ -1,4 +1,4 @@
-import { Editor, getEventRange, getEventTransfer } from 'slate-react'
+import { Editor, getEventTransfer } from 'slate-react'
import { Block, Value } from 'slate'
import React from 'react'
@@ -181,7 +181,7 @@ class Images extends React.Component {
*/
onDropOrPaste = (event, editor, next) => {
- const target = getEventRange(event, editor)
+ const target = editor.findEventRange(event)
if (!target && event.type === 'drop') return next()
const transfer = getEventTransfer(event)
diff --git a/examples/restore-dom/index.js b/examples/restore-dom/index.js
new file mode 100644
index 000000000..c8a572066
--- /dev/null
+++ b/examples/restore-dom/index.js
@@ -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 (
+
+
+
+
+ Click a brush to change color in state. Use refresh button to
+ `restoreDOM` which renders changes.
+
+
+ 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.
+
+
+
+
+ {this.renderHighlightButton('#ffeecc')}
+ {this.renderHighlightButton('#ffffcc')}
+ {this.renderHighlightButton('#ccffcc')}
+ {this.renderCorruptButton()}
+ {this.renderRestoreButton()}
+
+
+
+
+ )
+ }
+
+ /**
+ * Render a highlight button
+ *
+ * @param {String} bgcolor
+ * @return {Element}
+ */
+
+ renderHighlightButton = bgcolor => {
+ const isActive = this.state.bgcolor === bgcolor
+ return (
+
this.onClickHighlight(bgcolor)}
+ style={{ backgroundColor: bgcolor }}
+ >
+ format_paint
+
+ )
+ }
+
+ /**
+ * Render restoreDOM button
+ */
+
+ renderRestoreButton = () => {
+ const { editor } = this
+
+ function restoreDOM() {
+ editor.restoreDOM()
+ }
+
+ return (
+
+ refresh
+
+ )
+ }
+
+ /**
+ * 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 (
+
+ error_outline
+
+ )
+ }
+
+ /**
+ * 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 (
+
+ {children}
+
+ )
+ 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 (
+
+ {children}
+
+ )
+ default:
+ return next()
+ }
+ }
+
+ /**
+ * On change, save the new `value`.
+ *
+ * @param {Editor} editor
+ */
+
+ onChange = ({ value }) => {
+ this.setState({ value })
+ }
+}
+
+/**
+ * Export.
+ */
+
+export default RestoreDOMExample
diff --git a/examples/restore-dom/value.json b/examples/restore-dom/value.json
new file mode 100644
index 000000000..64a4a8e58
--- /dev/null
+++ b/examples/restore-dom/value.json
@@ -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"
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/packages/slate-react/src/components/content.js b/packages/slate-react/src/components/content.js
index c4ffe5d60..2d150f4c9 100644
--- a/packages/slate-react/src/components/content.js
+++ b/packages/slate-react/src/components/content.js
@@ -53,6 +53,7 @@ class Content extends React.Component {
static propTypes = {
autoCorrect: Types.bool.isRequired,
className: Types.string,
+ contentKey: Types.number,
editor: Types.object.isRequired,
id: Types.string,
readOnly: Types.bool.isRequired,
@@ -74,6 +75,22 @@ class Content extends React.Component {
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.
*
@@ -486,6 +503,7 @@ class Content extends React.Component {
return (
this.run(handler, event)}
diff --git a/packages/slate-react/src/components/leaf.js b/packages/slate-react/src/components/leaf.js
index f0588ec34..c8693b962 100644
--- a/packages/slate-react/src/components/leaf.js
+++ b/packages/slate-react/src/components/leaf.js
@@ -201,10 +201,11 @@ Leaf.propTypes = {
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
return (
+ next.block === prev.block &&
next.index === prev.index &&
next.marks === prev.marks &&
next.parent === prev.parent &&
- next.block === prev.block &&
+ next.text === prev.text &&
next.annotations.equals(prev.annotations) &&
next.decorations.equals(prev.decorations)
)
diff --git a/packages/slate-react/src/plugins/android/dom-snapshot.js b/packages/slate-react/src/plugins/android/dom-snapshot.js
index 34d731630..c56f522ff 100644
--- a/packages/slate-react/src/plugins/android/dom-snapshot.js
+++ b/packages/slate-react/src/plugins/android/dom-snapshot.js
@@ -1,4 +1,3 @@
-import getSelectionFromDom from '../../utils/get-selection-from-dom'
import ElementSnapshot from './element-snapshot'
import SELECTORS from '../../constants/selectors'
@@ -51,7 +50,7 @@ export default class DomSnapshot {
}
this.snapshot = new ElementSnapshot(elements)
- this.selection = getSelectionFromDom(window, editor, domSelection)
+ this.selection = editor.findSelection(domSelection)
}
/**
diff --git a/packages/slate-react/src/plugins/android/index.js b/packages/slate-react/src/plugins/android/index.js
index 0ef1e0e96..8372e6ed7 100644
--- a/packages/slate-react/src/plugins/android/index.js
+++ b/packages/slate-react/src/plugins/android/index.js
@@ -5,7 +5,6 @@ import pick from 'lodash/pick'
import { ANDROID_API_VERSION } from 'slate-dev-environment'
import fixSelectionInZeroWidthBlock from './fix-selection-in-zero-width-block'
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 isInputDataLastChar from './is-input-data-last-char'
import DomSnapshot from './dom-snapshot'
@@ -133,10 +132,10 @@ function AndroidPlugin() {
function reconcile(window, editor, { from }) {
debug.reconcile({ from })
const domSelection = window.getSelection()
- const selection = getSelectionFromDom(window, editor, domSelection)
+ const selection = editor.findSelection(domSelection)
nodes.forEach(node => {
- setTextFromDomNode(window, editor, node)
+ editor.reconcileDOMNode(node)
})
editor.select(selection)
diff --git a/packages/slate-react/src/plugins/debug/debug-batch-events.js b/packages/slate-react/src/plugins/debug/debug-batch-events.js
new file mode 100644
index 000000000..d9f9df849
--- /dev/null
+++ b/packages/slate-react/src/plugins/debug/debug-batch-events.js
@@ -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
diff --git a/packages/slate-react/src/plugins/debug/debug-events.js b/packages/slate-react/src/plugins/debug/debug-events.js
new file mode 100644
index 000000000..6eb00f0de
--- /dev/null
+++ b/packages/slate-react/src/plugins/debug/debug-events.js
@@ -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
diff --git a/packages/slate-react/src/plugins/debug/index.js b/packages/slate-react/src/plugins/debug/index.js
deleted file mode 100644
index 1b6aee4d8..000000000
--- a/packages/slate-react/src/plugins/debug/index.js
+++ /dev/null
@@ -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
diff --git a/packages/slate-react/src/plugins/debug/stringify-event.js b/packages/slate-react/src/plugins/debug/stringify-event.js
new file mode 100644
index 000000000..c6ea658a8
--- /dev/null
+++ b/packages/slate-react/src/plugins/debug/stringify-event.js
@@ -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
+ }
+}
diff --git a/packages/slate-react/src/plugins/dom/after.js b/packages/slate-react/src/plugins/dom/after.js
index 38100858d..479fd879a 100644
--- a/packages/slate-react/src/plugins/dom/after.js
+++ b/packages/slate-react/src/plugins/dom/after.js
@@ -8,7 +8,6 @@ import { IS_IOS, IS_IE, IS_EDGE } from 'slate-dev-environment'
import cloneFragment from '../../utils/clone-fragment'
import getEventTransfer from '../../utils/get-event-transfer'
import setEventTransfer from '../../utils/set-event-transfer'
-import setTextFromDomNode from '../../utils/set-text-from-dom-node'
/**
* Debug.
@@ -432,7 +431,7 @@ function AfterPlugin(options = {}) {
}
const { anchorNode } = domSelection
- setTextFromDomNode(window, editor, anchorNode)
+ editor.reconcileDOMNode(anchorNode)
next()
}
@@ -552,7 +551,7 @@ function AfterPlugin(options = {}) {
if (Hotkeys.isExtendBackward(event)) {
const startText = document.getNode(start.path)
- const prevEntry = document.texts({
+ const [prevEntry] = document.texts({
path: start.path,
direction: 'backward',
})
diff --git a/packages/slate-react/src/plugins/dom/before.js b/packages/slate-react/src/plugins/dom/before.js
index 7d3f1d59b..86c85752b 100644
--- a/packages/slate-react/src/plugins/dom/before.js
+++ b/packages/slate-react/src/plugins/dom/before.js
@@ -271,7 +271,7 @@ function BeforePlugin() {
// default, and calling `preventDefault` hides the cursor.
const node = editor.findNode(event.target)
- if (editor.isVoid(node)) {
+ if (!node || editor.isVoid(node)) {
event.preventDefault()
}
diff --git a/packages/slate-react/src/plugins/react/commands.js b/packages/slate-react/src/plugins/react/commands.js
new file mode 100644
index 000000000..ca0249fb0
--- /dev/null
+++ b/packages/slate-react/src/plugins/react/commands.js
@@ -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 '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
diff --git a/packages/slate-react/src/plugins/react/index.js b/packages/slate-react/src/plugins/react/index.js
index 4fa44e970..346c4dd6c 100644
--- a/packages/slate-react/src/plugins/react/index.js
+++ b/packages/slate-react/src/plugins/react/index.js
@@ -1,9 +1,14 @@
+import Debug from 'debug'
import PlaceholderPlugin from 'slate-react-placeholder'
import EditorPropsPlugin from './editor-props'
import RenderingPlugin from './rendering'
+import CommandsPlugin from './commands'
import QueriesPlugin from './queries'
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.
@@ -14,13 +19,20 @@ import DOMPlugin from '../dom'
function ReactPlugin(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 commandsPlugin = CommandsPlugin(options)
const queriesPlugin = QueriesPlugin(options)
const editorPropsPlugin = EditorPropsPlugin(options)
const domPlugin = DOMPlugin({
plugins: [editorPropsPlugin, ...plugins],
})
-
+ const restoreDomPlugin = RestoreDOMPlugin()
const placeholderPlugin = PlaceholderPlugin({
placeholder,
when: (editor, node) =>
@@ -30,7 +42,16 @@ function ReactPlugin(options = {}) {
Array.from(node.texts()).length === 1,
})
- return [domPlugin, placeholderPlugin, renderingPlugin, queriesPlugin]
+ return [
+ debugEventsPlugin,
+ debugBatchEventsPlugin,
+ domPlugin,
+ restoreDomPlugin,
+ placeholderPlugin,
+ renderingPlugin,
+ commandsPlugin,
+ queriesPlugin,
+ ]
}
/**
diff --git a/packages/slate-react/src/plugins/react/queries.js b/packages/slate-react/src/plugins/react/queries.js
index 789edfb36..acd3ff1b7 100644
--- a/packages/slate-react/src/plugins/react/queries.js
+++ b/packages/slate-react/src/plugins/react/queries.js
@@ -23,6 +23,10 @@ function QueriesPlugin() {
path = PathUtils.create(path)
const content = editor.tmp.contentRef.current
+ if (!content) {
+ return null
+ }
+
if (!path.size) {
return content.ref.current || null
}
@@ -177,13 +181,13 @@ function QueriesPlugin() {
: y - rect.top < rect.top + rect.height - y
const range = document.createRange()
- const iterable = isPrevious ? 'previousTexts' : 'nextTexts'
const move = isPrevious ? 'moveToEndOfNode' : 'moveToStartOfNode'
- const entry = document[iterable](path)
+ const entry = document[isPrevious ? 'getPreviousText' : 'getNextText'](
+ path
+ )
if (entry) {
- const [n] = entry
- return range[move](n)
+ return range[move](entry)
}
return null
@@ -230,13 +234,24 @@ function QueriesPlugin() {
function findPath(editor, element) {
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([])
}
const search = (instance, p) => {
- if (element === instance) {
+ if (nodeElement === instance) {
return p
}
@@ -244,7 +259,7 @@ function QueriesPlugin() {
return null
}
- if (element === instance.ref.current) {
+ if (nodeElement === instance.ref.current) {
return p
}
@@ -483,11 +498,14 @@ function QueriesPlugin() {
anchor.offset === anchorText.text.length
) {
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) {
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
) {
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) {
const [, nextPath] = next
- range = range.moveFocusTo(nextPath, 0)
+ const absolutePath = focus.path.slice(0, depth).concat(nextPath)
+ range = range.moveFocusTo(absolutePath, 0)
}
}
diff --git a/packages/slate-react/src/plugins/react/restore-dom.js b/packages/slate-react/src/plugins/react/restore-dom.js
new file mode 100644
index 000000000..c40a9c521
--- /dev/null
+++ b/packages/slate-react/src/plugins/react/restore-dom.js
@@ -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
diff --git a/packages/slate-react/src/utils/get-selection-from-dom.js b/packages/slate-react/src/utils/get-selection-from-dom.js
index f26e010b5..0f7a68cd2 100644
--- a/packages/slate-react/src/utils/get-selection-from-dom.js
+++ b/packages/slate-react/src/utils/get-selection-from-dom.js
@@ -1,4 +1,5 @@
import warning from 'tiny-warning'
+import { PathUtils } from 'slate'
import findRange from './find-range'
@@ -59,11 +60,14 @@ export default function getSelectionFromDOM(window, editor, domSelection) {
anchor.offset === anchorText.text.length
) {
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) {
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
) {
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) {
const [, nextPath] = next
- range = range.moveFocusTo(nextPath, 0)
+ const absolutePath = focus.path.slice(0, depth).concat(nextPath)
+ range = range.moveFocusTo(absolutePath, 0)
}
}
diff --git a/packages/slate-react/src/utils/set-text-from-dom-node.js b/packages/slate-react/src/utils/set-text-from-dom-node.js
deleted file mode 100644
index de1e6ecd2..000000000
--- a/packages/slate-react/src/utils/set-text-from-dom-node.js
+++ /dev/null
@@ -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 '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)
-}
diff --git a/packages/slate/src/commands/at-range.js b/packages/slate/src/commands/at-range.js
index 04d3e95e0..1a13fa9a4 100644
--- a/packages/slate/src/commands/at-range.js
+++ b/packages/slate/src/commands/at-range.js
@@ -114,7 +114,8 @@ Commands.deleteAtRange = (editor, range) => {
endOffset === 0 &&
isStartVoid === false &&
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 (isHanging && isEndVoid) {
@@ -661,15 +662,11 @@ Commands.insertBlockAtRange = (editor, range, block) => {
const startInline = document.getClosestInline(startKey)
const parent = document.getParent(startBlock.key)
const index = parent.nodes.indexOf(startBlock)
+ const insertionMode = getInsertionMode(editor, range)
- if (editor.isVoid(startBlock)) {
- 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)) {
+ if (insertionMode === 'before') {
editor.insertNodeByKey(parent.key, index, block)
- } else if (start.isAtEndOfNode(startBlock)) {
+ } else if (insertionMode === 'behind') {
editor.insertNodeByKey(parent.key, index + 1, block)
} else {
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`.
*
@@ -743,7 +768,12 @@ Commands.insertFragmentAtRange = (editor, range, fragment) => {
insertionNode === fragment &&
(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)
})
return
diff --git a/packages/slate/src/commands/with-intent.js b/packages/slate/src/commands/with-intent.js
index 977a101de..911e11f77 100644
--- a/packages/slate/src/commands/with-intent.js
+++ b/packages/slate/src/commands/with-intent.js
@@ -278,7 +278,19 @@ Commands.insertFragment = (editor, fragment) => {
if (newText && (lastInline || isInserting)) {
editor.moveToEndOfNode(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)
}
}
diff --git a/packages/slate/test/commands/at-current-range/delete/first-position.js b/packages/slate/test/commands/at-current-range/delete/first-position.js
new file mode 100644
index 000000000..076680879
--- /dev/null
+++ b/packages/slate/test/commands/at-current-range/delete/first-position.js
@@ -0,0 +1,29 @@
+/** @jsx h */
+
+import h from '../../../helpers/h'
+
+export default function(editor) {
+ editor.delete()
+}
+
+export const input = (
+
+
+
+
+ word
+
+
+
+)
+
+export const output = (
+
+
+
+
+ word
+
+
+
+)
diff --git a/packages/slate/test/commands/at-current-range/insert-fragment/fragment-inline-node.js b/packages/slate/test/commands/at-current-range/insert-fragment/fragment-inline-node.js
new file mode 100644
index 000000000..49652f5d1
--- /dev/null
+++ b/packages/slate/test/commands/at-current-range/insert-fragment/fragment-inline-node.js
@@ -0,0 +1,35 @@
+/** @jsx h */
+
+import h from '../../../helpers/h'
+
+export default function(editor) {
+ editor.insertFragment(
+
+
+ one
+ Some inline stuff
+ two
+
+
+ )
+}
+
+export const input = (
+
+
+
+ A B
+
+
+
+)
+
+export const output = (
+
+
+
+ AoneSome inline stuff two B
+
+
+
+)
diff --git a/packages/slate/test/commands/at-current-range/insert-fragment/fragment-nested-blocks-end-of-node.js b/packages/slate/test/commands/at-current-range/insert-fragment/fragment-nested-blocks-end-of-node.js
new file mode 100644
index 000000000..06fb7ce9b
--- /dev/null
+++ b/packages/slate/test/commands/at-current-range/insert-fragment/fragment-nested-blocks-end-of-node.js
@@ -0,0 +1,40 @@
+/** @jsx h */
+
+import h from '../../../helpers/h'
+
+export default function(editor) {
+ editor.insertFragment(
+
+
+ one
+ two
+
+ after quote
+
+ )
+}
+
+export const input = (
+
+
+
+ word
+
+
+
+)
+
+export const output = (
+
+
+ word
+
+ one
+ two
+
+
+ after quote
+
+
+
+)
diff --git a/packages/slate/test/commands/at-current-range/insert-fragment/fragment-nested-blocks-start-of-node.js b/packages/slate/test/commands/at-current-range/insert-fragment/fragment-nested-blocks-start-of-node.js
new file mode 100644
index 000000000..f150204e4
--- /dev/null
+++ b/packages/slate/test/commands/at-current-range/insert-fragment/fragment-nested-blocks-start-of-node.js
@@ -0,0 +1,40 @@
+/** @jsx h */
+
+import h from '../../../helpers/h'
+
+export default function(editor) {
+ editor.insertFragment(
+
+
+ one
+ two
+
+ after quote
+
+ )
+}
+
+export const input = (
+
+
+
+ word
+
+
+
+)
+
+export const output = (
+
+
+
+ one
+ two
+
+
+ after quote
+
+ word
+
+
+)