From 5bef253855787c72e589aab0a804b3ef14a5b197 Mon Sep 17 00:00:00 2001 From: Entkenntnis Date: Thu, 9 May 2019 22:50:59 +0200 Subject: [PATCH 01/19] fix selection after pasting inline nodes (#2738) * fix selection after pasting inline nodes * add forgotten line * added test case --- packages/slate/src/commands/with-intent.js | 14 +++++++- .../insert-fragment/fragment-inline-node.js | 35 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/slate/test/commands/at-current-range/insert-fragment/fragment-inline-node.js 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/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 = ( + + + + AB + + + +) + +export const output = ( + + + + AoneSome inline stufftwoB + + + +) From 376015df6c59e850d06fd6b45cae9186ad38c1f6 Mon Sep 17 00:00:00 2001 From: Dan Schuman Date: Fri, 10 May 2019 10:47:58 -0700 Subject: [PATCH 02/19] Text documentation update (#2758) `text` property is no longer computed. Add `marks` field. --- docs/reference/slate/text.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/reference/slate/text.md b/docs/reference/slate/text.md index 8ee4999e5..8e3533760 100644 --- a/docs/reference/slate/text.md +++ b/docs/reference/slate/text.md @@ -11,6 +11,8 @@ A text node in a Slate [`Document`](./document.md). Text nodes are always the bo ```js Text({ key: String, + text: String, + marks: Immutable.List, }) ``` @@ -20,20 +22,24 @@ Text({ A unique identifier for the node. +### `text` + +`String` + +The text contents of this node. + +### `marks` + +`Immutable.List,` + +A list of marks for this node. + ### `object` `String` 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 ### `Text.create` From f76a00acdc5d89e1a4b43558ad8bd6b310653872 Mon Sep 17 00:00:00 2001 From: Matthew Steedman Date: Sat, 11 May 2019 21:52:09 -0400 Subject: [PATCH 03/19] Replace renderNode with renderBlock (#2762) --- docs/walkthroughs/defining-custom-block-nodes.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/walkthroughs/defining-custom-block-nodes.md b/docs/walkthroughs/defining-custom-block-nodes.md index 20a5148af..593c2ccda 100644 --- a/docs/walkthroughs/defining-custom-block-nodes.md +++ b/docs/walkthroughs/defining-custom-block-nodes.md @@ -89,18 +89,18 @@ class App extends React.Component { render() { return ( - // Pass in the `renderNode` prop... + // Pass in the `renderBlock` prop... ) } - // Add a `renderNode` method to render a `CodeNode` for code blocks. - renderNode = (props, editor, next) => { + // Add a `renderBlock` method to render a `CodeNode` for code blocks. + renderBlock = (props, editor, next) => { switch (props.node.type) { case 'code': return @@ -148,12 +148,12 @@ class App extends React.Component { value={this.state.value} onChange={this.onChange} onKeyDown={this.onKeyDown} - renderNode={this.renderNode} + renderBlock={this.renderBlock} /> ) } - renderNode = (props, editor, next) => { + renderBlock = (props, editor, next) => { switch (props.node.type) { case 'code': return @@ -206,12 +206,12 @@ class App extends React.Component { value={this.state.value} onChange={this.onChange} onKeyDown={this.onKeyDown} - renderNode={this.renderNode} + renderBlock={this.renderBlock} /> ) } - renderNode = (props, editor, next) => { + renderBlock = (props, editor, next) => { switch (props.node.type) { case 'code': return From d21cb09a12c35453d9053acb45f51ae5645d0ca7 Mon Sep 17 00:00:00 2001 From: adjourn <17890701+adjourn@users.noreply.github.com> Date: Sun, 12 May 2019 06:29:11 +0300 Subject: [PATCH 04/19] Destructure texts iterable (#2764) --- packages/slate-react/src/plugins/dom/after.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/slate-react/src/plugins/dom/after.js b/packages/slate-react/src/plugins/dom/after.js index 38100858d..973683bc2 100644 --- a/packages/slate-react/src/plugins/dom/after.js +++ b/packages/slate-react/src/plugins/dom/after.js @@ -552,7 +552,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', }) From e4fae234542cd3dbb9cb6639533d02fc3ea8e5d7 Mon Sep 17 00:00:00 2001 From: Sunny Hirai Date: Wed, 15 May 2019 14:50:11 -0700 Subject: [PATCH 05/19] Separate out EditorValue component for examples (#2785) * Separate out EditorValue component for examples * Made it prettier --- examples/components.js | 46 ++++++++++++++++++++++++++++++++ examples/composition/index.js | 50 +++-------------------------------- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/examples/components.js b/examples/components.js index d0dcbce80..ef8427e3a 100644 --- a/examples/components.js +++ b/examples/components.js @@ -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 ( +
+
+ Slate's value as text +
+
+ {textLines} +
+
+ ) + } +) + export const Icon = React.forwardRef(({ className, ...props }, ref) => ( ( /> ) -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,7 +171,7 @@ 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 (
@@ -257,12 +218,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 : ' '}
- ))} -
+
) } From d24f3b16ec050c9eedca343adf444e30c67101d1 Mon Sep 17 00:00:00 2001 From: Sunny Hirai Date: Thu, 16 May 2019 10:59:51 -0700 Subject: [PATCH 06/19] Restore dom (#2782) * Working version of restore dom * Fix linting errors --- examples/app.js | 2 + examples/restore-dom/index.js | 146 ++++++++++++++++++ examples/restore-dom/value.json | 38 +++++ .../slate-react/src/components/content.js | 2 + packages/slate-react/src/components/editor.js | 4 +- .../slate-react/src/plugins/react/index.js | 11 +- .../src/plugins/react/restore-dom.js | 21 +++ 7 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 examples/restore-dom/index.js create mode 100644 examples/restore-dom/value.json create mode 100644 packages/slate-react/src/plugins/react/restore-dom.js diff --git a/examples/app.js b/examples/app.js index bc0f08cad..fb329271f 100644 --- a/examples/app.js +++ b/examples/app.js @@ -27,6 +27,7 @@ import PlainText from './plain-text' import Plugins from './plugins' import RTL from './rtl' import ReadOnly from './read-only' +import RestoreDOM from './restore-dom' import RichText from './rich-text' import SearchHighlighting from './search-highlighting' import Composition from './composition' @@ -63,6 +64,7 @@ const EXAMPLES = [ ['Plain Text', PlainText, '/plain-text'], ['Plugins', Plugins, '/plugins'], ['Read-only', ReadOnly, '/read-only'], + ['Restore DOM', RestoreDOM, '/restore-dom'], ['Rich Text', RichText, '/rich-text'], ['RTL', RTL, '/rtl'], ['Search Highlighting', SearchHighlighting, '/search-highlighting'], diff --git a/examples/restore-dom/index.js b/examples/restore-dom/index.js new file mode 100644 index 000000000..645939904 --- /dev/null +++ b/examples/restore-dom/index.js @@ -0,0 +1,146 @@ +import { Editor } from 'slate-react' +import { Value } from 'slate' + +import React from 'react' +import initialValue from './value.json' +import { Button, Icon, 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: '#ffffff', + } + + /** + * Store a reference to the `editor`. + * + * @param {Editor} editor + */ + + ref = editor => { + this.editor = editor + } + + /** + * Render. + * + * @return {Element} + */ + + render() { + return ( +
+ + {this.renderHighlightButton('#ffffff')} + {this.renderHighlightButton('#ffeecc')} + {this.renderHighlightButton('#ffffcc')} + {this.renderHighlightButton('#ccffcc')} + {this.renderHighlightButton('#ccffff')} + + +
+ ) + } + + /** + * Render a highlight button + * + * @param {String} bgcolor + * @return {Element} + */ + + renderHighlightButton = bgcolor => { + const isActive = this.state.bgcolor === bgcolor + return ( + + ) + } + + /** + * Highlight every block with a given background color + * + * @param {String} bgcolor + */ + + onClickHighlight = bgcolor => { + const { editor } = this + this.setState({ bgcolor }) + editor.restoreDOM() + } + + /** + * 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() + } + } + + /** + * 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..5011dacdd --- /dev/null +++ b/examples/restore-dom/value.json @@ -0,0 +1,38 @@ +{ + "object": "value", + "document": { + "object": "document", + "nodes": [ + { + "object": "block", + "type": "paragraph", + "nodes": [ + { + "object": "text", + "text": "First block of text" + } + ] + }, + { + "object": "block", + "type": "paragraph", + "nodes": [ + { + "object": "text", + "text": "Second block of text" + } + ] + }, + { + "object": "block", + "type": "paragraph", + "nodes": [ + { + "object": "text", + "text": "Third block of text" + } + ] + } + ] + } +} diff --git a/packages/slate-react/src/components/content.js b/packages/slate-react/src/components/content.js index c4ffe5d60..7c0cb8098 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, @@ -486,6 +487,7 @@ class Content extends React.Component { return ( this.run(handler, event)} diff --git a/packages/slate-react/src/plugins/react/index.js b/packages/slate-react/src/plugins/react/index.js index 4fa44e970..24bc7eb79 100644 --- a/packages/slate-react/src/plugins/react/index.js +++ b/packages/slate-react/src/plugins/react/index.js @@ -4,6 +4,7 @@ import EditorPropsPlugin from './editor-props' import RenderingPlugin from './rendering' import QueriesPlugin from './queries' import DOMPlugin from '../dom' +import RestoreDOMPlugin from './restore-dom' /** * A plugin that adds the React-specific rendering logic to the editor. @@ -20,7 +21,7 @@ function ReactPlugin(options = {}) { const domPlugin = DOMPlugin({ plugins: [editorPropsPlugin, ...plugins], }) - + const restoreDomPlugin = RestoreDOMPlugin() const placeholderPlugin = PlaceholderPlugin({ placeholder, when: (editor, node) => @@ -30,7 +31,13 @@ function ReactPlugin(options = {}) { Array.from(node.texts()).length === 1, }) - return [domPlugin, placeholderPlugin, renderingPlugin, queriesPlugin] + return [ + domPlugin, + restoreDomPlugin, + placeholderPlugin, + renderingPlugin, + queriesPlugin, + ] } /** 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 From 7603415e5d1e5fee074d994cafe47a56d962d3e1 Mon Sep 17 00:00:00 2001 From: Sunny Hirai Date: Thu, 16 May 2019 13:43:44 -0700 Subject: [PATCH 07/19] Extract Instruction component into components for examples (#2790) --- examples/components.js | 17 ++++++++++ examples/composition/index.js | 58 ++++++++++++--------------------- examples/composition/special.js | 8 ++--- examples/composition/util.js | 6 ++-- 4 files changed, 45 insertions(+), 44 deletions(-) diff --git a/examples/components.js b/examples/components.js index ef8427e3a..8b0d36d9e 100644 --- a/examples/components.js +++ b/examples/components.js @@ -80,6 +80,23 @@ export const Icon = React.forwardRef(({ className, ...props }, ref) => ( /> )) +export const Instruction = React.forwardRef(({ className, ...props }, ref) => ( +
+)) + export const Menu = React.forwardRef(({ className, ...props }, ref) => (
( -
-) - 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; `} /> ) @@ -174,26 +164,20 @@ class RichTextExample extends React.Component { // 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}
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' }] } } From fd8e18b9cf5662e374e246a98ee62d544bd59446 Mon Sep 17 00:00:00 2001 From: Ralph Feltis Date: Thu, 16 May 2019 16:25:49 -0700 Subject: [PATCH 08/19] Fix typo in commands.md (#2791) * Adding Guilded to Products list * Fixing typo * Fixing typo --- docs/reference/slate/commands.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/slate/commands.md b/docs/reference/slate/commands.md index c25f7e96f..cf1a748ee 100644 --- a/docs/reference/slate/commands.md +++ b/docs/reference/slate/commands.md @@ -538,8 +538,8 @@ Wrap the given node in a [`Inline`](./inline.md) node that match `properties`. F ### `wrapNodeByKey/Path` -`wraNodeByKey(key: String, parent: Node) => Editor`
-`wraNodeByPath(path: List, parent: Node) => Editor`
+`wrapNodeByKey(key: String, parent: Node) => Editor`
+`wrapNodeByPath(path: List, parent: Node) => Editor`
Wrap the node with the specified key with the parent [`Node`](./node.md). This will clear all children of the parent. From 48eb6700b7fbd825383f39b788072a4ad3486601 Mon Sep 17 00:00:00 2001 From: Sunny Hirai Date: Thu, 16 May 2019 16:26:13 -0700 Subject: [PATCH 09/19] Add error boundary to save Slate from a crash on corrupt DOM (#2792) * Working version of restore dom * Fix linting errors * Add button to corrupt DOM * Added error boundary that fixes DOM on render error * Fix linting errors * Fix debug output for componentDidCatch * Improve example by adding a separate restoreDOM button * Remove key change from error boundary which is not necessary * Fix linting error --- examples/restore-dom/index.js | 90 +++++++++++++++++-- examples/restore-dom/value.json | 33 ++++++- .../slate-react/src/components/content.js | 16 ++++ 3 files changed, 130 insertions(+), 9 deletions(-) diff --git a/examples/restore-dom/index.js b/examples/restore-dom/index.js index 645939904..c8a572066 100644 --- a/examples/restore-dom/index.js +++ b/examples/restore-dom/index.js @@ -3,7 +3,7 @@ import { Value } from 'slate' import React from 'react' import initialValue from './value.json' -import { Button, Icon, Toolbar } from '../components' +import { Button, EditorValue, Icon, Instruction, Toolbar } from '../components' /** * The Restore DOM example. @@ -31,7 +31,7 @@ class RestoreDOMExample extends React.Component { state = { value: Value.fromJSON(initialValue), - bgcolor: '#ffffff', + bgcolor: '#ffeecc', } /** @@ -53,12 +53,25 @@ class RestoreDOMExample extends React.Component { render() { return (
+ +
    +
  1. + Click a brush to change color in state. Use refresh button to + `restoreDOM` which renders changes. +
  2. +
  3. + 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. +
  4. +
+
- {this.renderHighlightButton('#ffffff')} {this.renderHighlightButton('#ffeecc')} {this.renderHighlightButton('#ffffcc')} {this.renderHighlightButton('#ccffcc')} - {this.renderHighlightButton('#ccffff')} + {this.renderCorruptButton()} + {this.renderRestoreButton()} +
) } @@ -93,6 +108,48 @@ class RestoreDOMExample extends React.Component { ) } + /** + * Render restoreDOM button + */ + + renderRestoreButton = () => { + const { editor } = this + + function restoreDOM() { + editor.restoreDOM() + } + + return ( + + ) + } + + /** + * 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 ( + + ) + } + /** * Highlight every block with a given background color * @@ -100,9 +157,7 @@ class RestoreDOMExample extends React.Component { */ onClickHighlight = bgcolor => { - const { editor } = this this.setState({ bgcolor }) - editor.restoreDOM() } /** @@ -128,6 +183,29 @@ class RestoreDOMExample extends React.Component { } } + /** + * 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`. * diff --git a/examples/restore-dom/value.json b/examples/restore-dom/value.json index 5011dacdd..64a4a8e58 100644 --- a/examples/restore-dom/value.json +++ b/examples/restore-dom/value.json @@ -9,7 +9,16 @@ "nodes": [ { "object": "text", - "text": "First block of text" + "text": "First block with " + }, + { + "object": "text", + "text": "bold", + "marks": [{ "type": "bold" }] + }, + { + "object": "text", + "text": " text in it" } ] }, @@ -19,7 +28,16 @@ "nodes": [ { "object": "text", - "text": "Second block of text" + "text": "Second block with " + }, + { + "object": "text", + "text": "bold", + "marks": [{ "type": "bold" }] + }, + { + "object": "text", + "text": " text in it" } ] }, @@ -29,7 +47,16 @@ "nodes": [ { "object": "text", - "text": "Third block of 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 7c0cb8098..2d150f4c9 100644 --- a/packages/slate-react/src/components/content.js +++ b/packages/slate-react/src/components/content.js @@ -75,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. * From 79a2d215713ee55f4f758eaecaa1a8189f804a26 Mon Sep 17 00:00:00 2001 From: Sunny Hirai Date: Fri, 17 May 2019 15:26:48 -0700 Subject: [PATCH 10/19] Reconcile dom node (#2801) * Add reconcileDOMNode command fixes set-text-from-dom-node * Fix linting and cleanup unused files * Removed unnecessary console log --- .../src/plugins/android/dom-snapshot.js | 3 +- .../slate-react/src/plugins/android/index.js | 5 +- packages/slate-react/src/plugins/dom/after.js | 3 +- .../slate-react/src/plugins/react/commands.js | 60 +++++++++++++++++ .../slate-react/src/plugins/react/index.js | 3 + .../src/utils/set-text-from-dom-node.js | 67 ------------------- 6 files changed, 67 insertions(+), 74 deletions(-) create mode 100644 packages/slate-react/src/plugins/react/commands.js delete mode 100644 packages/slate-react/src/utils/set-text-from-dom-node.js 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/dom/after.js b/packages/slate-react/src/plugins/dom/after.js index 973683bc2..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() } 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..2df38dbef --- /dev/null +++ b/packages/slate-react/src/plugins/react/commands.js @@ -0,0 +1,60 @@ +/** + * A set of commands for the React plugin. + * + * @return {Object} + */ + +function CommandsPlugin() { + /** + * reconcileDOMNode takes text from inside the `domNode` and uses it to set + * the text inside the matching `node` in Slate. + * + * @param {Window} window + * @param {Editor} editor + * @param {Node} domNode + */ + + function reconcileDOMNode(editor, domNode) { + const { value } = editor + const { document, selection } = value + const domElement = domNode.parentElement.closest('[data-key]') + const point = editor.findPoint(domElement, 0) + const node = document.getDescendant(point.path) + const block = document.getClosestBlock(point.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(point.path, 0) + .moveFocusTo(point.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 + } + + return { + commands: { + 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 24bc7eb79..b70bdc9ec 100644 --- a/packages/slate-react/src/plugins/react/index.js +++ b/packages/slate-react/src/plugins/react/index.js @@ -2,6 +2,7 @@ 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' @@ -16,6 +17,7 @@ import RestoreDOMPlugin from './restore-dom' function ReactPlugin(options = {}) { const { placeholder = '', plugins = [] } = options const renderingPlugin = RenderingPlugin(options) + const commandsPlugin = CommandsPlugin(options) const queriesPlugin = QueriesPlugin(options) const editorPropsPlugin = EditorPropsPlugin(options) const domPlugin = DOMPlugin({ @@ -36,6 +38,7 @@ function ReactPlugin(options = {}) { restoreDomPlugin, placeholderPlugin, renderingPlugin, + commandsPlugin, queriesPlugin, ] } 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) -} From cf4bf38aca4565c88dfc36efc81d5858cc992a81 Mon Sep 17 00:00:00 2001 From: Sunny Hirai Date: Fri, 17 May 2019 16:28:32 -0700 Subject: [PATCH 11/19] Reconcile node (#2802) * Add reconcileNode command * Fix lint errors * Remove node.path shortcut as path is not a property anymore --- .../slate-react/src/plugins/react/commands.js | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/slate-react/src/plugins/react/commands.js b/packages/slate-react/src/plugins/react/commands.js index 2df38dbef..ca0249fb0 100644 --- a/packages/slate-react/src/plugins/react/commands.js +++ b/packages/slate-react/src/plugins/react/commands.js @@ -6,21 +6,20 @@ function CommandsPlugin() { /** - * reconcileDOMNode takes text from inside the `domNode` and uses it to set - * the text inside the matching `node` in Slate. + * Takes a `node`, find the matching `domNode` and uses it to set the text + * in the `node`. * - * @param {Window} window * @param {Editor} editor - * @param {Node} domNode + * @param {Node} node */ - function reconcileDOMNode(editor, domNode) { + function reconcileNode(editor, node) { const { value } = editor const { document, selection } = value - const domElement = domNode.parentElement.closest('[data-key]') - const point = editor.findPoint(domElement, 0) - const node = document.getDescendant(point.path) - const block = document.getClosestBlock(point.path) + const path = document.getPath(node.key) + + const domElement = editor.findDOMNode(path) + const block = document.getClosestBlock(path) // Get text information const { text } = node @@ -39,9 +38,7 @@ function CommandsPlugin() { // If the text is no different, abort. if (text === domText) return - let entire = selection - .moveAnchorTo(point.path, 0) - .moveFocusTo(point.path, text.length) + let entire = selection.moveAnchorTo(path, 0).moveFocusTo(path, text.length) entire = document.resolveRange(entire) @@ -50,8 +47,23 @@ function CommandsPlugin() { 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, }, } From b9cdaeb6a5d0fe4cc5be05529ff38a4ec2873a9c Mon Sep 17 00:00:00 2001 From: Sunny Hirai Date: Fri, 17 May 2019 18:08:19 -0700 Subject: [PATCH 12/19] Add debug plugins (#2803) * Add debug plugins * Fix lint errors --- .../src/plugins/debug/debug-batch-events.js | 111 ++++++++++++++++++ .../src/plugins/debug/debug-events.js | 52 ++++++++ .../slate-react/src/plugins/debug/index.js | 64 ---------- .../src/plugins/debug/stringify-event.js | 21 ++++ .../slate-react/src/plugins/react/index.js | 11 ++ 5 files changed, 195 insertions(+), 64 deletions(-) create mode 100644 packages/slate-react/src/plugins/debug/debug-batch-events.js create mode 100644 packages/slate-react/src/plugins/debug/debug-events.js delete mode 100644 packages/slate-react/src/plugins/debug/index.js create mode 100644 packages/slate-react/src/plugins/debug/stringify-event.js 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/react/index.js b/packages/slate-react/src/plugins/react/index.js index b70bdc9ec..346c4dd6c 100644 --- a/packages/slate-react/src/plugins/react/index.js +++ b/packages/slate-react/src/plugins/react/index.js @@ -1,3 +1,4 @@ +import Debug from 'debug' import PlaceholderPlugin from 'slate-react-placeholder' import EditorPropsPlugin from './editor-props' @@ -6,6 +7,8 @@ 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. @@ -16,6 +19,12 @@ import RestoreDOMPlugin from './restore-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) @@ -34,6 +43,8 @@ function ReactPlugin(options = {}) { }) return [ + debugEventsPlugin, + debugBatchEventsPlugin, domPlugin, restoreDomPlugin, placeholderPlugin, From 09f4662d31217d5d2f156303316a593f815d44e8 Mon Sep 17 00:00:00 2001 From: Justin Weiss Date: Mon, 20 May 2019 13:17:28 -0700 Subject: [PATCH 13/19] Use relative path to find a node's text nodes (#2799) When `block.texts()` is passed a path, it treats that path as relative to itself. `findSelection` passes it the selection path, which is relative to the document. In order to find the correct text nodes, we must convert the selection path to a path relative to the block we found. Then, when a new path is returned, we need to convert that new block-relative path back to a document-relative path. --- packages/slate-react/src/plugins/react/queries.js | 14 ++++++++++---- .../src/utils/get-selection-from-dom.js | 15 +++++++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/slate-react/src/plugins/react/queries.js b/packages/slate-react/src/plugins/react/queries.js index 789edfb36..73481985c 100644 --- a/packages/slate-react/src/plugins/react/queries.js +++ b/packages/slate-react/src/plugins/react/queries.js @@ -483,11 +483,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 +500,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/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) } } From 559bde9a213547a643e103b5de3bac4e8e35ce5c Mon Sep 17 00:00:00 2001 From: Justin Weiss Date: Mon, 20 May 2019 13:18:31 -0700 Subject: [PATCH 14/19] Allow deleteAtRange with a zero-length range on the first character (#2800) deleteAtRange will consider a zero-length range on the first character of a text node as a hanging selection, which is incorrect. This should not be considered hanging. It's still possible to hit the `startKey === endKey && isHanging` conditional if endKey is in a void node, since we will bump the selection to the previous node and update endKey (but then endOffset is no longer 0, so it's not _really_ hanging anymore). We have an existing test for that, and it still passes after this change. --- packages/slate/src/commands/at-range.js | 3 +- .../at-current-range/delete/first-position.js | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 packages/slate/test/commands/at-current-range/delete/first-position.js diff --git a/packages/slate/src/commands/at-range.js b/packages/slate/src/commands/at-range.js index 04d3e95e0..52d92bd94 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) { 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 + + + +) From 020672a2ea2eb328100101c29d6618d606569797 Mon Sep 17 00:00:00 2001 From: Entkenntnis Date: Mon, 20 May 2019 22:31:41 +0200 Subject: [PATCH 15/19] fixing order of blocks if inserted at start of node (#2772) * fixing order of blocks if inserted at start of node * fix lint errors --- packages/slate/src/commands/at-range.js | 45 +++++++++++++++---- .../fragment-nested-blocks-end-of-node.js | 40 +++++++++++++++++ .../fragment-nested-blocks-start-of-node.js | 40 +++++++++++++++++ 3 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 packages/slate/test/commands/at-current-range/insert-fragment/fragment-nested-blocks-end-of-node.js create mode 100644 packages/slate/test/commands/at-current-range/insert-fragment/fragment-nested-blocks-start-of-node.js diff --git a/packages/slate/src/commands/at-range.js b/packages/slate/src/commands/at-range.js index 52d92bd94..1a13fa9a4 100644 --- a/packages/slate/src/commands/at-range.js +++ b/packages/slate/src/commands/at-range.js @@ -662,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)) { @@ -694,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`. * @@ -744,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/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 + + +) From 7adf56ffd162a733e0479441d63c556fc568c340 Mon Sep 17 00:00:00 2001 From: Cary Dunn Date: Mon, 20 May 2019 13:34:16 -0700 Subject: [PATCH 16/19] gracefully handle null content ref in findDOMNode query (#2774) --- packages/slate-react/src/plugins/react/queries.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/slate-react/src/plugins/react/queries.js b/packages/slate-react/src/plugins/react/queries.js index 73481985c..2235727bf 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 } From fbe70a1f2803b9f4ee3c93d8e5bff8604fc8059d Mon Sep 17 00:00:00 2001 From: Jon Portella <35599326+jportella93@users.noreply.github.com> Date: Mon, 20 May 2019 22:34:30 +0200 Subject: [PATCH 17/19] Fix: prop name (#2780) Nodes weren't properly rendering because the Editor was trying to call the function prop renderBlock but instead another called renderNode was passed. --- docs/walkthroughs/saving-and-loading-html-content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/walkthroughs/saving-and-loading-html-content.md b/docs/walkthroughs/saving-and-loading-html-content.md index 2f9e0692d..8969ba97c 100644 --- a/docs/walkthroughs/saving-and-loading-html-content.md +++ b/docs/walkthroughs/saving-and-loading-html-content.md @@ -258,7 +258,7 @@ class App extends React.Component { value={this.state.value} onChange={this.onChange} // Add the ability to render our nodes and marks... - renderNode={this.renderNode} + renderBlock={this.renderNode} renderMark={this.renderMark} /> ) From 4eff9b5a066125d603f83eef2f1424b793055963 Mon Sep 17 00:00:00 2001 From: adjourn <17890701+adjourn@users.noreply.github.com> Date: Tue, 21 May 2019 00:37:31 +0300 Subject: [PATCH 18/19] Fix leaf memoization bug (#2766) * Fix leaf memoization bug * Update leaf.js --- packages/slate-react/src/components/leaf.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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) ) From 3c013c567a1092ad35b9d223a04f1b5c1461bb5f Mon Sep 17 00:00:00 2001 From: Cary Dunn Date: Mon, 20 May 2019 14:41:40 -0700 Subject: [PATCH 19/19] fallback findPath query to find closest element with a data-key (#2794) * fallback findPath query to find closest element with a data-key * lint * move example to findEventRange query * lint --- examples/images/index.js | 4 +-- .../slate-react/src/plugins/dom/before.js | 2 +- .../slate-react/src/plugins/react/queries.js | 25 +++++++++++++------ 3 files changed, 21 insertions(+), 10 deletions(-) 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/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/queries.js b/packages/slate-react/src/plugins/react/queries.js index 2235727bf..acd3ff1b7 100644 --- a/packages/slate-react/src/plugins/react/queries.js +++ b/packages/slate-react/src/plugins/react/queries.js @@ -181,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 @@ -234,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 } @@ -248,7 +259,7 @@ function QueriesPlugin() { return null } - if (element === instance.ref.current) { + if (nodeElement === instance.ref.current) { return p }