diff --git a/docs/guides/changes.md b/docs/guides/changes.md index 7072d8649..6001c201f 100644 --- a/docs/guides/changes.md +++ b/docs/guides/changes.md @@ -67,7 +67,9 @@ These are changes like `blur()`, `collapseToStart()`, `moveToRangeOf()`, etc. th ### On a Specific Node -These are changes like `removeNodeByKey()`, `setNodeByKey()`, `removeMarkByKey()`, etc. that take a `key` string referring to a specific node, and then change that node in different ways. These are often what you use when making programmatic changes from inside your custom node components, where you already have a reference to `props.node.key`. +There are two types of changes referring to specific nodes, either by `path` or by `key`. These are often what you use when making programmatic changes from inside your custom node components, where you already have a reference to `props.node.key`. + +Path-based changes are ones like `removeNodeByPath()`, `insertNodeByPath()`, etc. that take a `path` pinpointing the node in the document. And key-based changes are ones like `removeNodeByKey()`, `setNodeByKey()`, `removeMarkByKey()`, etc. that take a `key` string referring to a specific node, and then change that node in different ways. ### On the Top-level Value diff --git a/docs/guides/data-model.md b/docs/guides/data-model.md index e66a26a01..d4657e4cb 100644 --- a/docs/guides/data-model.md +++ b/docs/guides/data-model.md @@ -94,7 +94,7 @@ That all sounds pretty complex, but you don't have to think about it much, as lo Just like in the DOM, you can reference a part of the document using a `Range`. And there's one special range that Slate keeps track of that refers to the user's current cursor selection, called the "selection". -Ranges are defined by an "anchor" and "focus" point. The anchor is where the range starts, and the focus is where it ends. And each point is a combination of a "key" referencing a specific node, and an "offset". This ends up looking like this: +Ranges are defined by an "anchor" and "focus" point. The anchor is where the range starts, and the focus is where it ends. And each point is a combination of a "path" or "key" referencing a specific node, and an "offset". This ends up looking like this: ```js const range = Range.create({ @@ -104,9 +104,17 @@ const range = Range.create({ focusOffset: 4, isBackward: false, }) + +const range = Range.create({ + anchorPath: [0, 2, 1], + anchorOffset: 0, + focusPath: [0, 3, 2], + focusOffset: 4, + isBackward: false, +}) ``` -The more readable `node-a` name is just pseudocode, because Slate uses auto-incrementing numerical strings by default—`'1', '2', '3', ...` But the important part is that every node has a unique `key` property, and a range references nodes by their keys. +The more readable `node-a` name is just pseudocode, because Slate uses auto-incrementing numerical strings by default—`'1', '2', '3', ...` But the important part is that every node has a unique `key` property, and a range can reference nodes by their keys. The terms "anchor" and "focus" are borrowed from the DOM, where they mean the same thing. The anchor point isn't always _before_ the focus point in the document. Just like in the DOM, it depends on whether the range is backwards or forwards. @@ -115,7 +123,7 @@ Here's how MDN explains it: > A user may make a selection from left to right (in document order) or right to left (reverse of document order). The anchor is where the user began the selection and the focus is where the user ends the selection. If you make a selection with a desktop mouse, the anchor is placed where you pressed the mouse button and the focus is placed where you released the mouse button. Anchor and focus should not be confused with the start and end positions of a selection, since anchor can be placed before the focus or vice versa, depending on the direction you made your selection. > — [`Selection`, MDN](https://developer.mozilla.org/en-US/docs/Web/API/Selection) -To make dealing with ranges easier though, they also provide "start" and "end" properties that take whether the range is forward or backward into account. The `startKey` and `startOffset` will always be before the `endKey` and `endOffset` in the document. +To make dealing with ranges easier though, they also provide "start" and "end" properties that take whether the range is forward or backward into account. The `startKey` and `startPath` will always be before the `endKey` and `endPath` in the document. One important thing to note is that the anchor and focus points of ranges **always reference the "leaf-most" text nodes**. They never reference blocks or inlines, always their child text nodes. This makes dealing with ranges a _lot_ easier. diff --git a/docs/reference/slate/node.md b/docs/reference/slate/node.md index c703f7e42..894212378 100644 --- a/docs/reference/slate/node.md +++ b/docs/reference/slate/node.md @@ -44,21 +44,40 @@ Deeply filter the descendant nodes of a node by `iterator`. ### `findDescendant` -`findDescendant(iterator: Function) => Node || Void` +`findDescendant(iterator: Function) => Node|Void` Deeply find a descendant node by `iterator`. +### `getAncestors` + +`getAncestors(path: List|Array) => List|Void` +`getAncestors(key: String) => List|Void` + +Get the ancestors of a descendant by `path` or `key`. + +### `getBlocks` + +`getBlocks() => List` + +Get all of the bottom-most [`Block`](./block.md) node descendants. + ### `getBlocksAtRange` `getBlocksAtRange(range: Range) => List` Get all of the bottom-most [`Block`](./block.md) nodes in a `range`. -### `getBlocks` +### `getBlocksByType` -`getBlocks() => List` +`getBlocksByType(type: String) => List` -Get all of the bottom-most [`Block`](./block.md) node descendants. +Get all of the bottom-most [`Block`](./block.md) nodes by `type`. + +### `getCharacters` + +`getCharacters() => List` + +Get a list of all of the [`Characters`](./character.md) in the node. ### `getCharactersAtRange` @@ -68,43 +87,63 @@ Get a list of all of the [`Characters`](./character.md) in a `range`. ### `getChild` -`getChild(key: String || Node) => Node || Void` +`getChild(path: List|Array) => Node|Void` +`getChild(key: String) => Node|Void` -Get a child by `key`. - -### `getClosestBlock` - -`getClosestBlock(key: String || Node) => Node || Void` - -Get the closest [`Block`](./block.md) node to a descendant node by `key`. - -### `getClosestInline` - -`getClosestInline(key: String || Node) => Node || Void` - -Get the closest [`Inline`](./inline.md) node to a descendant node by `key`. +Get a child by `path` or `key`. ### `getClosest` -`getClosest(key: String || Node, match: Function) => Node || Void` +`getClosest(path: List|Array, match: Function) => Node|Void` +`getClosest(key: String, match: Function) => Node|Void` -Get the closest parent node of a descendant node by `key` that matches a `match` function. +Get the closest parent node of a descendant node by `path` or `key` that matches a `match` function. + +### `getClosestBlock` + +`getClosestBlock(path: List|Array) => Node|Void` +`getClosestBlock(key: String) => Node|Void` + +Get the closest [`Block`](./block.md) node to a descendant node by `path` or `key`. + +### `getClosestInline` + +`getClosestInline(path: List|Array) => Node|Void` +`getClosestInline(key: String) => Node|Void` + +Get the closest [`Inline`](./inline.md) node to a descendant node by `path` or `key`. + +### `getClosestVoid` + +`getClosestVoid(path: List|Array) => Node|Void` +`getClosestVoid(key: String) => Node|Void` + +Get the closest void parent of a descendant node by `path` or `key`. + +### `getCommonAncestor` + +`getCommonAncestor(path: List|Array) => Number` +`getCommonAncestor(key: String) => Number` + +Get the lowest common ancestor of a descendant node by `path` or `key`. ### `getDepth` -`getDepth(key: String || Node) => Number` +`getDepth(path: List|Array) => Number` +`getDepth(key: String) => Number` -Get the depth of a descendant node by `key`. +Get the depth of a descendant node by `path` or `key`. ### `getDescendant` -`getDescendant(key: String || Node) => Node || Void` +`getDescendant(path: List|Array) => Node|Void` +`getDescendant(key: String) => Node|Void` -Get a descendant node by `key`. +Get a descendant node by `path` or `key`. ### `getFirstText` -`getFirstText() => Node || Void` +`getFirstText() => Text|Void` Get the first child text node inside a node. @@ -116,33 +155,44 @@ Get a document fragment of the nodes in a `range`. ### `getFurthest` -`getFurthest(key: String, iterator: Function) => Node || Null` +`getFurthest(path: List|Array, iterator: Function) => Node|Null` +`getFurthest(key: String, iterator: Function) => Node|Null` -Get the furthest parent of a node by `key` that matches an `iterator`. +Get the furthest parent of a node by `path` or `key` that matches an `iterator`. ### `getFurthestAncestor` -`getFurthestAncestor(key: String) => Node || Null` +`getFurthestAncestor(path: List|Array) => Node|Null` +`getFurthestAncestor(key: String) => Node|Null` -Get the furthest ancestor of a node by `key`. +Get the furthest ancestor of a node by `path` or `key`. ### `getFurthestBlock` -`getFurthestBlock(key: String) => Node || Null` +`getFurthestBlock(path: List|Array) => Node|Null` +`getFurthestBlock(key: String) => Node|Null` -Get the furthest block parent of a node by `key`. +Get the furthest block parent of a node by `path` or `key`. ### `getFurthestInline` -`getFurthestInline(key: String) => Node || Null` +`getFurthestInline(path: List|Array) => Node|Null` +`getFurthestInline(key: String) => Node|Null` -Get the furthest inline parent of a node by `key`. +Get the furthest inline parent of a node by `path` or `key`. ### `getFurthestOnlyChildAncestor` -`getFurthestOnlyChildAncestor(key: String) => Node || Null` +`getFurthestOnlyChildAncestor(path: List|Array) => Node|Null` +`getFurthestOnlyChildAncestor(key: String) => Node|Null` -Get the furthest ancestor of a node by `key` that has only one child. +Get the furthest ancestor of a node by `path` or `key` that has only one child. + +### `getInlines` + +`getInlines() => List` + +Get all of the top-most [`Inline`](./inline.md) nodes in a node. ### `getInlinesAtRange` @@ -150,59 +200,119 @@ Get the furthest ancestor of a node by `key` that has only one child. Get all of the top-most [`Inline`](./inline.md) nodes in a `range`. +### `getInlinesByType` + +`getInlinesByType(type: string) => List` + +Get all of the top-most [`Inline`](./inline.md) nodes by `type`. + ### `getLastText` -`getLastText() => Node || Void` +`getLastText() => Node|Void` Get the last child text node inside a node. +### `getMarks` + +`getMarks(range: Range) => Set` + +Get a set of all of the marks in a node. + ### `getMarksAtRange` `getMarksAtRange(range: Range) => Set` Get a set of all of the marks in a `range`. +### `getMarksByType` + +`getMarksByType(type: String) => Set` + +Get a set of all of the marks by `type`. + ### `getNextBlock` -`getNextBlock(key: String || Node) => Node || Void` +`getNextBlock(path: List|Array) => Node|Void` +`getNextBlock(key: String) => Node|Void` -Get the next, bottom-most [`Block`](./block.md) node after a descendant by `key`. +Get the next, bottom-most [`Block`](./block.md) node after a descendant by `path` or `key`. + +### `getNextNode` + +`getNextNode(path: List|Array) => Node|Void` +`getNextNode(key: String) => Node|Void` + +Get the next node in the tree of a descendant by `path` or `key`. This will not only check for siblings but instead move up the tree returning the next ancestor if no sibling is found. ### `getNextSibling` -`getNextSibling(key: String || Node) => Node || Void` +`getNextSibling(path: List|Array) => Node|Void` +`getNextSibling(key: String) => Node|Void` -Get the next sibling of a descendant by `key`. +Get the next sibling of a descendant by `path` or `key`. ### `getNextText` -`getNextText(key: String || Node) => Node || Void` +`getNextText(path: List|Array) => Node|Void` +`getNextText(key: String) => Node|Void` -Get the next [`Text`](./text.md) node after a descendant by `key`. +Get the next [`Text`](./text.md) node after a descendant by `path` or `key`. + +### `getNode` + +`getNode(path: List|Array) => Node|Void` +`getNode(key: String) => Node|Void` + +Get a node in the tree by `path` or `key`. + +### `getOffset` + +`getOffset(path: List|Array) => Number` +`getOffset(key: String) => Number` + +Get the text offset of a descendant in the tree by `path` or `key`. ### `getParent` -`getParent(key: String || Node) => Node || Void` +`getParent(path: List|Array) => Node|Void` +`getParent(key: String) => Node|Void` -Get the parent node of a descendant by `key`. +Get the parent node of a descendant by `path` or `key`. + +### `getPath` + +`getPath(path: List|Array) => Node|Void` +`getPath(key: String) => Node|Void` + +Get the path to a descendant by `path` or `key`. ### `getPreviousBlock` -`getPreviousBlock(key: String || Node) => Node || Void` +`getPreviousBlock(path: List|Array) => Node|Void` +`getPreviousBlock(key: String) => Node|Void` -Get the previous, bottom-most [`Block`](./block.md) node before a descendant by `key`. +Get the previous, bottom-most [`Block`](./block.md) node before a descendant by `path` or `key`. + +### `getPreviousNode` + +`getPreviousNode(path: List|Array) => Node|Void` +`getPreviousNode(key: String) => Node|Void` + +Get the previous node in the tree of a descendant by `path` or `key`. This will not only check for siblings but instead move up the tree returning the previous ancestor if no sibling is found. ### `getPreviousSibling` -`getPreviousSibling(key: String || Node) => Node || Void` +`getPreviousSibling(path: List|Array) => Node|Void` +`getPreviousSibling(key: String) => Node|Void` -Get the previous sibling of a descendant by `key`. +Get the previous sibling of a descendant by `path` or `key`. ### `getPreviousText` -`getPreviousText(key: String || Node) => Node || Void` +`getPreviousText(path: List|Array) => Node|Void` +`getPreviousText(key: String) => Node|Void` -Get the previous [`Text`](./text.md) node before a descendant by `key`. +Get the previous [`Text`](./text.md) node before a descendant by `path` or `key`. ### `getTextAtOffset` @@ -210,6 +320,18 @@ Get the previous [`Text`](./text.md) node before a descendant by `key`. Get the [`Text`](./text.md) node at an `offset`. +### `getTextDirection` + +`getTextDirection() => String` + +Get the direction of the text content in the node. + +### `getTexts` + +`getTexts(range: Range) => List` + +Get all of the [`Text`](./text.md) nodes in a node. + ### `getTextsAtRange` `getTextsAtRange(range: Range) => List` @@ -218,12 +340,21 @@ Get all of the [`Text`](./text.md) nodes in a `range`. ### `hasChild` -`hasChild(key: String || Node) => Boolean` +`hasChild(path: List|Array) => Boolean` +`hasChild(key: String) => Boolean` -Check whether the node has a child node by `key`. +Check whether the node has a child node by `path` or `key`. ### `hasDescendant` -`hasDescendant(key: String || Node) => Boolean` +`hasDescendant(path: List|Array) => Boolean` +`hasDescendant(key: String) => Boolean` -Check whether the node has a descendant node by `key`. +Check whether the node has a descendant node by `path` or `key`. + +### `hasNode` + +`hasNode(path: List|Array) => Boolean` +`hasNode(key: String) => Boolean` + +Check whether a node exists in the tree by `path` or `key`. diff --git a/docs/reference/slate/range.md b/docs/reference/slate/range.md index 9176d65d9..0909ae7ad 100644 --- a/docs/reference/slate/range.md +++ b/docs/reference/slate/range.md @@ -15,8 +15,10 @@ Often times, you don't need to specifically know which point is the "anchor" and ```js Range({ anchorKey: String, + anchorPath: List, anchorOffset: Number, focusKey: String, + focusPath: List, focusOffset: Number, isFocused: Boolean, isBackward: Boolean, @@ -29,6 +31,12 @@ Range({ The key of the text node at the range's anchor point. +### `anchorPath` + +`List` + +The path to the text node at the range's anchor point. + ### `anchorOffset` `Number` @@ -41,6 +49,12 @@ The number of characters from the start of the text node at the range's anchor p The key of the text node at the range's focus point. +### `focusPath` + +`List` + +The path to the text node at the range's focus point. + ### `focusOffset` `Number` @@ -95,10 +109,14 @@ The opposite of `isBackward`, for convenience. ### `startKey` +### `startPath` + ### `startOffset` ### `endKey` +### `endPath` + ### `endOffset` A few convenience properties for accessing the first and last point of the range. When the range is forward, `start` refers to the `anchor` point and `end` refers to the `focus` point. And when it's backward they are reversed. diff --git a/examples/search-highlighting/index.js b/examples/search-highlighting/index.js index 5a03f68b5..1ff9d64ed 100644 --- a/examples/search-highlighting/index.js +++ b/examples/search-highlighting/index.js @@ -4,7 +4,7 @@ import { Value } from 'slate' import React from 'react' import initialValue from './value.json' import styled from 'react-emotion' -import { Toolbar } from '../components' +import { Icon, Toolbar } from '../components' /** * Some styled components for the search box. @@ -16,7 +16,7 @@ const SearchWrapper = styled('div')` position: relative; ` -const SearchIcon = styled('icon')` +const SearchIcon = styled(Icon)` position: absolute; top: 0.5em; left: 0.5em; @@ -130,7 +130,7 @@ class SearchHighlighting extends React.Component { focusKey: key, focusOffset: offset, marks: [{ type: 'highlight' }], - atomic: true, + isAtomic: true, }) } diff --git a/package.json b/package.json index cb6295319..67f29c954 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "copy-webpack-plugin": "^4.4.1", "cross-env": "^5.1.3", "css-loader": "^0.28.9", + "emojis": "^1.0.10", "emotion": "^9.2.4", "eslint": "^4.19.1", - "emojis": "^1.0.10", "eslint-config-prettier": "^2.9.0", "eslint-plugin-import": "^2.8.0", "eslint-plugin-prettier": "^2.5.0", @@ -71,6 +71,7 @@ "source-map-support": "^0.4.0", "style-loader": "^0.20.2", "to-camel-case": "^1.0.0", + "to-snake-case": "^1.0.0", "uglifyjs-webpack-plugin": "^1.1.8", "webpack": "^3.11.0", "webpack-dev-server": "^2.11.1" diff --git a/packages/slate-base64-serializer/Changelog.md b/packages/slate-base64-serializer/Changelog.md index d1d8d9e52..a97268434 100644 --- a/packages/slate-base64-serializer/Changelog.md +++ b/packages/slate-base64-serializer/Changelog.md @@ -8,7 +8,7 @@ This document maintains a list of changes to the `slate-base64-serializer` packa ###### BREAKING -* **Updated to work with `slate@0.29.0`.** This is required because `slate-base64-serializer` needs access to the new `Value` model. +**Updated to work with `slate@0.29.0`.** This is required because `slate-base64-serializer` needs access to the new `Value` model. --- diff --git a/packages/slate-dev-benchmark/test/index.js b/packages/slate-dev-benchmark/test/index.js index 26edceef7..08c77a1f1 100644 --- a/packages/slate-dev-benchmark/test/index.js +++ b/packages/slate-dev-benchmark/test/index.js @@ -7,6 +7,6 @@ */ describe('slate-dev-benchmark', () => { - require('./tries/') - require('./time/') + // require('./tries/') + // require('./time/') }) diff --git a/packages/slate-dev-environment/Changelog.md b/packages/slate-dev-environment/Changelog.md deleted file mode 100644 index 615921fe1..000000000 --- a/packages/slate-dev-environment/Changelog.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -This document maintains a list of changes to the `slate-dev-environment` package with each new version. Until `1.0.0` is released, breaking changes will be added as minor version bumps, and smaller changes won't be accounted for since the library is moving quickly. - ---- - -### `0.1.0` — April 5, 2018 - -:tada: diff --git a/packages/slate-dev-logger/Changelog.md b/packages/slate-dev-logger/Changelog.md deleted file mode 100644 index 96db2a9ed..000000000 --- a/packages/slate-dev-logger/Changelog.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -This document maintains a list of changes to the `slate-dev-logger` package with each new version. Until `1.0.0` is released, breaking changes will be added as minor version bumps, and smaller changes won't be accounted for since the library is moving quickly. - ---- - -### `0.1.0` — September 17, 2017 - -:tada: diff --git a/packages/slate-html-serializer/Changelog.md b/packages/slate-html-serializer/Changelog.md index 3b90fad56..b7affbebe 100644 --- a/packages/slate-html-serializer/Changelog.md +++ b/packages/slate-html-serializer/Changelog.md @@ -8,7 +8,7 @@ This document maintains a list of changes to the `slate-html-serializer` package ###### BREAKING -* **Returning `null` now ignores the node.** Previously it would be treated the same as `undefined`, which will move on to the next rule in the stack. Now it ignores the node and moves onto the next node instead. +**Returning `null` now ignores the node.** Previously it would be treated the same as `undefined`, which will move on to the next rule in the stack. Now it ignores the node and moves onto the next node instead. --- @@ -16,9 +16,9 @@ This document maintains a list of changes to the `slate-html-serializer` package ###### BREAKING -* **The `kind` property of Slate objects has been renamed to `object`.** This is to reduce the confusion over the difference between "kind" and "type" which are practically synonyms. The "object" name was chosen to match the Stripe API, since it seems like a sensible choice and reads much more nicely when looking through JSON. +**The `kind` property of Slate objects has been renamed to `object`.** This is to reduce the confusion over the difference between "kind" and "type" which are practically synonyms. The "object" name was chosen to match the Stripe API, since it seems like a sensible choice and reads much more nicely when looking through JSON. -* **Serializing with `parse5` is no longer possible.** The codebase previously made concessions to allow this, but it was never a good idea because `parse5` does not match the `DOMParser` behavior exactly. Instead, you should use `jsdom` to get a matching behavior, otherwise your serialization rules need to account for two slightly different syntax trees. +**Serializing with `parse5` is no longer possible.** The codebase previously made concessions to allow this, but it was never a good idea because `parse5` does not match the `DOMParser` behavior exactly. Instead, you should use `jsdom` to get a matching behavior, otherwise your serialization rules need to account for two slightly different syntax trees. --- @@ -26,7 +26,7 @@ This document maintains a list of changes to the `slate-html-serializer` package ###### BREAKING -* **Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. +**Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. --- @@ -34,7 +34,7 @@ This document maintains a list of changes to the `slate-html-serializer` package ###### BREAKING -* **Updated to work with `slate@0.29.0`.** This is required because `slate-html-serializer` needs access to the new `Value` model. +**Updated to work with `slate@0.29.0`.** This is required because `slate-html-serializer` needs access to the new `Value` model. --- @@ -42,7 +42,7 @@ This document maintains a list of changes to the `slate-html-serializer` package ###### BREAKING -* **Updated work with `slate@0.27.0`.** The new version of Slate renames the old `Range` model to `Leaf`, and the old `Selection` model to `Range`. +**Updated work with `slate@0.27.0`.** The new version of Slate renames the old `Range` model to `Leaf`, and the old `Selection` model to `Range`. --- diff --git a/packages/slate-html-serializer/test/index.js b/packages/slate-html-serializer/test/index.js index 4dbfffaf6..e7eef3c3f 100644 --- a/packages/slate-html-serializer/test/index.js +++ b/packages/slate-html-serializer/test/index.js @@ -6,7 +6,7 @@ import Html from '..' import assert from 'assert' import fs from 'fs' import { JSDOM } from 'jsdom' // eslint-disable-line import/no-extraneous-dependencies -import { Value, resetKeyGenerator } from 'slate' +import { Value, KeyUtils } from 'slate' import { basename, extname, resolve } from 'path' /** @@ -14,7 +14,7 @@ import { basename, extname, resolve } from 'path' */ beforeEach(() => { - resetKeyGenerator() + KeyUtils.resetGenerator() }) /** diff --git a/packages/slate-hyperscript/Changelog.md b/packages/slate-hyperscript/Changelog.md index 149992f84..7942209e6 100644 --- a/packages/slate-hyperscript/Changelog.md +++ b/packages/slate-hyperscript/Changelog.md @@ -4,11 +4,19 @@ This document maintains a list of changes to the `slate-hyperscript` package wit --- +### `0.6.0` — July 27, 2018 + +###### NEW + +**Updated to work with the `slate@0.35.0` with paths.** The original logic for selections and decorations didn't account for paths properly. This isn't a breaking change, but to use this library with the latest Slate you'll need to upgrade. + +--- + ### `0.5.0` — January 4, 2018 ###### BREAKING -* **The `kind` property of Slate objects has been renamed to `object`.** This is to reduce the confusion over the difference between "kind" and "type" which are practically synonyms. The "object" name was chosen to match the Stripe API, since it seems like a sensible choice and reads much more nicely when looking through JSON. +**The `kind` property of Slate objects has been renamed to `object`.** This is to reduce the confusion over the difference between "kind" and "type" which are practically synonyms. The "object" name was chosen to match the Stripe API, since it seems like a sensible choice and reads much more nicely when looking through JSON. --- @@ -16,7 +24,7 @@ This document maintains a list of changes to the `slate-hyperscript` package wit ###### BREAKING -* **Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. +**Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. --- @@ -24,11 +32,11 @@ This document maintains a list of changes to the `slate-hyperscript` package wit ###### BREAKING -* **Updated to work with `slate@0.29.0`.** This is required because `slate-hyperscript` needs access to the new `Value` model. +**Updated to work with `slate@0.29.0`.** This is required because `slate-hyperscript` needs access to the new `Value` model. ###### DEPRECATED -* **The `` tag has been renamed to ``.** This is to stay in line with the newest version of Slate where the `State` object was renamed to `Value`. +**The `` tag has been renamed to ``.** This is to stay in line with the newest version of Slate where the `State` object was renamed to `Value`. --- @@ -36,7 +44,7 @@ This document maintains a list of changes to the `slate-hyperscript` package wit ###### BREAKING -* **Updated work with `slate@0.27.0`.** The new version of Slate renames the old `Range` model to `Leaf`, and the old `Selection` model to `Range`. +**Updated work with `slate@0.27.0`.** The new version of Slate renames the old `Range` model to `Leaf`, and the old `Selection` model to `Range`. --- diff --git a/packages/slate-hyperscript/package.json b/packages/slate-hyperscript/package.json index 1de27088e..cf3540771 100644 --- a/packages/slate-hyperscript/package.json +++ b/packages/slate-hyperscript/package.json @@ -18,7 +18,7 @@ "slate-dev-logger": "^0.1.39" }, "peerDependencies": { - "slate": ">=0.32.0" + "slate": ">=0.35.0" }, "devDependencies": { "mocha": "^2.5.3", diff --git a/packages/slate-hyperscript/src/index.js b/packages/slate-hyperscript/src/index.js index 3423efa9a..3e1f23d9a 100644 --- a/packages/slate-hyperscript/src/index.js +++ b/packages/slate-hyperscript/src/index.js @@ -201,18 +201,17 @@ const CREATORS = { ) } - if (!isEmpty(props)) { - selection = selection.merge(props).normalize(document) - } - let value = Value.fromJSON({ data, document, selection }, { normalize }) - // apply any decorations built + if (!isEmpty(props)) { + selection = selection.merge(props).normalize(value.document) + value = value.set('selection', selection) + } + if (decorations.length > 0) { - value = value - .change() - .setValue({ decorations: decorations.map(d => d.normalize(document)) }) - .value + decorations = decorations.map(d => d.normalize(value.document)) + decorations = Range.createList(decorations) + value = value.set('decorations', decorations) } return value diff --git a/packages/slate-hyperscript/test/default/single-block.js b/packages/slate-hyperscript/test/fixtures/block.js similarity index 84% rename from packages/slate-hyperscript/test/default/single-block.js rename to packages/slate-hyperscript/test/fixtures/block.js index 83c6e047f..96de2c3c5 100644 --- a/packages/slate-hyperscript/test/default/single-block.js +++ b/packages/slate-hyperscript/test/fixtures/block.js @@ -4,7 +4,7 @@ import h from '../..' export const input = ( - Single block + word ) @@ -23,7 +23,7 @@ export const output = { leaves: [ { object: 'leaf', - text: 'Single block', + text: 'word', marks: [], }, ], diff --git a/packages/slate-hyperscript/test/selections/selection-tag-with-matching-key.js b/packages/slate-hyperscript/test/fixtures/cursor-across-block.js similarity index 52% rename from packages/slate-hyperscript/test/selections/selection-tag-with-matching-key.js rename to packages/slate-hyperscript/test/fixtures/cursor-across-block.js index d78a1231a..ba9303467 100644 --- a/packages/slate-hyperscript/test/selections/selection-tag-with-matching-key.js +++ b/packages/slate-hyperscript/test/fixtures/cursor-across-block.js @@ -6,42 +6,38 @@ export const input = ( - - This is{' '} - - a paragraph with a cursor position (closed selection). - - + word - ) +export const options = { + preserveSelection: true, + preserveKeys: true, +} + export const output = { object: 'value', document: { object: 'document', + key: '3', data: {}, nodes: [ { object: 'block', + key: '1', type: 'paragraph', isVoid: false, data: {}, nodes: [ { object: 'text', + key: '0', leaves: [ { object: 'leaf', - text: - 'This is a paragraph with a cursor position (closed selection).', + text: 'word', marks: [], }, ], @@ -50,12 +46,17 @@ export const output = { }, ], }, -} - -export const expectSelection = { - isCollapsed: true, - anchorOffset: 30, - focusOffset: 30, - anchorKey: input.texts.get(0).key, - focusKey: input.texts.get(0).key, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 1, + focusKey: '0', + focusPath: [0, 0], + focusOffset: 3, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, } diff --git a/packages/slate-hyperscript/test/fixtures/cursor-across-blocks-and-inlines.js b/packages/slate-hyperscript/test/fixtures/cursor-across-blocks-and-inlines.js new file mode 100644 index 000000000..1969ca986 --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/cursor-across-blocks-and-inlines.js @@ -0,0 +1,151 @@ +/** @jsx h */ + +import h from '../..' + +export const input = ( + + + + + one + + + + + two + + + + +) + +export const options = { + preserveSelection: true, + preserveKeys: true, +} + +export const output = { + object: 'value', + document: { + object: 'document', + key: '10', + data: {}, + nodes: [ + { + object: 'block', + key: '3', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '11', + leaves: [ + { + object: 'leaf', + text: '', + marks: [], + }, + ], + }, + { + object: 'inline', + key: '1', + type: 'link', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '0', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + { + object: 'text', + key: '12', + leaves: [ + { + object: 'leaf', + text: '', + marks: [], + }, + ], + }, + ], + }, + { + object: 'block', + key: '7', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '13', + leaves: [ + { + object: 'leaf', + text: '', + marks: [], + }, + ], + }, + { + object: 'inline', + key: '5', + type: 'link', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '4', + leaves: [ + { + object: 'leaf', + text: 'two', + marks: [], + }, + ], + }, + ], + }, + { + object: 'text', + key: '14', + leaves: [ + { + object: 'leaf', + text: '', + marks: [], + }, + ], + }, + ], + }, + ], + }, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 1, 0], + anchorOffset: 2, + focusKey: '4', + focusPath: [1, 1, 0], + focusOffset: 1, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, +} diff --git a/packages/slate-hyperscript/test/selections/range-across-blocks.js b/packages/slate-hyperscript/test/fixtures/cursor-across-blocks-end.js similarity index 65% rename from packages/slate-hyperscript/test/selections/range-across-blocks.js rename to packages/slate-hyperscript/test/fixtures/cursor-across-blocks-end.js index 7e93c327b..70ae8d949 100644 --- a/packages/slate-hyperscript/test/selections/range-across-blocks.js +++ b/packages/slate-hyperscript/test/fixtures/cursor-across-blocks-end.js @@ -6,33 +6,41 @@ export const input = ( - This is one block. + one - This is block two. + two ) +export const options = { + preserveSelection: true, + preserveKeys: true, +} + export const output = { object: 'value', document: { object: 'document', + key: '6', data: {}, nodes: [ { object: 'block', + key: '1', type: 'paragraph', isVoid: false, data: {}, nodes: [ { object: 'text', + key: '0', leaves: [ { object: 'leaf', - text: 'This is one block.', + text: 'one', marks: [], }, ], @@ -41,16 +49,18 @@ export const output = { }, { object: 'block', + key: '3', type: 'paragraph', isVoid: false, data: {}, nodes: [ { object: 'text', + key: '2', leaves: [ { object: 'leaf', - text: 'This is block two.', + text: 'two', marks: [], }, ], @@ -59,12 +69,17 @@ export const output = { }, ], }, -} - -export const expectSelection = { - isCollapsed: false, - anchorOffset: 12, - focusOffset: 13, - anchorKey: input.texts.get(0).key, - focusKey: input.texts.get(1).key, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 3, + focusKey: '2', + focusPath: [1, 0], + focusOffset: 3, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, } diff --git a/packages/slate-hyperscript/test/fixtures/cursor-across-blocks-middle.js b/packages/slate-hyperscript/test/fixtures/cursor-across-blocks-middle.js new file mode 100644 index 000000000..a9b948895 --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/cursor-across-blocks-middle.js @@ -0,0 +1,85 @@ +/** @jsx h */ + +import h from '../..' + +export const input = ( + + + + one + + + two + + + +) + +export const options = { + preserveSelection: true, + preserveKeys: true, +} + +export const output = { + object: 'value', + document: { + object: 'document', + key: '6', + data: {}, + nodes: [ + { + object: 'block', + key: '1', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '0', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + { + object: 'block', + key: '3', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '2', + leaves: [ + { + object: 'leaf', + text: 'two', + marks: [], + }, + ], + }, + ], + }, + ], + }, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 2, + focusKey: '2', + focusPath: [1, 0], + focusOffset: 1, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, +} diff --git a/packages/slate-hyperscript/test/fixtures/cursor-across-blocks-start.js b/packages/slate-hyperscript/test/fixtures/cursor-across-blocks-start.js new file mode 100644 index 000000000..62111625f --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/cursor-across-blocks-start.js @@ -0,0 +1,85 @@ +/** @jsx h */ + +import h from '../..' + +export const input = ( + + + + one + + + two + + + +) + +export const options = { + preserveSelection: true, + preserveKeys: true, +} + +export const output = { + object: 'value', + document: { + object: 'document', + key: '6', + data: {}, + nodes: [ + { + object: 'block', + key: '1', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '0', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + { + object: 'block', + key: '3', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '2', + leaves: [ + { + object: 'leaf', + text: 'two', + marks: [], + }, + ], + }, + ], + }, + ], + }, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 0, + focusKey: '2', + focusPath: [1, 0], + focusOffset: 0, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, +} diff --git a/packages/slate-hyperscript/test/fixtures/cursor-across-multiple-blocks-end.js b/packages/slate-hyperscript/test/fixtures/cursor-across-multiple-blocks-end.js new file mode 100644 index 000000000..6e4476240 --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/cursor-across-multiple-blocks-end.js @@ -0,0 +1,106 @@ +/** @jsx h */ + +import h from '../..' + +export const input = ( + + + + one + + two + + three + + + +) + +export const options = { + preserveSelection: true, + preserveKeys: true, +} + +export const output = { + object: 'value', + document: { + object: 'document', + key: '9', + data: {}, + nodes: [ + { + object: 'block', + key: '1', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '0', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + { + object: 'block', + key: '3', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '2', + leaves: [ + { + object: 'leaf', + text: 'two', + marks: [], + }, + ], + }, + ], + }, + { + object: 'block', + key: '5', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '4', + leaves: [ + { + object: 'leaf', + text: 'three', + marks: [], + }, + ], + }, + ], + }, + ], + }, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 3, + focusKey: '4', + focusPath: [2, 0], + focusOffset: 5, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, +} diff --git a/packages/slate-hyperscript/test/fixtures/cursor-across-multiple-blocks-middle.js b/packages/slate-hyperscript/test/fixtures/cursor-across-multiple-blocks-middle.js new file mode 100644 index 000000000..d0e47b4c4 --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/cursor-across-multiple-blocks-middle.js @@ -0,0 +1,106 @@ +/** @jsx h */ + +import h from '../..' + +export const input = ( + + + + one + + two + + three + + + +) + +export const options = { + preserveSelection: true, + preserveKeys: true, +} + +export const output = { + object: 'value', + document: { + object: 'document', + key: '9', + data: {}, + nodes: [ + { + object: 'block', + key: '1', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '0', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + { + object: 'block', + key: '3', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '2', + leaves: [ + { + object: 'leaf', + text: 'two', + marks: [], + }, + ], + }, + ], + }, + { + object: 'block', + key: '5', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '4', + leaves: [ + { + object: 'leaf', + text: 'three', + marks: [], + }, + ], + }, + ], + }, + ], + }, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 2, + focusKey: '4', + focusPath: [2, 0], + focusOffset: 1, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, +} diff --git a/packages/slate-hyperscript/test/fixtures/cursor-across-multiple-blocks-start.js b/packages/slate-hyperscript/test/fixtures/cursor-across-multiple-blocks-start.js new file mode 100644 index 000000000..5f1a28c2c --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/cursor-across-multiple-blocks-start.js @@ -0,0 +1,106 @@ +/** @jsx h */ + +import h from '../..' + +export const input = ( + + + + one + + two + + three + + + +) + +export const options = { + preserveSelection: true, + preserveKeys: true, +} + +export const output = { + object: 'value', + document: { + object: 'document', + key: '9', + data: {}, + nodes: [ + { + object: 'block', + key: '1', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '0', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + { + object: 'block', + key: '3', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '2', + leaves: [ + { + object: 'leaf', + text: 'two', + marks: [], + }, + ], + }, + ], + }, + { + object: 'block', + key: '5', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '4', + leaves: [ + { + object: 'leaf', + text: 'three', + marks: [], + }, + ], + }, + ], + }, + ], + }, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 0, + focusKey: '4', + focusPath: [2, 0], + focusOffset: 0, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, +} diff --git a/packages/slate-hyperscript/test/selections/cursor-in-block.js b/packages/slate-hyperscript/test/fixtures/cursor-block-end.js similarity index 58% rename from packages/slate-hyperscript/test/selections/cursor-in-block.js rename to packages/slate-hyperscript/test/fixtures/cursor-block-end.js index 78ac675b5..2da3e985b 100644 --- a/packages/slate-hyperscript/test/selections/cursor-in-block.js +++ b/packages/slate-hyperscript/test/fixtures/cursor-block-end.js @@ -6,31 +6,38 @@ export const input = ( - This is a paragraph with a cursor position (closed selection). + one ) +export const options = { + preserveSelection: true, + preserveKeys: true, +} + export const output = { object: 'value', document: { object: 'document', + key: '3', data: {}, nodes: [ { object: 'block', + key: '1', type: 'paragraph', isVoid: false, data: {}, nodes: [ { object: 'text', + key: '0', leaves: [ { object: 'leaf', - text: - 'This is a paragraph with a cursor position (closed selection).', + text: 'one', marks: [], }, ], @@ -39,12 +46,17 @@ export const output = { }, ], }, -} - -export const expectSelection = { - isCollapsed: true, - anchorOffset: 43, - focusOffset: 43, - anchorKey: input.texts.get(0).key, - focusKey: input.texts.get(0).key, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 3, + focusKey: '0', + focusPath: [0, 0], + focusOffset: 3, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, } diff --git a/packages/slate-hyperscript/test/fixtures/cursor-block-middle.js b/packages/slate-hyperscript/test/fixtures/cursor-block-middle.js new file mode 100644 index 000000000..9a20ba412 --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/cursor-block-middle.js @@ -0,0 +1,62 @@ +/** @jsx h */ + +import h from '../..' + +export const input = ( + + + + one + + + +) + +export const options = { + preserveSelection: true, + preserveKeys: true, +} + +export const output = { + object: 'value', + document: { + object: 'document', + key: '3', + data: {}, + nodes: [ + { + object: 'block', + key: '1', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '0', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + ], + }, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 1, + focusKey: '0', + focusPath: [0, 0], + focusOffset: 1, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, +} diff --git a/packages/slate-hyperscript/test/fixtures/cursor-block-start.js b/packages/slate-hyperscript/test/fixtures/cursor-block-start.js new file mode 100644 index 000000000..1eaf3500b --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/cursor-block-start.js @@ -0,0 +1,62 @@ +/** @jsx h */ + +import h from '../..' + +export const input = ( + + + + one + + + +) + +export const options = { + preserveSelection: true, + preserveKeys: true, +} + +export const output = { + object: 'value', + document: { + object: 'document', + key: '3', + data: {}, + nodes: [ + { + object: 'block', + key: '1', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '0', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + ], + }, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 0, + focusKey: '0', + focusPath: [0, 0], + focusOffset: 0, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, +} diff --git a/packages/slate-hyperscript/test/fixtures/cursor-custom-block-middle.js b/packages/slate-hyperscript/test/fixtures/cursor-custom-block-middle.js new file mode 100644 index 000000000..7da451288 --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/cursor-custom-block-middle.js @@ -0,0 +1,68 @@ +/** @jsx h */ + +import { createHyperscript } from '../..' + +const h = createHyperscript({ + blocks: { + paragraph: 'paragraph', + }, +}) + +export const input = ( + + + + one + + + +) + +export const options = { + preserveSelection: true, + preserveKeys: true, +} + +export const output = { + object: 'value', + document: { + object: 'document', + key: '3', + data: {}, + nodes: [ + { + object: 'block', + key: '1', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '0', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + ], + }, + ], + }, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 1, + focusKey: '0', + focusPath: [0, 0], + focusOffset: 1, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, +} diff --git a/packages/slate-hyperscript/test/fixtures/cursor-inline.js b/packages/slate-hyperscript/test/fixtures/cursor-inline.js new file mode 100644 index 000000000..f41057ec6 --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/cursor-inline.js @@ -0,0 +1,97 @@ +/** @jsx h */ + +import h from '../..' + +export const input = ( + + + + one + + two + + three + + + +) + +export const options = { + preserveSelection: true, + preserveKeys: true, +} + +export const output = { + object: 'value', + document: { + object: 'document', + data: {}, + key: '6', + nodes: [ + { + object: 'block', + key: '4', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '2', + leaves: [ + { + object: 'leaf', + text: 'one', + marks: [], + }, + ], + }, + { + object: 'inline', + key: '1', + type: 'link', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: '0', + leaves: [ + { + object: 'leaf', + text: 'two', + marks: [], + }, + ], + }, + ], + }, + { + object: 'text', + key: '3', + leaves: [ + { + object: 'leaf', + text: 'three', + marks: [], + }, + ], + }, + ], + }, + ], + }, + selection: { + object: 'range', + anchorKey: '0', + anchorPath: [0, 1, 0], + anchorOffset: 1, + focusKey: '0', + focusPath: [0, 1, 0], + focusOffset: 1, + isBackward: false, + isFocused: true, + isAtomic: false, + marks: null, + }, +} diff --git a/packages/slate-hyperscript/test/custom/custom-tags.js b/packages/slate-hyperscript/test/fixtures/custom-tags.js similarity index 100% rename from packages/slate-hyperscript/test/custom/custom-tags.js rename to packages/slate-hyperscript/test/fixtures/custom-tags.js diff --git a/packages/slate-hyperscript/test/decorations/single.js b/packages/slate-hyperscript/test/fixtures/decoration.js similarity index 55% rename from packages/slate-hyperscript/test/decorations/single.js rename to packages/slate-hyperscript/test/fixtures/decoration.js index 0d15402e2..043bc7a1b 100644 --- a/packages/slate-hyperscript/test/decorations/single.js +++ b/packages/slate-hyperscript/test/fixtures/decoration.js @@ -15,32 +15,38 @@ export const input = ( - This is a paragraph with a cursor position{' '} - (closed selection). + onetwothree ) +export const options = { + preserveDecorations: true, + preserveKeys: true, +} + export const output = { object: 'value', document: { object: 'document', + key: '3', data: {}, nodes: [ { object: 'block', + key: '1', type: 'paragraph', isVoid: false, data: {}, nodes: [ { object: 'text', + key: '0', leaves: [ { object: 'leaf', - text: - 'This is a paragraph with a cursor position (closed selection).', + text: 'onetwothree', marks: [], }, ], @@ -49,20 +55,25 @@ export const output = { }, ], }, + decorations: [ + { + object: 'range', + anchorKey: '0', + anchorPath: [0, 0], + anchorOffset: 3, + focusKey: '0', + focusPath: [0, 0], + focusOffset: 6, + isBackward: false, + isFocused: false, + isAtomic: false, + marks: [ + { + object: 'mark', + type: 'highlight', + data: {}, + }, + ], + }, + ], } - -export const expectDecorations = [ - { - anchorOffset: 10, - focusOffset: 24, - anchorKey: input.texts.get(0).key, - focusKey: input.texts.get(0).key, - marks: [ - { - object: 'mark', - type: 'highlight', - data: {}, - }, - ], - }, -] diff --git a/packages/slate-hyperscript/test/default/empty-marked-text.js b/packages/slate-hyperscript/test/fixtures/mark-empty.js similarity index 100% rename from packages/slate-hyperscript/test/default/empty-marked-text.js rename to packages/slate-hyperscript/test/fixtures/mark-empty.js diff --git a/packages/slate-hyperscript/test/selections/range-in-block.js b/packages/slate-hyperscript/test/fixtures/normalize-default.js similarity index 61% rename from packages/slate-hyperscript/test/selections/range-in-block.js rename to packages/slate-hyperscript/test/fixtures/normalize-default.js index 231921a05..aa66c3079 100644 --- a/packages/slate-hyperscript/test/selections/range-in-block.js +++ b/packages/slate-hyperscript/test/fixtures/normalize-default.js @@ -5,9 +5,8 @@ import h from '../..' export const input = ( - - This is a paragraph with an open selection. - + word + invalid ) @@ -29,7 +28,7 @@ export const output = { leaves: [ { object: 'leaf', - text: 'This is a paragraph with an open selection.', + text: 'word', marks: [], }, ], @@ -39,11 +38,3 @@ export const output = { ], }, } - -export const expectSelection = { - isCollapsed: false, - anchorOffset: 10, - focusOffset: 19, - anchorKey: input.texts.get(0).key, - focusKey: input.texts.get(0).key, -} diff --git a/packages/slate-hyperscript/test/fixtures/normalize-disabled.js b/packages/slate-hyperscript/test/fixtures/normalize-disabled.js new file mode 100644 index 000000000..ad0d51154 --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/normalize-disabled.js @@ -0,0 +1,50 @@ +/** @jsx h */ + +import h from '../..' + +export const input = ( + + + word + invalid + + +) + +export const output = { + object: 'value', + document: { + object: 'document', + data: {}, + nodes: [ + { + object: 'block', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + leaves: [ + { + object: 'leaf', + text: 'word', + marks: [], + }, + ], + }, + ], + }, + { + object: 'text', + leaves: [ + { + object: 'leaf', + text: 'invalid', + marks: [], + }, + ], + }, + ], + }, +} diff --git a/packages/slate-hyperscript/test/fixtures/selection.js b/packages/slate-hyperscript/test/fixtures/selection.js new file mode 100644 index 000000000..f8cd7bf4f --- /dev/null +++ b/packages/slate-hyperscript/test/fixtures/selection.js @@ -0,0 +1,63 @@ +/** @jsx h */ + +import h from '../..' + +export const input = ( + + + + onetwothree + + + + +) + +export const options = { + preserveSelection: true, + preserveKeys: true, +} + +export const output = { + object: 'value', + document: { + object: 'document', + key: '2', + data: {}, + nodes: [ + { + object: 'block', + key: '0', + type: 'paragraph', + isVoid: false, + data: {}, + nodes: [ + { + object: 'text', + key: 'a', + leaves: [ + { + object: 'leaf', + text: 'onetwothree', + marks: [], + }, + ], + }, + ], + }, + ], + }, + selection: { + object: 'range', + anchorKey: 'a', + anchorPath: [0, 0], + anchorOffset: 1, + focusKey: 'a', + focusPath: [0, 0], + focusOffset: 2, + isBackward: false, + isFocused: false, + isAtomic: false, + marks: null, + }, +} diff --git a/packages/slate-hyperscript/test/default/perserve-keys-in-right-text.js b/packages/slate-hyperscript/test/fixtures/text-perserve-keys.js similarity index 86% rename from packages/slate-hyperscript/test/default/perserve-keys-in-right-text.js rename to packages/slate-hyperscript/test/fixtures/text-perserve-keys.js index d475d4a00..358a8e0d9 100644 --- a/packages/slate-hyperscript/test/default/perserve-keys-in-right-text.js +++ b/packages/slate-hyperscript/test/fixtures/text-perserve-keys.js @@ -1,7 +1,5 @@ /** @jsx h */ -import assert from 'assert' - import h from '../..' export const input = ( @@ -13,24 +11,25 @@ export const input = ( ) -export function test() { - const block = input.nodes.first() - assert.notEqual(block.nodes.first().key, 'a') - assert.equal(block.nodes.last().key, 'a') +export const options = { + preserveKeys: true, } export const output = { object: 'document', + key: '6', data: {}, nodes: [ { object: 'block', + key: '4', type: 'paragraph', isVoid: false, data: {}, nodes: [ { object: 'text', + key: '2', leaves: [ { object: 'leaf', @@ -41,12 +40,14 @@ export const output = { }, { object: 'inline', + key: '1', type: 'link', data: {}, isVoid: false, nodes: [ { object: 'text', + key: '0', leaves: [ { object: 'leaf', @@ -59,6 +60,7 @@ export const output = { }, { object: 'text', + key: 'a', leaves: [ { object: 'leaf', diff --git a/packages/slate-hyperscript/test/default/with-marks-and-inlines.js b/packages/slate-hyperscript/test/fixtures/with-marks-and-inlines.js similarity index 100% rename from packages/slate-hyperscript/test/default/with-marks-and-inlines.js rename to packages/slate-hyperscript/test/fixtures/with-marks-and-inlines.js diff --git a/packages/slate-hyperscript/test/index.js b/packages/slate-hyperscript/test/index.js index 3f019758c..e9335ce24 100644 --- a/packages/slate-hyperscript/test/index.js +++ b/packages/slate-hyperscript/test/index.js @@ -1,91 +1,37 @@ -/** - * Dependencies. - */ - import assert from 'assert' import fs from 'fs' -import { Value } from 'slate' +import { Value, KeyUtils } from 'slate' import { basename, extname, resolve } from 'path' -/** - * Tests. - */ +beforeEach(KeyUtils.resetGenerator) describe('slate-hyperscript', () => { - describe('default settings', () => { - const dir = resolve(__dirname, './default') - const tests = fs - .readdirSync(dir) + const dir = resolve(__dirname, './fixtures') + const tests = fs + .readdirSync(dir) + .filter(t => t[0] != '.') + .map(t => basename(t, extname(t))) + + for (const test of tests) { + it(test, async () => { + const module = require(resolve(dir, test)) + const { input, output, options } = module + const actual = input.toJSON(options) + const expected = Value.isValue(output) ? output.toJSON() : output + assert.deepEqual(actual, expected) + }) + } + + describe.skip('decorations', () => { + const decDir = resolve(__dirname, './decorations') + const decTests = fs + .readdirSync(decDir) .filter(t => t[0] != '.') .map(t => basename(t, extname(t))) - for (const test of tests) { + for (const test of decTests) { it(test, async () => { - const module = require(resolve(dir, test)) - const { input, output } = module - - const actual = input.toJSON() - const expected = Value.isValue(output) ? output.toJSON() : output - assert.deepEqual(actual, expected) - if (module.test) module.test() - }) - } - }) - - describe('custom tags', () => { - const dir = resolve(__dirname, './custom') - const tests = fs - .readdirSync(dir) - .filter(t => t[0] != '.') - .map(t => basename(t, extname(t))) - - for (const test of tests) { - it(test, async () => { - const module = require(resolve(dir, test)) - const { input, output } = module - - const actual = input.toJSON() - const expected = Value.isValue(output) ? output.toJSON() : output - assert.deepEqual(actual, expected) - }) - } - }) - - describe('selections', () => { - const dir = resolve(__dirname, './selections') - const tests = fs - .readdirSync(dir) - .filter(t => t[0] != '.') - .map(t => basename(t, extname(t))) - - for (const test of tests) { - it(test, async () => { - const module = require(resolve(dir, test)) - const { input, output, expectSelection } = module - - // ensure deserialization was okay - const actual = input.toJSON() - const expected = Value.isValue(output) ? output.toJSON() : output - assert.deepEqual(actual, expected) - - // ensure expected properties of selection match - Object.keys(expectSelection).forEach(prop => { - assert.equal(input.selection[prop], expectSelection[prop]) - }) - }) - } - }) - - describe('decorations', () => { - const dir = resolve(__dirname, './decorations') - const tests = fs - .readdirSync(dir) - .filter(t => t[0] != '.') - .map(t => basename(t, extname(t))) - - for (const test of tests) { - it(test, async () => { - const module = require(resolve(dir, test)) + const module = require(resolve(decDir, test)) const { input, output, expectDecorations } = module // ensure deserialization was okay @@ -107,23 +53,4 @@ describe('slate-hyperscript', () => { }) } }) - - describe('normalize', () => { - const dir = resolve(__dirname, './normalize') - const tests = fs - .readdirSync(dir) - .filter(t => t[0] != '.') - .map(t => basename(t, extname(t))) - - for (const test of tests) { - it(test, async () => { - const module = require(resolve(dir, test)) - const { input, output } = module - - const actual = Value.isValue(input) ? input.toJSON() : input - const expected = Value.isValue(output) ? output.toJSON() : output - assert.deepEqual(actual, expected) - }) - } - }) }) diff --git a/packages/slate-hyperscript/test/normalize/on-by-default.js b/packages/slate-hyperscript/test/normalize/on-by-default.js deleted file mode 100644 index 97dce111e..000000000 --- a/packages/slate-hyperscript/test/normalize/on-by-default.js +++ /dev/null @@ -1,24 +0,0 @@ -/** @jsx h */ - -import h from '../..' -import { Value, Document, Block, Text } from 'slate' - -export const input = ( - - - Valid block - Invalid text - - -) - -export const output = Value.create({ - document: Document.create({ - nodes: [ - Block.create({ - type: 'paragraph', - nodes: [Text.create('Valid block')], - }), - ], - }), -}) diff --git a/packages/slate-hyperscript/test/normalize/optionally-disabled.js b/packages/slate-hyperscript/test/normalize/optionally-disabled.js deleted file mode 100644 index d6fdbc34b..000000000 --- a/packages/slate-hyperscript/test/normalize/optionally-disabled.js +++ /dev/null @@ -1,28 +0,0 @@ -/** @jsx h */ - -import h from '../..' -import { Value, Document, Block, Text } from 'slate' - -export const input = ( - - - Valid block - Invalid text - - -) - -export const output = Value.fromJSON( - { - document: Document.create({ - nodes: [ - Block.create({ - type: 'paragraph', - nodes: [Text.create('Valid block')], - }), - Text.create('Invalid text'), - ], - }), - }, - { normalize: false } -) diff --git a/packages/slate-hyperscript/test/selections/cursor-in-mark.js b/packages/slate-hyperscript/test/selections/cursor-in-mark.js deleted file mode 100644 index f675fb0ff..000000000 --- a/packages/slate-hyperscript/test/selections/cursor-in-mark.js +++ /dev/null @@ -1,97 +0,0 @@ -/** @jsx h */ - -import { createHyperscript } from '../..' - -const h = createHyperscript({ - blocks: { - paragraph: 'paragraph', - }, - marks: { - b: 'bold', - }, -}) - -export const input = ( - - - First paragraph - - This is a paragraph with a cursor{' '} - - position - {' '} - within a mark. - - - -) - -export const output = { - object: 'value', - document: { - object: 'document', - data: {}, - nodes: [ - { - object: 'block', - type: 'paragraph', - isVoid: false, - data: {}, - nodes: [ - { - object: 'text', - leaves: [ - { - object: 'leaf', - text: 'First paragraph', - marks: [], - }, - ], - }, - ], - }, - { - object: 'block', - type: 'paragraph', - isVoid: false, - data: {}, - nodes: [ - { - object: 'text', - leaves: [ - { - object: 'leaf', - text: 'This is a paragraph with a cursor ', - marks: [], - }, - { - object: 'leaf', - text: 'position', - marks: [ - { - object: 'mark', - type: 'bold', - data: {}, - }, - ], - }, - { - object: 'leaf', - text: ' within a mark.', - marks: [], - }, - ], - }, - ], - }, - ], - }, -} - -export const expectSelection = { - isCollapsed: true, - anchorOffset: 40, - focusOffset: 40, - anchorKey: input.texts.get(0).key, - focusKey: input.texts.get(0).key, -} diff --git a/packages/slate-plain-serializer/Changelog.md b/packages/slate-plain-serializer/Changelog.md index 4fba19a68..58161f9be 100644 --- a/packages/slate-plain-serializer/Changelog.md +++ b/packages/slate-plain-serializer/Changelog.md @@ -8,7 +8,7 @@ This document maintains a list of changes to the `slate-plain-serializer` packag ###### BREAKING -* **The `kind` property of Slate objects has been renamed to `object`.** This is to reduce the confusion over the difference between "kind" and "type" which are practically synonyms. The "object" name was chosen to match the Stripe API, since it seems like a sensible choice and reads much more nicely when looking through JSON. +**The `kind` property of Slate objects has been renamed to `object`.** This is to reduce the confusion over the difference between "kind" and "type" which are practically synonyms. The "object" name was chosen to match the Stripe API, since it seems like a sensible choice and reads much more nicely when looking through JSON. --- @@ -16,7 +16,7 @@ This document maintains a list of changes to the `slate-plain-serializer` packag ###### BREAKING -* **Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. +**Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. --- @@ -24,7 +24,7 @@ This document maintains a list of changes to the `slate-plain-serializer` packag ###### BREAKING -* **Updated to work with `slate@0.29.0`.** This is required because `slate-plain-serializer` needs access to the new `Value` model. +**Updated to work with `slate@0.29.0`.** This is required because `slate-plain-serializer` needs access to the new `Value` model. --- @@ -32,7 +32,7 @@ This document maintains a list of changes to the `slate-plain-serializer` packag ###### BREAKING -* **Updated work with `slate@0.27.0`.** The new version of Slate renames the old `Range` model to `Leaf`, and the old `Selection` model to `Range`. +**Updated work with `slate@0.27.0`.** The new version of Slate renames the old `Range` model to `Leaf`, and the old `Selection` model to `Range`. --- diff --git a/packages/slate-plain-serializer/test/index.js b/packages/slate-plain-serializer/test/index.js index 35bec354b..ace3249eb 100644 --- a/packages/slate-plain-serializer/test/index.js +++ b/packages/slate-plain-serializer/test/index.js @@ -1,24 +1,10 @@ -/** - * Dependencies. - */ - import Plain from '..' import assert from 'assert' import fs from 'fs' -import { Value, resetKeyGenerator } from 'slate' +import { Value, KeyUtils } from 'slate' import { basename, extname, resolve } from 'path' -/** - * Reset Slate's internal key generator state before each text. - */ - -beforeEach(() => { - resetKeyGenerator() -}) - -/** - * Tests. - */ +beforeEach(KeyUtils.resetGenerator) describe('slate-plain-serializer', () => { describe('deserialize()', () => { diff --git a/packages/slate-prop-types/Changelog.md b/packages/slate-prop-types/Changelog.md index 5025c3e7b..bcb10fdc9 100644 --- a/packages/slate-prop-types/Changelog.md +++ b/packages/slate-prop-types/Changelog.md @@ -8,7 +8,7 @@ This document maintains a list of changes to the `slate-prop-types` package with ###### BREAKING -* **Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. +**Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. --- @@ -16,11 +16,11 @@ This document maintains a list of changes to the `slate-prop-types` package with ###### BREAKING -* **Updated to work with `slate@0.29.0`.** This is required because `slate-prop-types` needs access to the new `Value` model. +**Updated to work with `slate@0.29.0`.** This is required because `slate-prop-types` needs access to the new `Value` model. ###### DEPRECATED -* **The `state` prop type has been renamed to `value`.** This is to stay in line with `slate-react@0.29.0` where the `State` object was renamed. +**The `state` prop type has been renamed to `value`.** This is to stay in line with `slate-react@0.29.0` where the `State` object was renamed. --- @@ -28,7 +28,7 @@ This document maintains a list of changes to the `slate-prop-types` package with ###### BREAKING -* **Updated work with `slate@0.27.0`.** The new version of Slate renames the old `Range` model to `Leaf`, and the old `Selection` model to `Range`. +**Updated work with `slate@0.27.0`.** The new version of Slate renames the old `Range` model to `Leaf`, and the old `Selection` model to `Range`. --- diff --git a/packages/slate-react/Changelog.md b/packages/slate-react/Changelog.md index 0095a8407..1d2267992 100644 --- a/packages/slate-react/Changelog.md +++ b/packages/slate-react/Changelog.md @@ -4,11 +4,19 @@ This document maintains a list of changes to the `slate-react` package with each --- +### `0.14.0` — July 27, 2018 + +###### NEW + +**Updated to work with the `slate@0.35.0` with paths.** It now uses the `PathUtils` export in the latest `slate` internally to work with paths. This isn't a breaking change, but to use this library with the latest Slate you'll need to upgrade. + +--- + ### `0.13.0` — July 3, 2018 ###### BREAKING -* **The `isSelected` prop of nodes has changed.** Previously it was only `true` when the node was selected and the editor was focused. Now it is true even when the editor is not focused, and a new `isFocused` property has been added for the old behavior. +**The `isSelected` prop of nodes has changed.** Previously it was only `true` when the node was selected and the editor was focused. Now it is true even when the editor is not focused, and a new `isFocused` property has been added for the old behavior. --- @@ -16,7 +24,7 @@ This document maintains a list of changes to the `slate-react` package with each ###### BREAKING -* **Update to use `slate@0.33.0`.** This is to match the changes to void node behavior where their content is no longer restricted. +**Update to use `slate@0.33.0`.** This is to match the changes to void node behavior where their content is no longer restricted. --- @@ -24,7 +32,7 @@ This document maintains a list of changes to the `slate-react` package with each ###### BREAKING -* **The `kind` property of Slate objects has been renamed to `object`.** This is to reduce the confusion over the difference between "kind" and "type" which are practically synonyms. The "object" name was chosen to match the Stripe API, since it seems like a sensible choice and reads much more nicely when looking through JSON. +**The `kind` property of Slate objects has been renamed to `object`.** This is to reduce the confusion over the difference between "kind" and "type" which are practically synonyms. The "object" name was chosen to match the Stripe API, since it seems like a sensible choice and reads much more nicely when looking through JSON. --- @@ -32,7 +40,7 @@ This document maintains a list of changes to the `slate-react` package with each ###### BREAKING -* **Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. +**Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. --- @@ -40,23 +48,23 @@ This document maintains a list of changes to the `slate-react` package with each ###### BREAKING -* **Updated to use `slate@0.29.0`.** This is to gain access to the new `Value` model introduced in the newest version of Slate. +**Updated to use `slate@0.29.0`.** This is to gain access to the new `Value` model introduced in the newest version of Slate. -* **Custom components no longer receive `props.state` or `props.schema`.** These are now exposed directly on the `props.editor` instance itself as `editor.value` and `editor.schema`. This helps eliminate a common issue where because of `shouldComponentUpdate` returning `false`, the `props.state` value was actually outdated, and transforming from it would cause incorrect behaviors. +**Custom components no longer receive `props.state` or `props.schema`.** These are now exposed directly on the `props.editor` instance itself as `editor.value` and `editor.schema`. This helps eliminate a common issue where because of `shouldComponentUpdate` returning `false`, the `props.state` value was actually outdated, and transforming from it would cause incorrect behaviors. -* **The `plugin.renderEditor` function's signature has changed.** Previously it received `(props, state, editor)` but it now receives just `(props, editor)`. If you need access to the editor's current value, use the new `editor.value` property. This is simply to clean up the API, since the value is already accessible on `editor`. +**The `plugin.renderEditor` function's signature has changed.** Previously it received `(props, state, editor)` but it now receives just `(props, editor)`. If you need access to the editor's current value, use the new `editor.value` property. This is simply to clean up the API, since the value is already accessible on `editor`. ###### DEPRECATED -* **The "state" has been renamed to "value" everywhere.** All of the current references are maintained as deprecations, so you should be able to upgrade and see warnings logged instead of being greeted with a broken editor. This is to reduce the confusion between React's "state" and Slate's editor value, and in an effort to further mimic the native DOM APIs. +**The "state" has been renamed to "value" everywhere.** All of the current references are maintained as deprecations, so you should be able to upgrade and see warnings logged instead of being greeted with a broken editor. This is to reduce the confusion between React's "state" and Slate's editor value, and in an effort to further mimic the native DOM APIs. -* **The editor `getSchema()`, `getStack()` and `getState()` methods are deprecated.** These have been replaced by property getters on the editor instance itself—`editor.schema`, `editor.stack` and `editor.value`, respectively. This is to reduce confusion with React's own `setState`, and to make accessing these commonly used properties more convenient. +**The editor `getSchema()`, `getStack()` and `getState()` methods are deprecated.** These have been replaced by property getters on the editor instance itself—`editor.schema`, `editor.stack` and `editor.value`, respectively. This is to reduce confusion with React's own `setState`, and to make accessing these commonly used properties more convenient. ###### NEW -* **Added a new `editor.value` getter property.** This now mimics the DOM for things like `input.value` and `textarea.value`, and is the new way to access the editor's current value. +**Added a new `editor.value` getter property.** This now mimics the DOM for things like `input.value` and `textarea.value`, and is the new way to access the editor's current value. -* **Added new `editor.schema` and `editor.stack` getters.** Similarly to the new `value` getter, these two new getters give you access to the editor's current schema and stack. +**Added new `editor.schema` and `editor.stack` getters.** Similarly to the new `value` getter, these two new getters give you access to the editor's current schema and stack. --- @@ -64,23 +72,23 @@ This document maintains a list of changes to the `slate-react` package with each ###### BREAKING -* **The `Schema` objects in Slate have changed!** Previously, they used to be where you could define normalization rules, define rendering rules, and define decoration rules. This was overloaded, and made other improvements hard. Now, rendering and decorating is done via the newly added plugin functions (`renderNode`, `renderMark`, `decorateNode`). And validation is done either via the lower-level `validateNode` plugin function, or via the new `schema` objects. +**The `Schema` objects in Slate have changed!** Previously, they used to be where you could define normalization rules, define rendering rules, and define decoration rules. This was overloaded, and made other improvements hard. Now, rendering and decorating is done via the newly added plugin functions (`renderNode`, `renderMark`, `decorateNode`). And validation is done either via the lower-level `validateNode` plugin function, or via the new `schema` objects. -* **The `plugin.onBeforeChange` function was removed.** Previously there was both an `onBeforeChange` handler and an `onChange` handler. Now there is just an `onChange` handler, and the core plugin adds it's own logic before others. +**The `plugin.onBeforeChange` function was removed.** Previously there was both an `onBeforeChange` handler and an `onChange` handler. Now there is just an `onChange` handler, and the core plugin adds it's own logic before others. -* **The `plugin.render` function was renamed to `plugin.renderEditor`.** It performs the same function, but has been renamed to disambiguate between all of the other new rendering functions available to plugins. +**The `plugin.render` function was renamed to `plugin.renderEditor`.** It performs the same function, but has been renamed to disambiguate between all of the other new rendering functions available to plugins. ###### NEW -* **`State` objects now have an embedded `state.schema` property.** This new schema property is used to automatically normalize the state as it changes, according to the editor's current schema. This makes normalization much easier. +**`State` objects now have an embedded `state.schema` property.** This new schema property is used to automatically normalize the state as it changes, according to the editor's current schema. This makes normalization much easier. -* **A new `renderNode` plugin function was added.** This is the new way to render nodes, instead of using the schema. Any plugin can define a `renderNode(props)` function which is passed the props to render the custom node component with. This is similar to `react-router`'s `render={...}` prop if you are familiar with that. +**A new `renderNode` plugin function was added.** This is the new way to render nodes, instead of using the schema. Any plugin can define a `renderNode(props)` function which is passed the props to render the custom node component with. This is similar to `react-router`'s `render={...}` prop if you are familiar with that. -* **A new `renderPlaceholder` plugin function was added.** This is similar to the `renderNode` helper, except for rendering placeholders. +**A new `renderPlaceholder` plugin function was added.** This is similar to the `renderNode` helper, except for rendering placeholders. -* **A new `decorateNode` plugin function was added.** This is similar to the old `rule.decorate` function from schemas. Any plugin can define a `decorateNode(node)` function and that can return extra decoration ranges of marks to apply to the document. +**A new `decorateNode` plugin function was added.** This is similar to the old `rule.decorate` function from schemas. Any plugin can define a `decorateNode(node)` function and that can return extra decoration ranges of marks to apply to the document. -* **A new `validateNode` plugin function was added.** This is the new way to do specific, custom validations. (There's also the new schema, which is the easier way to do most common validations.) Any plugin can define a `validateNode(node)` function that will be called to ensure nodes are valid. If they are valid, the function should return nothing. Otherwise, it should return a change function that normalizes the node to make it valid again. +**A new `validateNode` plugin function was added.** This is the new way to do specific, custom validations. (There's also the new schema, which is the easier way to do most common validations.) Any plugin can define a `validateNode(node)` function that will be called to ensure nodes are valid. If they are valid, the function should return nothing. Otherwise, it should return a change function that normalizes the node to make it valid again. --- @@ -88,7 +96,7 @@ This document maintains a list of changes to the `slate-react` package with each ###### BREAKING -* **The `` component no longer exists!** Previously there was a `Placeholder` component exported from `slate-react`, but it had lots of problems and a confusing API. Instead, placeholder logic can now be defined via the `schema` by providing a `placeholder` component to render what a node is matched. +**The `` component no longer exists!** Previously there was a `Placeholder` component exported from `slate-react`, but it had lots of problems and a confusing API. Instead, placeholder logic can now be defined via the `schema` by providing a `placeholder` component to render what a node is matched. --- @@ -96,13 +104,13 @@ This document maintains a list of changes to the `slate-react` package with each ###### BREAKING -* **The `data` argument to event handlers has been removed.** Previously event handlers had a signature of `(event, data, change, editor)`, but now they have a signature of just `(event, change, editor)`. This leads to simpler internal Slate logic, and less complex relationship dependencies between plugins. All of the information inside the old `data` argument can be accessed via the similar properties on the `event` argument, or via the `getEventRange`, `getEventTransfer` and `setEventTransfer` helpers. +**The `data` argument to event handlers has been removed.** Previously event handlers had a signature of `(event, data, change, editor)`, but now they have a signature of just `(event, change, editor)`. This leads to simpler internal Slate logic, and less complex relationship dependencies between plugins. All of the information inside the old `data` argument can be accessed via the similar properties on the `event` argument, or via the `getEventRange`, `getEventTransfer` and `setEventTransfer` helpers. ###### NEW -* **Added a new `setEventTransfer` helper.** This is useful if you're working with `onDrop` or `onPaste` event and you want to set custom data in the event, to retrieve later or for others to consume. It takes a data `type` and a `value` to set the type do. +**Added a new `setEventTransfer` helper.** This is useful if you're working with `onDrop` or `onPaste` event and you want to set custom data in the event, to retrieve later or for others to consume. It takes a data `type` and a `value` to set the type do. -* **Event handlers now have access to new events.** The `onClick`, `onCompositionEnd`, `onCompositionStart`, `onDragEnd`, `onDragEnter`, `onDragExit`, `onDragLeave`, `onDragOver`, `onDragStart`, and `onInput` events are all now newly exposed. Your plugin logic can use them to solve some more advanced use cases, and even override the internal Slate logic when necessary. 99% of use cases won't require them still, but they can be useful to have when needed. +**Event handlers now have access to new events.** The `onClick`, `onCompositionEnd`, `onCompositionStart`, `onDragEnd`, `onDragEnter`, `onDragExit`, `onDragLeave`, `onDragOver`, `onDragStart`, and `onInput` events are all now newly exposed. Your plugin logic can use them to solve some more advanced use cases, and even override the internal Slate logic when necessary. 99% of use cases won't require them still, but they can be useful to have when needed. --- @@ -110,13 +118,13 @@ This document maintains a list of changes to the `slate-react` package with each ###### DEPRECATED -* **The `data` objects in event handlers have been deprecated.** There were a few different issues with these "helpers": `data.key` didn't account for international keyboards, many properties awkwardly duplicated information that was available on `event.*`, but not completely, and many properties were confusing as to when they applied. If you were using these, you'll now need to use the native `event.*` properties instead. There's also a helpful [`is-hotkey`](https://github.com/ianstormtaylor/is-hotkey) package for more complex hotkey matching. +**The `data` objects in event handlers have been deprecated.** There were a few different issues with these "helpers": `data.key` didn't account for international keyboards, many properties awkwardly duplicated information that was available on `event.*`, but not completely, and many properties were confusing as to when they applied. If you were using these, you'll now need to use the native `event.*` properties instead. There's also a helpful [`is-hotkey`](https://github.com/ianstormtaylor/is-hotkey) package for more complex hotkey matching. ###### NEW -* **Added a new `getEventRange` helper.** This gets the affected `Range` of Slate document given a DOM `event`. This is useful in the `onDrop` or `onPaste` handlers to retrieve the range in the document where the drop or paste will occur. +**Added a new `getEventRange` helper.** This gets the affected `Range` of Slate document given a DOM `event`. This is useful in the `onDrop` or `onPaste` handlers to retrieve the range in the document where the drop or paste will occur. -* **Added a new `getEventTransfer` helper.** This gets any Slate-related data from an `event`. It is modelled after the DOM's [`DataTransfer`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer) API, and is useful for retrieve the data being dropped or pasted in `onDrop` or `onPaste` events. +**Added a new `getEventTransfer` helper.** This gets any Slate-related data from an `event`. It is modelled after the DOM's [`DataTransfer`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer) API, and is useful for retrieve the data being dropped or pasted in `onDrop` or `onPaste` events. --- @@ -124,15 +132,15 @@ This document maintains a list of changes to the `slate-react` package with each ###### BREAKING -* **Updated work with `slate@0.27.0`.** The new version of Slate renames the old `Range` model to `Leaf`, and the old `Selection` model to `Range`. +**Updated work with `slate@0.27.0`.** The new version of Slate renames the old `Range` model to `Leaf`, and the old `Selection` model to `Range`. ###### NEW -* **Added a new `findDOMRange` helper.** Give a Slate `Range` object, it will return a DOM `Range` object with the correct start and end points, making it easier to work with lower-level DOM selections. +**Added a new `findDOMRange` helper.** Give a Slate `Range` object, it will return a DOM `Range` object with the correct start and end points, making it easier to work with lower-level DOM selections. -* **Added a new `findRange` helper.** Given either a DOM `Selection` or DOM `Range` object and a Slate `State`, it will return a Slate `Range` representing the same part of the document, making it easier to work with DOM selection changes. +**Added a new `findRange` helper.** Given either a DOM `Selection` or DOM `Range` object and a Slate `State`, it will return a Slate `Range` representing the same part of the document, making it easier to work with DOM selection changes. -* **Added a new `findNode` helper.** Given a DOM `Element`, it will find the closest Slate `Node` that it represents, making +**Added a new `findNode` helper.** Given a DOM `Element`, it will find the closest Slate `Node` that it represents, making --- @@ -140,7 +148,7 @@ This document maintains a list of changes to the `slate-react` package with each ###### BREAKING -* **The decoration logic has been updated to use `slate@0.26.0`.** This allows for more complex decoration logic, and even decorations based on external information. +**The decoration logic has been updated to use `slate@0.26.0`.** This allows for more complex decoration logic, and even decorations based on external information. --- @@ -148,7 +156,7 @@ This document maintains a list of changes to the `slate-react` package with each ###### BREAKING -* **`onBeforeChange` is now called automatically again in ``.** This was removed before, in attempt to decrease the "magic" that the editor was performing, since it normalizes when new props are passed to it, creating instant changes. But we discovered that it is actually necessary for now, so it has been added again. +**`onBeforeChange` is now called automatically again in ``.** This was removed before, in attempt to decrease the "magic" that the editor was performing, since it normalizes when new props are passed to it, creating instant changes. But we discovered that it is actually necessary for now, so it has been added again. --- diff --git a/packages/slate-react/package.json b/packages/slate-react/package.json index 0f73937e2..b805b6ad7 100644 --- a/packages/slate-react/package.json +++ b/packages/slate-react/package.json @@ -33,7 +33,7 @@ "immutable": ">=3.8.1", "react": ">=0.14.0", "react-dom": ">=0.14.0", - "slate": ">=0.32.0" + "slate": ">=0.35.0" }, "devDependencies": { "immutable": "^3.8.1", diff --git a/packages/slate-react/src/components/text.js b/packages/slate-react/src/components/text.js index a464082f7..4e9637bdb 100644 --- a/packages/slate-react/src/components/text.js +++ b/packages/slate-react/src/components/text.js @@ -3,6 +3,7 @@ import ImmutableTypes from 'react-immutable-proptypes' import React from 'react' import SlateTypes from 'slate-prop-types' import Types from 'prop-types' +import { PathUtils } from 'slate' import Leaf from './leaf' @@ -108,13 +109,23 @@ class Text extends React.Component { const { key } = node const decs = decorations.filter(d => { - const { startKey, endKey } = d - if (startKey == key || endKey == key) return true + const { startKey, endKey, startPath, endPath } = d + + // If either of the decoration's keys match, include it. + if (startKey === key || endKey === key) return true + + // Otherwise, if the decoration is in a single node, it's not ours. if (startKey === endKey) return false - const startsBefore = document.areDescendantsSorted(startKey, key) - if (!startsBefore) return false - const endsAfter = document.areDescendantsSorted(key, endKey) - return endsAfter + + // If the node's path is before the start path, ignore it. + const path = document.assertPathByKey(key) + if (PathUtils.compare(path, startPath) === -1) return false + + // If the node's path is after the end path, ignore it. + if (PathUtils.compare(path, endPath) === 1) return false + + // Otherwise, include it. + return true }) // PERF: Take advantage of cache by avoiding arguments diff --git a/packages/slate-react/test/index.js b/packages/slate-react/test/index.js index 9f5e403d5..1c79e0e04 100644 --- a/packages/slate-react/test/index.js +++ b/packages/slate-react/test/index.js @@ -1,23 +1,9 @@ -/** - * Dependencies. - */ +import { KeyUtils } from 'slate' -import { resetKeyGenerator } from 'slate' - -/** - * Tests. - */ +beforeEach(KeyUtils.resetGenerator) describe('slate-react', () => { require('./plugins') require('./rendering') require('./utils') }) - -/** - * Reset Slate's internal state before each text. - */ - -beforeEach(() => { - resetKeyGenerator() -}) diff --git a/packages/slate-react/test/utils/index.js b/packages/slate-react/test/utils/index.js index a3f92c7d5..c7f454f2a 100644 --- a/packages/slate-react/test/utils/index.js +++ b/packages/slate-react/test/utils/index.js @@ -1,21 +1,7 @@ -/** - * Dependencies. - */ +import { KeyUtils } from 'slate' -import { resetKeyGenerator } from 'slate' - -/** - * Tests. - */ +beforeEach(KeyUtils.resetGenerator) describe('utils', () => { require('./get-children-decorations') }) - -/** - * Reset Slate's internal state before each text. - */ - -beforeEach(() => { - resetKeyGenerator() -}) diff --git a/packages/slate-simulator/Changelog.md b/packages/slate-simulator/Changelog.md index 160672072..11e4e4014 100644 --- a/packages/slate-simulator/Changelog.md +++ b/packages/slate-simulator/Changelog.md @@ -8,7 +8,7 @@ This document maintains a list of changes to the `slate-simulator` package with ###### BREAKING -* **Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. +**Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. --- @@ -16,9 +16,9 @@ This document maintains a list of changes to the `slate-simulator` package with ###### DEPRECATED -* **The `props.state` prop has been renamed to `props.value`.** This is to stay in line with `slate-react@0.9.0` where the same change was made to the ``. +**The `props.state` prop has been renamed to `props.value`.** This is to stay in line with `slate-react@0.9.0` where the same change was made to the ``. -* **The `simulator.state` property is now `simulator.value`** This is to stay in line with `slate@0.29.0` where the same change as made to the `Change` objects. +**The `simulator.state` property is now `simulator.value`** This is to stay in line with `slate@0.29.0` where the same change as made to the `Change` objects. --- @@ -26,7 +26,7 @@ This document maintains a list of changes to the `slate-simulator` package with ###### BREAKING -* **Updated to work with `slate@0.28.0`.** Along with the new Schema, the `Stack` which is used internally by the simulator has changed slightly. +**Updated to work with `slate@0.28.0`.** Along with the new Schema, the `Stack` which is used internally by the simulator has changed slightly. --- diff --git a/packages/slate/Changelog.md b/packages/slate/Changelog.md index cec8497e8..d4f8be1a9 100644 --- a/packages/slate/Changelog.md +++ b/packages/slate/Changelog.md @@ -4,19 +4,47 @@ This document maintains a list of changes to the `slate` package with each new v --- +### `0.35.0` — July 27, 2018 + +###### BREAKING + +**Internal-yet-public `Node` methods have been changed.** There were a handful of internal methods that shouldn't be used in 99% of Slate implementations that updated or removed. This was done in the process of streamlining many of the `Node` methods to make them more consistent and easier to use. For a list of those affected: + +* `Node.assertPath` was changed. It was previously confusingly named because the equivalent `Node.getPath` did something completely different. You should now use `Node.assertNode(path)` if you need this behavior. +* `Node.removeDescendant` was removed. There's no reason you should have been using this, since it was an undocumented and unused method that was left over from a previous version. +* `Node.updateNode`, `Node.insertNode`, `Node.removeNode`, `Node.splitNode` and `Node.mergeNode` mutating methods were changed. All of your changes should be done with operations, so you likely weren't using these internal methods. They have been changed internally to use paths. + +###### DEPRECATED + +**The `setKeyGenerator` and `resetKeyGenerator` helpers are deprecated.** These were previously used to change the default key generation logic. Now you can use the equivalent `KeyUtils.setGenerator` and `KeyUtils.resetGenerator` helpers instead. This follows the new pattern of grouping related utilities into single namespaces, as is the case with the new `PathUtils` and `TextUtils`. + +**Internal-yet-public `Node` methods have been deprecated.** There were a handful of internal methods that shouldn't be used in 99% of Slate implementations that were deprecated. For a list of those affected: + +* `Node.getKeys` and `Node.getKeysAsArray` were deprecated. If you really need to check the presence of a key, use the new `Node.getKeysToPathsObject` instead. +* `Node.areDescendantsSorted` and `Node.isInRange` were deprecated. These were used to check whether a node was in a range, but this can be done more performantly and more easily with paths now. +* `Node.getNodeAtPath` and `Node.getDescendantAtPath` were deprecated. These were probably not in use by anyone, but if you were using them you can use the existing `Node.getNode` and `Node.getDescendant` methods instead which now take either paths or keys. + +###### NEW + +**`Range` objects now keep track of paths, in addition to keys.** Previously ranges only stored their points as keys. Now both paths and keys are used, which allows you to choose which one is the most convenient or most performant for your use case. They are kept in sync my Slate under the covers. + +**A new set of `*ByPath` change methods have been added.** All of the changes you could previously do with a `*ByKey` change are now also supported with a `*ByPath` change of the same name. The path-based changes are often more performant than the key-based ones. + +--- + ### `0.34.0` — June 14, 2018 ###### BREAKING -* **Text nodes now represent their content as "leaves".** Previously their immutable representation used individual `Character` instance for each character. Now they have changed to group characters into `Leaf` models, which more closely resembles how they are used, and results in a _lot_ fewer immutable object instances floating around. _For most people this shouldn't cause any issues, since this is a low-level aspect of Slate._ +**Text nodes now represent their content as "leaves".** Previously their immutable representation used individual `Character` instance for each character. Now they have changed to group characters into `Leaf` models, which more closely resembles how they are used, and results in a _lot_ fewer immutable object instances floating around. _For most people this shouldn't cause any issues, since this is a low-level aspect of Slate._ ###### DEPRECATED -* **The `Character` model is deprecated.** Although the character concept is still in the repository for now, it is deprecated and will be removed in a future release. Everything it solves can be solved with leaves instead. +**The `Character` model is deprecated.** Although the character concept is still in the repository for now, it is deprecated and will be removed in a future release. Everything it solves can be solved with leaves instead. ###### NEW -* **Decorations can now be "atomic".** If you set a decoration as atomic, it will be removed when changed, preventing it from entering a "partial" state, which can be useful for some use cases. +**Decorations can now be "atomic".** If you set a decoration as atomic, it will be removed when changed, preventing it from entering a "partial" state, which can be useful for some use cases. --- @@ -24,13 +52,13 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **Void nodes no longer prescribe their text content.** Previously void nodes would automatically normalize their text content to be a single text node containing `' '` an empty string of content. This restriction was removed, so that void nodes can have arbitrary content. You can use this to store information in void nodes in a way that is more consistent with non-void nodes. +**Void nodes no longer prescribe their text content.** Previously void nodes would automatically normalize their text content to be a single text node containing `' '` an empty string of content. This restriction was removed, so that void nodes can have arbitrary content. You can use this to store information in void nodes in a way that is more consistent with non-void nodes. ###### DEPRECATED -* **The `setBlock` method has been renamed to `setBlocks`.** This is to make it more clear that it operates on any of the current blocks in the selection, not just a single blocks. +**The `setBlock` method has been renamed to `setBlocks`.** This is to make it more clear that it operates on any of the current blocks in the selection, not just a single blocks. -* **The `setInline` method has been renamed to `setInlines`.** For the same reason as `setBlocks`, to be clear and stay consistent. +**The `setInline` method has been renamed to `setInlines`.** For the same reason as `setBlocks`, to be clear and stay consistent. --- @@ -38,9 +66,9 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **The `kind` property of Slate objects has been renamed to `object`.** This is to reduce the confusion over the difference between "kind" and "type" which are practically synonyms. The "object" name was chosen to match the Stripe API, since it seems like a sensible choice and reads much more nicely when looking through JSON. +**The `kind` property of Slate objects has been renamed to `object`.** This is to reduce the confusion over the difference between "kind" and "type" which are practically synonyms. The "object" name was chosen to match the Stripe API, since it seems like a sensible choice and reads much more nicely when looking through JSON. -* **All normalization reasons containing `kind` have been renamed too.** Previously there were normalization reason strings like `child_kind_invalid`. These types of strings have been renamed to `child_object_invalid` to stay consistent. +**All normalization reasons containing `kind` have been renamed too.** Previously there were normalization reason strings like `child_kind_invalid`. These types of strings have been renamed to `child_object_invalid` to stay consistent. --- @@ -48,13 +76,13 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **Operation objects in Slate are now immutable records.** Previously they were native, mutable Javascript objects. Now, there's a new immutable `Operation` model in Slate, ensuring that all of the data inside `Value` objects are immutable. And it allows for easy serialization of operations using `operation.toJSON()` for when sending them between editors. This should not affect most users, unless you are relying on changing the values of the low-level Slate operations (simply reading them is fine). +**Operation objects in Slate are now immutable records.** Previously they were native, mutable Javascript objects. Now, there's a new immutable `Operation` model in Slate, ensuring that all of the data inside `Value` objects are immutable. And it allows for easy serialization of operations using `operation.toJSON()` for when sending them between editors. This should not affect most users, unless you are relying on changing the values of the low-level Slate operations (simply reading them is fine). -* **Operation lists in Slate are now immutable lists.** Previously they were native, mutable Javascript arrays. Now, to keep consistent with other immutable uses, they are immutable lists. This should not affect most users. +**Operation lists in Slate are now immutable lists.** Previously they were native, mutable Javascript arrays. Now, to keep consistent with other immutable uses, they are immutable lists. This should not affect most users. ###### NEW -* **Added a new `Operation` model.** This model is used to store operations for the history stack, and (de)serializes them in a consistent way for collaborative editing use cases. +**Added a new `Operation` model.** This model is used to store operations for the history stack, and (de)serializes them in a consistent way for collaborative editing use cases. --- @@ -62,7 +90,7 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. +**Remove all previously deprecated code paths.** This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's _highly_ recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version. --- @@ -70,15 +98,15 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **The `set_state` operation has been renamed `set_value`**. This shouldn't affect almost anyone, but in the event that you were relying on the low-level operation types you'll need to update this. +**The `set_state` operation has been renamed `set_value`**. This shouldn't affect almost anyone, but in the event that you were relying on the low-level operation types you'll need to update this. ###### DEPRECATED -* **The "state" has been renamed to "value" everywhere.** All of the current references are maintained as deprecations, so you should be able to upgrade and see warnings logged instead of being greeted with a broken editor. This is to reduce the confusion between React's "state" and Slate's editor value, and in an effort to further mimic the native DOM APIs. +**The "state" has been renamed to "value" everywhere.** All of the current references are maintained as deprecations, so you should be able to upgrade and see warnings logged instead of being greeted with a broken editor. This is to reduce the confusion between React's "state" and Slate's editor value, and in an effort to further mimic the native DOM APIs. ###### NEW -* **Added the new `Value` model to replace `State`.** The new model is exactly the same, but with a new name. There is also a shimmed `State` model exported that warns when used, to ease migration. +**Added the new `Value` model to replace `State`.** The new model is exactly the same, but with a new name. There is also a shimmed `State` model exported that warns when used, to ease migration. --- @@ -86,13 +114,13 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **The `Schema` objects in Slate have changed!** Previously, they used to be where you could define normalization rules, define rendering rules, and define decoration rules. This was overloaded, and made other improvements hard. Now, rendering and decorating is done via the newly added plugin functions (`renderNode`, `renderMark`, `decorateNode`). And validation is done either via the lower-level `validateNode` plugin function, or via the new `schema` objects. +**The `Schema` objects in Slate have changed!** Previously, they used to be where you could define normalization rules, define rendering rules, and define decoration rules. This was overloaded, and made other improvements hard. Now, rendering and decorating is done via the newly added plugin functions (`renderNode`, `renderMark`, `decorateNode`). And validation is done either via the lower-level `validateNode` plugin function, or via the new `schema` objects. -* **The `normalize*` change methods no longer take a `schema` argument.** Previously you had to maintain a reference to your schema, and pass it into the normalize methods when you called them. Since `State` objects now have an embedded `state.schema` property, this is no longer needed. +**The `normalize*` change methods no longer take a `schema` argument.** Previously you had to maintain a reference to your schema, and pass it into the normalize methods when you called them. Since `State` objects now have an embedded `state.schema` property, this is no longer needed. ###### NEW -* **`State` objects now have an embedded `state.schema` property.** This new schema property is used to automatically normalize the state as it changes, according to the editor's current schema. This makes normalization much easier. +**`State` objects now have an embedded `state.schema` property.** This new schema property is used to automatically normalize the state as it changes, according to the editor's current schema. This makes normalization much easier. --- @@ -100,15 +128,15 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **The `Range` model is now called `Leaf`.** This is to disambiguate with the concept of "ranges" that is used throughout the codebase to be synonymous to selections. For example in methods like `getBlocksAtRange(selection)`. +**The `Range` model is now called `Leaf`.** This is to disambiguate with the concept of "ranges" that is used throughout the codebase to be synonymous to selections. For example in methods like `getBlocksAtRange(selection)`. -* **The `text.ranges` property in the JSON representation is now `text.leaves`.** When passing in JSON with `text.ranges` you'll now receive a deprecation warning in the console in development. +**The `text.ranges` property in the JSON representation is now `text.leaves`.** When passing in JSON with `text.ranges` you'll now receive a deprecation warning in the console in development. ###### DEPRECATED -* **The `Selection` model is now called `Range`.** This is to make it more clear what a "selection" really is, to make many of the other methods that act on "ranges" make sense, and to more closely parallel the native DOM API for selections and ranges. A mock `Selection` object is still exported with deprecated `static` methods, to make the transition to the new API easier. +**The `Selection` model is now called `Range`.** This is to make it more clear what a "selection" really is, to make many of the other methods that act on "ranges" make sense, and to more closely parallel the native DOM API for selections and ranges. A mock `Selection` object is still exported with deprecated `static` methods, to make the transition to the new API easier. -* **The `Text.getRanges()` method is now `Text.getLeaves()`.** It will still work, and it will return a list of leaves, but you will see a deprecation warning in the console in development. +**The `Text.getRanges()` method is now `Text.getLeaves()`.** It will still work, and it will return a list of leaves, but you will see a deprecation warning in the console in development. --- @@ -116,19 +144,19 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **The `decorate` function of schema rules has changed.** Previously, in `decorate` you would receive a text node and the matched node, and you'd need to manually add any marks you wanted to the text node's characters. Now, "decorations" have changed to just be `Selection` objects with marks in the `selection.marks` property. Instead of applying the marks yourself, you simply return selection ranges with the marks to be applied, and Slate will apply them internally. This makes it possible to write much more complex decoration behaviors. Check out the revamped [`code-highlighting`](https://github.com/ianstormtaylor/slate/blob/master/examples/code-highlighting/index.js) example and the new [`search-highlighting`](https://github.com/ianstormtaylor/slate/blob/master/examples/search-highlighting/index.js) example to see this in action. +**The `decorate` function of schema rules has changed.** Previously, in `decorate` you would receive a text node and the matched node, and you'd need to manually add any marks you wanted to the text node's characters. Now, "decorations" have changed to just be `Selection` objects with marks in the `selection.marks` property. Instead of applying the marks yourself, you simply return selection ranges with the marks to be applied, and Slate will apply them internally. This makes it possible to write much more complex decoration behaviors. Check out the revamped [`code-highlighting`](https://github.com/ianstormtaylor/slate/blob/master/examples/code-highlighting/index.js) example and the new [`search-highlighting`](https://github.com/ianstormtaylor/slate/blob/master/examples/search-highlighting/index.js) example to see this in action. -* **The `set_data` operation type has been replaced by `set_state`.** With the new `state.decorations` property, it doesn't make sense to have a new operation type for every property of `State` objects. Instead, the new `set_state` operation more closely mimics the existing `set_mark` and `set_node` operations. +**The `set_data` operation type has been replaced by `set_state`.** With the new `state.decorations` property, it doesn't make sense to have a new operation type for every property of `State` objects. Instead, the new `set_state` operation more closely mimics the existing `set_mark` and `set_node` operations. ###### DEPRECATED -* **The `setData` change method has been replaced by `setState`.** Previously you would call `change.setData(data)`. But as new `State` properties are introduced it doesn't make sense to need to add new change methods each time. Instead, the new `change.setState(properties)` more closesely mimics the existing `setMarkByKey` and `setNodeByKey`. To achieve the old behavior, you can do `change.setState({ data })`. +**The `setData` change method has been replaced by `setState`.** Previously you would call `change.setData(data)`. But as new `State` properties are introduced it doesn't make sense to need to add new change methods each time. Instead, the new `change.setState(properties)` more closesely mimics the existing `setMarkByKey` and `setNodeByKey`. To achieve the old behavior, you can do `change.setState({ data })`. -* **The `preserveStateData` option of `state.toJSON` has changed.** The same behavior is now called `preserveData` instead. This makes it consistent with all of the existing options, and the new `preserveDecorations` option as well. +**The `preserveStateData` option of `state.toJSON` has changed.** The same behavior is now called `preserveData` instead. This makes it consistent with all of the existing options, and the new `preserveDecorations` option as well. ###### NEW -* **You can now set decorations based on external information.** Previously, the "decoration" logic in Slate was always based off of the text of a node, and would only re-render when that text changed. Now, there is a new `state.decorations` property that you can set via `change.setState({ decorations })`. You can use this to add presentation-only marks to arbitrary ranges of text in the document. Check out the new [`search-highlighting`](https://github.com/ianstormtaylor/slate/blob/master/examples/search-highlighting/index.js) example to see this in action. +**You can now set decorations based on external information.** Previously, the "decoration" logic in Slate was always based off of the text of a node, and would only re-render when that text changed. Now, there is a new `state.decorations` property that you can set via `change.setState({ decorations })`. You can use this to add presentation-only marks to arbitrary ranges of text in the document. Check out the new [`search-highlighting`](https://github.com/ianstormtaylor/slate/blob/master/examples/search-highlighting/index.js) example to see this in action. --- @@ -136,9 +164,9 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **The `insertBlock` change method no longer replaces empty blocks.** Previously if you used `insertBlock` and the selection was in an empty block, it would replace it. Now you'll need to perform that check yourself and use the new `replaceNodeByKey` method instead. +**The `insertBlock` change method no longer replaces empty blocks.** Previously if you used `insertBlock` and the selection was in an empty block, it would replace it. Now you'll need to perform that check yourself and use the new `replaceNodeByKey` method instead. -* **The `Block.create` and `Inline.create` methods no longer normalize.** Previously if you used one of them to create a block or inline with zero nodes in it, they would automatically add a single empty text node as the only child. This was unexpected in certain situations, and if you were relying on this you'll need to handle it manually instead now. +**The `Block.create` and `Inline.create` methods no longer normalize.** Previously if you used one of them to create a block or inline with zero nodes in it, they would automatically add a single empty text node as the only child. This was unexpected in certain situations, and if you were relying on this you'll need to handle it manually instead now. --- @@ -146,21 +174,21 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **`immutable` is now a _peer_ dependency of Slate.** Previously it was a regular dependency, but this prevented you from bringing your own version, or you'd have duplication. You'll need to ensure you install it! +**`immutable` is now a _peer_ dependency of Slate.** Previously it was a regular dependency, but this prevented you from bringing your own version, or you'd have duplication. You'll need to ensure you install it! -* **The `Html`, `Plain` and `Raw` serializers are broken into new packages.** Previously you'd import them from `slate`. But now you'll import them from `slate-html-serializer` and `slate-plain-serializer`. And the `Raw` serializer that was deprecated is now removed. +**The `Html`, `Plain` and `Raw` serializers are broken into new packages.** Previously you'd import them from `slate`. But now you'll import them from `slate-html-serializer` and `slate-plain-serializer`. And the `Raw` serializer that was deprecated is now removed. -* **The `Editor` and `Placeholder` components are broken into a new React-specific package.** Previously you'd import them from `slate`. But now you `import { Editor } from 'slate-react'` instead. +**The `Editor` and `Placeholder` components are broken into a new React-specific package.** Previously you'd import them from `slate`. But now you `import { Editor } from 'slate-react'` instead. ###### NEW -* **Slate is now a "monorepo".** Instead of a single package, Slate has been divided up into individual packages so that you can only require what you need, cutting down on file size. In the process, some helpful modules that used to be internal-only are now exposed. +**Slate is now a "monorepo".** Instead of a single package, Slate has been divided up into individual packages so that you can only require what you need, cutting down on file size. In the process, some helpful modules that used to be internal-only are now exposed. -* **There's a new `slate-hyperscript` helper.** This was possible thanks to the work on [`slate-sugar`](https://github.com/GitbookIO/slate-sugar), which paved the way. +**There's a new `slate-hyperscript` helper.** This was possible thanks to the work on [`slate-sugar`](https://github.com/GitbookIO/slate-sugar), which paved the way. -* **The `slate-prop-types` package is now exposed.** Previously this was an internal module, but now you can use it for adding prop types to any components or plugins you create. +**The `slate-prop-types` package is now exposed.** Previously this was an internal module, but now you can use it for adding prop types to any components or plugins you create. -* **The `slate-simulator` package is now exposed.** Previously this was an internal testing utility, but now you can use it in your own tests as well. It's currently pretty bare bones, but we can add to it over time. +**The `slate-simulator` package is now exposed.** Previously this was an internal testing utility, but now you can use it in your own tests as well. It's currently pretty bare bones, but we can add to it over time. --- @@ -168,25 +196,25 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **The `isNative` property of `State` has been removed.** Previously this was used for performance reasons to avoid re-rendering, but it is no longer needed. This shouldn't really affect most people because it's rare that you'd be relying on this property to exist. +**The `isNative` property of `State` has been removed.** Previously this was used for performance reasons to avoid re-rendering, but it is no longer needed. This shouldn't really affect most people because it's rare that you'd be relying on this property to exist. ###### DEPRECATED -* **The `Raw` serializer is now deprecated.** The entire "raw" concept is being removed, in favor of allowing all models to be able to serialize and deserialize to JSON themselves. Instead of using the `Raw` serializer, you can now use the `fromJSON` and `toJSON` on the models directly. +**The `Raw` serializer is now deprecated.** The entire "raw" concept is being removed, in favor of allowing all models to be able to serialize and deserialize to JSON themselves. Instead of using the `Raw` serializer, you can now use the `fromJSON` and `toJSON` on the models directly. -* **The `toRaw` options for the `Plain` and `Html` serializers are now called `toJSON`.** This is to stay symmetrical with the removal of the "raw" concept everywhere. +**The `toRaw` options for the `Plain` and `Html` serializers are now called `toJSON`.** This is to stay symmetrical with the removal of the "raw" concept everywhere. -* **The `terse` option for JSON serialization has been deprecated!** This option causes lots of abstraction leakiness because it means there is no one canonical JSON representation of objects. You had to work with either terse or not terse data. +**The `terse` option for JSON serialization has been deprecated!** This option causes lots of abstraction leakiness because it means there is no one canonical JSON representation of objects. You had to work with either terse or not terse data. -* **The `Html` serializer no longer uses the `terse` representation.** This shouldn't actually be an issue for anyone because the main manifestation of this has a deprecation notice with a patch in place for now. +**The `Html` serializer no longer uses the `terse` representation.** This shouldn't actually be an issue for anyone because the main manifestation of this has a deprecation notice with a patch in place for now. -* **The `defaultBlockType` of the `Html` serializer is now called `defaultBlock`.** This is just to make it more clear that it supports not only setting the default `type` but also `data` and `isVoid`. +**The `defaultBlockType` of the `Html` serializer is now called `defaultBlock`.** This is just to make it more clear that it supports not only setting the default `type` but also `data` and `isVoid`. ###### NEW -* **Slate models now have `Model.fromJSON(object)` and `model.toJSON()` methods.** These methods operate with the canonical JSON form (which used to be called "raw"). This way you don't need to `import` a serializer to retrieve JSON, if you have the model you can serialize/deserialize. +**Slate models now have `Model.fromJSON(object)` and `model.toJSON()` methods.** These methods operate with the canonical JSON form (which used to be called "raw"). This way you don't need to `import` a serializer to retrieve JSON, if you have the model you can serialize/deserialize. -* **Models also have `toJS` and `fromJS` aliases.** This is just to match Immutable.js objects, which have both methods. For Slate though, the methods are equivalent. +**Models also have `toJS` and `fromJS` aliases.** This is just to match Immutable.js objects, which have both methods. For Slate though, the methods are equivalent. --- @@ -194,13 +222,13 @@ This document maintains a list of changes to the `slate` package with each new v ###### BREAKING -* **The `Plain` serializer now adds line breaks between blocks.** Previously between blocks the text would be joined without any space whatsoever, but this wasn't really that useful or what you'd expect. +**The `Plain` serializer now adds line breaks between blocks.** Previously between blocks the text would be joined without any space whatsoever, but this wasn't really that useful or what you'd expect. -* **The `toggleMark` transform now checks the intersection of marks.** Previously, toggling would remove the mark from the range if any of the characters in a range didn't have it. However, this wasn't what all other rich-text editors did, so the behavior has changed to mimic the standard behavior. Now, if any characters in the selection have the mark applied, it will first be added when toggling. +**The `toggleMark` transform now checks the intersection of marks.** Previously, toggling would remove the mark from the range if any of the characters in a range didn't have it. However, this wasn't what all other rich-text editors did, so the behavior has changed to mimic the standard behavior. Now, if any characters in the selection have the mark applied, it will first be added when toggling. -* **The `.length` property of nodes has been removed.** This property caused issues with code like in Lodash that checked for "array-likeness" by simply looking for a `.length` property that was a number. +**The `.length` property of nodes has been removed.** This property caused issues with code like in Lodash that checked for "array-likeness" by simply looking for a `.length` property that was a number. -* **`onChange` now receives a `Change` object (previously named `Transform`) instead of a `State`.** This is needed because it enforces that all changes are represented by a single set of operations. Otherwise right now it's possible to do things like `state.transform()....apply({ save: false }).transform()....apply()` and result in losing the operation information in the history. With OT, we need all transforms that may happen to be exposed and emitted by the editor. The new syntax looks like: +**`onChange` now receives a `Change` object (previously named `Transform`) instead of a `State`.** This is needed because it enforces that all changes are represented by a single set of operations. Otherwise right now it's possible to do things like `state.transform()....apply({ save: false }).transform()....apply()` and result in losing the operation information in the history. With OT, we need all transforms that may happen to be exposed and emitted by the editor. The new syntax looks like: ```js onChange(change) { @@ -212,7 +240,7 @@ onChange({ state }) { } ``` -* **Similarly, handlers now receive `e, data, change` instead of `e, data, state`.** Instead of doing `return state.transform()....apply()` the plugins can now act on the change object directly. Plugins can still `return change...` if they want to break the stack from continuing on to other plugins. (Any `!= null` value will break out.) But they can also now not return anything, and the stack will apply their changes and continue onwards. This was previously impossible. The new syntax looks like: +**Similarly, handlers now receive `e, data, change` instead of `e, data, state`.** Instead of doing `return state.transform()....apply()` the plugins can now act on the change object directly. Plugins can still `return change...` if they want to break the stack from continuing on to other plugins. (Any `!= null` value will break out.) But they can also now not return anything, and the stack will apply their changes and continue onwards. This was previously impossible. The new syntax looks like: ```js function onKeyDown(e, data, change) { @@ -222,29 +250,29 @@ function onKeyDown(e, data, change) { } ``` -* **The `onChange` and `on[Before]Change` handlers now receive `Change` objects.** Previously they would also receive a `state` object, but now they receive `change` objects like the rest of the plugin API. +**The `onChange` and `on[Before]Change` handlers now receive `Change` objects.** Previously they would also receive a `state` object, but now they receive `change` objects like the rest of the plugin API. -* **The `.apply({ save })` option is now `state.change({ save })` instead.** This is the easiest way to use it, but requires that you know whether to save or not up front. If you want to use it inline after already saving some changes, you can use the `change.setOperationFlag('save', true)` flag instead. This shouldn't be necessary for 99% of use cases though. +**The `.apply({ save })` option is now `state.change({ save })` instead.** This is the easiest way to use it, but requires that you know whether to save or not up front. If you want to use it inline after already saving some changes, you can use the `change.setOperationFlag('save', true)` flag instead. This shouldn't be necessary for 99% of use cases though. -* **The `.undo()` and `.redo()` transforms don't save by default.** Previously you had to specifically tell these transforms not to save into the history, which was awkward. Now they won't save the operations they're undoing/redoing by default. +**The `.undo()` and `.redo()` transforms don't save by default.** Previously you had to specifically tell these transforms not to save into the history, which was awkward. Now they won't save the operations they're undoing/redoing by default. -* **`onBeforeChange` is no longer called from `componentWillReceiveProps`,** when a new `state` is passed in as props to the `` component. This caused lots of state-management issues and was weird in the first place because passing in props would result in changes firing. **It is now the parent component's responsibility to not pass in improperly formatted `State` objects**. +**`onBeforeChange` is no longer called from `componentWillReceiveProps`,** when a new `state` is passed in as props to the `` component. This caused lots of state-management issues and was weird in the first place because passing in props would result in changes firing. **It is now the parent component's responsibility to not pass in improperly formatted `State` objects**. -* **The `splitNodeByKey` change method has changed to be shallow.** Previously, it would deeply split to an offset. But now it is shallow and another `splitDescendantsByKey` change method has been added (with a different signature) for the deep splitting behavior. This is needed because splitting and joining operations have been changed to all be shallow, which is required so that operational transforms can be written against them. +**The `splitNodeByKey` change method has changed to be shallow.** Previously, it would deeply split to an offset. But now it is shallow and another `splitDescendantsByKey` change method has been added (with a different signature) for the deep splitting behavior. This is needed because splitting and joining operations have been changed to all be shallow, which is required so that operational transforms can be written against them. -* **Blocks cannot have mixed "inline" and "block" children anymore.** Blocks were implicitly expected to either contain "text" and "inline" nodes only, or to contain "block" nodes only. Invalid case are now normalized by the core schema. +**Blocks cannot have mixed "inline" and "block" children anymore.** Blocks were implicitly expected to either contain "text" and "inline" nodes only, or to contain "block" nodes only. Invalid case are now normalized by the core schema. -* **The shape of many operations has changed.** This was needed to make operations completely invertible without any extra context. The operations were never really exposed in a consumable way, so I won't detail all of the changes here, but feel free to look at the source to see the details. +**The shape of many operations has changed.** This was needed to make operations completely invertible without any extra context. The operations were never really exposed in a consumable way, so I won't detail all of the changes here, but feel free to look at the source to see the details. -* **All references to "joining" nodes is now called "merging".** This is to be slightly clearer, since merging can only happen with adjacent nodes already, and to have a nicer parallel with "splitting", as in cells. The operation is now called `merge_node`, and the transforms are now `merge*`. +**All references to "joining" nodes is now called "merging".** This is to be slightly clearer, since merging can only happen with adjacent nodes already, and to have a nicer parallel with "splitting", as in cells. The operation is now called `merge_node`, and the transforms are now `merge*`. ###### DEPRECATED -* **The `transform.apply()` method is deprecated.** Previously this is where the saving into the history would happen, but it created an awkward convention that wasn't necessary. Now operations are saved into the history as they are created with change methods, instead of waiting until the end. You can access the new `State` of a change at any time via `change.state`. +**The `transform.apply()` method is deprecated.** Previously this is where the saving into the history would happen, but it created an awkward convention that wasn't necessary. Now operations are saved into the history as they are created with change methods, instead of waiting until the end. You can access the new `State` of a change at any time via `change.state`. ###### NEW -* **The `state.activeMarks` returns the intersection of marks in the selection.** Previously there was only `state.marks` which returns marks that appeared on _any_ character in the selection. But `state.activeMarks` returns marks that appear on _every_ character in the selection, which is often more useful for implementing standard rich-text editor behaviors. +**The `state.activeMarks` returns the intersection of marks in the selection.** Previously there was only `state.marks` which returns marks that appeared on _any_ character in the selection. But `state.activeMarks` returns marks that appear on _every_ character in the selection, which is often more useful for implementing standard rich-text editor behaviors. --- @@ -252,7 +280,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **The `Html` serializer now uses `DOMParser` instead of `cheerio`.** Previously, the `Html` serializer used the `cheerio` library for representing elements in the serialization rule logic, but cheerio was a very large dependency. It has been removed, and the native browser `DOMParser` is now used instead. All HTML serialization rules will need to be updated. If you are working with Slate on the server, you can now pass in a custom serializer to the `Html` constructor, using the `parse5` library. +**The `Html` serializer now uses `DOMParser` instead of `cheerio`.** Previously, the `Html` serializer used the `cheerio` library for representing elements in the serialization rule logic, but cheerio was a very large dependency. It has been removed, and the native browser `DOMParser` is now used instead. All HTML serialization rules will need to be updated. If you are working with Slate on the server, you can now pass in a custom serializer to the `Html` constructor, using the `parse5` library. --- @@ -260,7 +288,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **Returning `null` from the `Html` serializer skips the element.** Previously, `null` and `undefined` had the same behavior of skipping the rule and trying the rest of the rules. Now if you explicitly return `null` it will skip the element itself. +**Returning `null` from the `Html` serializer skips the element.** Previously, `null` and `undefined` had the same behavior of skipping the rule and trying the rest of the rules. Now if you explicitly return `null` it will skip the element itself. --- @@ -268,21 +296,22 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **The `filterDescendants` and `findDescendants` methods are now depth-first.** This shouldn't affect almost anyone, since they are usually not the best things to be using for performance reasons. If you happen to have a very specific use case that needs breadth-first, (or even likely something better), you'll need to implement it yourself. +**The `filterDescendants` and `findDescendants` methods are now depth-first.** This shouldn't affect almost anyone, since they are usually not the best things to be using for performance reasons. If you happen to have a very specific use case that needs breadth-first, (or even likely something better), you'll need to implement it yourself. ###### DEPRECATED -* **Some `Node` methods have been deprecated!** There were a few methods that had been added over time that were either poorly named that have been deprecated and renamed, and a handful of methods that are no longer useful for the core library that have been deprecated. Here's a full list: - * `areDescendantSorted` -> `areDescendantsSorted` - * `getHighestChild` -> `getFurthestAncestor` - * `getHighestOnlyChildParent` -> `getFurthestOnlyChildAncestor` - * `concatChildren` - * `decorateTexts` - * `filterDescendantsDeep` - * `findDescendantDeep` - * `getChildrenBetween` - * `getChildrenBetweenIncluding` - * `isInlineSplitAtRange` +**Some `Node` methods have been deprecated!** There were a few methods that had been added over time that were either poorly named that have been deprecated and renamed, and a handful of methods that are no longer useful for the core library that have been deprecated. Here's a full list: + +* `areDescendantSorted` -> `areDescendantsSorted` +* `getHighestChild` -> `getFurthestAncestor` +* `getHighestOnlyChildParent` -> `getFurthestOnlyChildAncestor` +* `concatChildren` +* `decorateTexts` +* `filterDescendantsDeep` +* `findDescendantDeep` +* `getChildrenBetween` +* `getChildrenBetweenIncluding` +* `isInlineSplitAtRange` --- @@ -290,7 +319,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **The `plugin.render` property is now called `plugin.renderPortal`.** This is to make way for the new `plugin.render` property that offers HOC-like behavior, so that plugins can augment the editor however they choose. +**The `plugin.render` property is now called `plugin.renderPortal`.** This is to make way for the new `plugin.render` property that offers HOC-like behavior, so that plugins can augment the editor however they choose. --- @@ -298,31 +327,32 @@ function onKeyDown(e, data, change) { ###### DEPRECATED -* **Some `Selection` methods have been deprecated!** Previously there were many inconsistencies in the naming and handling of selection changes. This has all been cleaned up, but in the process some methods have been deprecated. Here is a full list of the deprecated methods and their new alternatives: +**Some `Selection` methods have been deprecated!** Previously there were many inconsistencies in the naming and handling of selection changes. This has all been cleaned up, but in the process some methods have been deprecated. Here is a full list of the deprecated methods and their new alternatives: - * `moveToOffsets` -> `moveOffsetsTo` - * `moveForward` -> `move` - * `moveBackward` -> `move` - * `moveAnchorOffset` -> `moveAnchor` - * `moveFocusOffset` -> `moveFocus` - * `moveStartOffset` -> `moveStart` - * `moveEndOffset` -> `moveEnd` - * `extendForward` -> `extend` - * `extendBackward` -> `extend` - * `unset` -> `deselect` +* `moveToOffsets` -> `moveOffsetsTo` +* `moveForward` -> `move` +* `moveBackward` -> `move` +* `moveAnchorOffset` -> `moveAnchor` +* `moveFocusOffset` -> `moveFocus` +* `moveStartOffset` -> `moveStart` +* `moveEndOffset` -> `moveEnd` +* `extendForward` -> `extend` +* `extendBackward` -> `extend` +* `unset` -> `deselect` -* **Some selection transforms have been deprecated!** Along with the methods, the selection-based transforms have also been refactored, resulting in deprecations. Here is a full list of the deprecated transforms and their new alternatives: - * `moveTo` -> `select` - * `moveToOffsets` -> `moveOffsetsTo` - * `moveForward` -> `move` - * `moveBackward` -> `move` - * `moveStartOffset` -> `moveStart` - * `moveEndOffset` -> `moveEnd` - * `extendForward` -> `extend` - * `extendBackward` -> `extend` - * `flipSelection` -> `flip` - * `unsetSelection` -> `deselect` - * `unsetMarks` +**Some selection transforms have been deprecated!** Along with the methods, the selection-based transforms have also been refactored, resulting in deprecations. Here is a full list of the deprecated transforms and their new alternatives: + +* `moveTo` -> `select` +* `moveToOffsets` -> `moveOffsetsTo` +* `moveForward` -> `move` +* `moveBackward` -> `move` +* `moveStartOffset` -> `moveStart` +* `moveEndOffset` -> `moveEnd` +* `extendForward` -> `extend` +* `extendBackward` -> `extend` +* `flipSelection` -> `flip` +* `unsetSelection` -> `deselect` +* `unsetMarks` --- @@ -330,7 +360,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **Inline nodes are now always surrounded by text nodes.** Previously this behavior only occured for inline nodes with `isVoid: true`. Now, all inline nodes will always be surrounded by text nodes. If text nodes don't exist, empty ones will be created. This allows for more consistent behavior across Slate, and parity with other editing experiences. +**Inline nodes are now always surrounded by text nodes.** Previously this behavior only occured for inline nodes with `isVoid: true`. Now, all inline nodes will always be surrounded by text nodes. If text nodes don't exist, empty ones will be created. This allows for more consistent behavior across Slate, and parity with other editing experiences. --- @@ -338,15 +368,15 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **The unique `key` generated values have changed.** Previously, Slate generated unique keys that looked like `'9dk3'`. But they were not very conflict-resistant. Now the keys are simple string of auto-incrementing numbers, like `'0'`, `'1'`, `'2'`. This makes more clear that keys are simply a convenient way to uniquely reference nodes in the **short-term** lifespan of a single in-memory instance of Slate. They are not designed to be used for long-term uniqueness. A new `setKeyGenerator` function has been exported that allows you to pass in your own key generating mechanism if you want to ensure uniqueness. +**The unique `key` generated values have changed.** Previously, Slate generated unique keys that looked like `'9dk3'`. But they were not very conflict-resistant. Now the keys are simple string of auto-incrementing numbers, like `'0'`, `'1'`, `'2'`. This makes more clear that keys are simply a convenient way to uniquely reference nodes in the **short-term** lifespan of a single in-memory instance of Slate. They are not designed to be used for long-term uniqueness. A new `setKeyGenerator` function has been exported that allows you to pass in your own key generating mechanism if you want to ensure uniqueness. -* **The `Raw` serializer doesn't preserve keys by default.** Previously, the `Raw` serializer would omit keys when passed the `terse: true` option, but preserve them without it. Now it will always omit keys, unless you pass the new `preserveKeys: true` option. This better reflects that keys are temporary, in-memory IDs. +**The `Raw` serializer doesn't preserve keys by default.** Previously, the `Raw` serializer would omit keys when passed the `terse: true` option, but preserve them without it. Now it will always omit keys, unless you pass the new `preserveKeys: true` option. This better reflects that keys are temporary, in-memory IDs. -* **Operations on the document now update the selection when needed.** This won't affect you unless you were doing some very specific things with transforms and updating selections. Overall, this makes it much easier to write transforms, since in most cases, the underlying operations will update the selection as you would expect without you doing anything. +**Operations on the document now update the selection when needed.** This won't affect you unless you were doing some very specific things with transforms and updating selections. Overall, this makes it much easier to write transforms, since in most cases, the underlying operations will update the selection as you would expect without you doing anything. ###### DEPRECATED -* **Node accessor methods no longer accept being passed another node!** Previously, node accessor methods like `node.getParent` could be passed either a `key` string or a `node` object. For performance reasons, passing in a `node` object is being deprecated. So if you have any calls that look like: `node.getParent(descendant)`, they will now need to be written as `node.getParent(descendant.key)`. They will throw a warning for now, and will throw an error in a later version of Slate. +**Node accessor methods no longer accept being passed another node!** Previously, node accessor methods like `node.getParent` could be passed either a `key` string or a `node` object. For performance reasons, passing in a `node` object is being deprecated. So if you have any calls that look like: `node.getParent(descendant)`, they will now need to be written as `node.getParent(descendant.key)`. They will throw a warning for now, and will throw an error in a later version of Slate. --- @@ -354,13 +384,13 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **The `undo` and `redo` transforms need to be applied!** Previously, `undo` and `redo` were special cased such that they did not require an `.apply()` call, and instead would return a new `State` directly. Now this is no longer the case, and they are just like every other transform. +**The `undo` and `redo` transforms need to be applied!** Previously, `undo` and `redo` were special cased such that they did not require an `.apply()` call, and instead would return a new `State` directly. Now this is no longer the case, and they are just like every other transform. -* **Transforms are no longer exposed on `State` or `Node`.** The transforms API has been completely refactored to be built up of "operations" for collaborative editing support. As part of this refactor, the transforms are now only available via the `state.transform()` API, and aren't exposed on the `State` or `Node` objects as they were before. +**Transforms are no longer exposed on `State` or `Node`.** The transforms API has been completely refactored to be built up of "operations" for collaborative editing support. As part of this refactor, the transforms are now only available via the `state.transform()` API, and aren't exposed on the `State` or `Node` objects as they were before. -* **`Transform` objects are now mutable.** Previously `Transform` was an Immutable.js `Record`, but now it is a simple constructor. This is because transforms are inherently mutating their representation of a state, but this decision is [up for discussion](https://github.com/ianstormtaylor/slate/issues/328). +**`Transform` objects are now mutable.** Previously `Transform` was an Immutable.js `Record`, but now it is a simple constructor. This is because transforms are inherently mutating their representation of a state, but this decision is [up for discussion](https://github.com/ianstormtaylor/slate/issues/328). -* **The selection can now be "unset".** Previously, a selection could never be in an "unset" state where the `anchorKey` or `focusKey` was null. This is no longer technically true, although this shouldn't really affect anyone in practice. +**The selection can now be "unset".** Previously, a selection could never be in an "unset" state where the `anchorKey` or `focusKey` was null. This is no longer technically true, although this shouldn't really affect anyone in practice. --- @@ -368,9 +398,9 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **The `renderNode` and `renderMark` properties are gone!** Previously, rendering nodes and marks happened via these two properties of the ``, but this has been replaced by the new `schema` property. Check out the updated examples to see how to define a schema! There's a good chance this eliminates extra code for most use cases! :smile: +**The `renderNode` and `renderMark` properties are gone!** Previously, rendering nodes and marks happened via these two properties of the ``, but this has been replaced by the new `schema` property. Check out the updated examples to see how to define a schema! There's a good chance this eliminates extra code for most use cases! :smile: -* **The `renderDecorations` property is gone!** Decoration rendering has also been replaced by the new `schema` property of the ``. +**The `renderDecorations` property is gone!** Decoration rendering has also been replaced by the new `schema` property of the ``. --- @@ -378,7 +408,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **The `data.files` property is now an `Array`.** Previously it was a native `FileList` object, but needed to be changed to add full support for pasting an dropping files in all browsers. This shouldn't affect you unless you were specifically depending on it being array-like instead of a true `Array`. +**The `data.files` property is now an `Array`.** Previously it was a native `FileList` object, but needed to be changed to add full support for pasting an dropping files in all browsers. This shouldn't affect you unless you were specifically depending on it being array-like instead of a true `Array`. --- @@ -386,7 +416,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **Void nodes are renderered implicitly again!** Previously Slate had required that you wrap void node renderers yourself with the exposed `` wrapping component. This was to allow for selection styling, but a change was made to make selection styling able to handled in Javascript. Now the `` wrapper will be implicitly rendered by Slate, so you do not need to worry about it, and "voidness" only needs to toggled in one place, the `isVoid: true` property of a node. +**Void nodes are renderered implicitly again!** Previously Slate had required that you wrap void node renderers yourself with the exposed `` wrapping component. This was to allow for selection styling, but a change was made to make selection styling able to handled in Javascript. Now the `` wrapper will be implicitly rendered by Slate, so you do not need to worry about it, and "voidness" only needs to toggled in one place, the `isVoid: true` property of a node. --- @@ -394,7 +424,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **Marks are now renderable as components.** Previously the only supported way to render marks was by returning a `style` object. Now you can return a style object, a class name string, or a full React component. Because of this, the DOM will be renderered slightly differently than before, resulting in an extra `` when rendering non-component marks. This won't affect you unless you were depending on the DOM output by Slate for some reason. +**Marks are now renderable as components.** Previously the only supported way to render marks was by returning a `style` object. Now you can return a style object, a class name string, or a full React component. Because of this, the DOM will be renderered slightly differently than before, resulting in an extra `` when rendering non-component marks. This won't affect you unless you were depending on the DOM output by Slate for some reason. --- @@ -402,7 +432,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **The `wrap` and `unwrap` method signatures have changed!** Previously, you would pass `type` and `data` as separate parameters, for example: `wrapBlock('code', { src: true })`. This was inconsistent with other transforms, and has been updated such that a single argument of `properties` is passed instead. So that example could now be: `wrapBlock({ type: 'code', { data: { src: true }})`. You can still pass a `type` string as shorthand, which will be the most frequent use case, for example: `wrapBlock('code')`. +**The `wrap` and `unwrap` method signatures have changed!** Previously, you would pass `type` and `data` as separate parameters, for example: `wrapBlock('code', { src: true })`. This was inconsistent with other transforms, and has been updated such that a single argument of `properties` is passed instead. So that example could now be: `wrapBlock({ type: 'code', { data: { src: true }})`. You can still pass a `type` string as shorthand, which will be the most frequent use case, for example: `wrapBlock('code')`. --- @@ -410,13 +440,13 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **The `onKeyDown` and `onBeforeInput` handlers signatures have changed!** Previously, some Slate handlers had a signature of `(e, state, editor)` and others had a signature of `(e, data, state, editor)`. Now all handlers will be passed a `data` object—which contains Slate-specific data related to the event—even if it is empty. This is helpful for future compatibility where we might need to add data to a handler that previously didn't have any, and is nicer for consistency. The `onKeyDown` handler's new `data` object contains the `key` name, `code` and a series of `is*` properties to make working with hotkeys easier. The `onBeforeInput` handler's new `data` object is empty. +**The `onKeyDown` and `onBeforeInput` handlers signatures have changed!** Previously, some Slate handlers had a signature of `(e, state, editor)` and others had a signature of `(e, data, state, editor)`. Now all handlers will be passed a `data` object—which contains Slate-specific data related to the event—even if it is empty. This is helpful for future compatibility where we might need to add data to a handler that previously didn't have any, and is nicer for consistency. The `onKeyDown` handler's new `data` object contains the `key` name, `code` and a series of `is*` properties to make working with hotkeys easier. The `onBeforeInput` handler's new `data` object is empty. -* **The `Utils` export has been removed.** Previously, a `Key` utility and the `findDOMNode` utility were exposed under the `Utils` object. The `Key` has been removed in favor of the `data` object passed to `onKeyDown`. And then `findDOMNode` utility has been upgraded to a top-level named export, so you'll now need to access it via `import { findDOMNode } from 'slate'`. +**The `Utils` export has been removed.** Previously, a `Key` utility and the `findDOMNode` utility were exposed under the `Utils` object. The `Key` has been removed in favor of the `data` object passed to `onKeyDown`. And then `findDOMNode` utility has been upgraded to a top-level named export, so you'll now need to access it via `import { findDOMNode } from 'slate'`. -* **Void nodes now permanently have `" "` as content.** Previously, they contained an empty string, but this isn't technically correct, since they have content and shouldn't be considered "empty". Now they will have a single space of content. This shouldn't really affect anyone, unless you happened to be accessing that string for serialization. +**Void nodes now permanently have `" "` as content.** Previously, they contained an empty string, but this isn't technically correct, since they have content and shouldn't be considered "empty". Now they will have a single space of content. This shouldn't really affect anyone, unless you happened to be accessing that string for serialization. -* **Empty inline nodes are now impossible.** This is to stay consistent with native `contenteditable` behavior, where although technically the elements can exist, they have odd behavior and can never be selected. +**Empty inline nodes are now impossible.** This is to stay consistent with native `contenteditable` behavior, where although technically the elements can exist, they have odd behavior and can never be selected. --- @@ -424,7 +454,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **The `Raw` serializer is no longer terse by default!** Previously, the `Raw` serializer would return a "terse" representation of the document, omitting information that wasn't _strictly_ necessary to deserialize later, like the `key` of nodes. By default this no longer happens. You have to opt-in to the behavior by passing `{ terse: true }` as the second `options` argument of the `deserialize` and `serialize` methods. +**The `Raw` serializer is no longer terse by default!** Previously, the `Raw` serializer would return a "terse" representation of the document, omitting information that wasn't _strictly_ necessary to deserialize later, like the `key` of nodes. By default this no longer happens. You have to opt-in to the behavior by passing `{ terse: true }` as the second `options` argument of the `deserialize` and `serialize` methods. --- @@ -432,9 +462,9 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **Void components are no longer rendered implicity!** Previously, Slate would automatically wrap any node with `isVoid: true` in a `` component. But doing this prevented you from customizing the wrapper, like adding a `className` or `style` property. So you **must now render the wrapper yourself**, and it has been exported as `Slate.Void`. This, combined with a small change to the `` component's structure allows the "selected" state of void nodes to be rendered purely with CSS based on the `:focus` property of a `` element, which previously [had to be handled in Javascript](https://github.com/ianstormtaylor/slate/commit/31782cb11a272466b6b9f1e4d6cc0c698504d97f). This allows us to streamline selection-handling logic, improving performance and reducing complexity. +**Void components are no longer rendered implicity!** Previously, Slate would automatically wrap any node with `isVoid: true` in a `` component. But doing this prevented you from customizing the wrapper, like adding a `className` or `style` property. So you **must now render the wrapper yourself**, and it has been exported as `Slate.Void`. This, combined with a small change to the `` component's structure allows the "selected" state of void nodes to be rendered purely with CSS based on the `:focus` property of a `` element, which previously [had to be handled in Javascript](https://github.com/ianstormtaylor/slate/commit/31782cb11a272466b6b9f1e4d6cc0c698504d97f). This allows us to streamline selection-handling logic, improving performance and reducing complexity. -* **`data-offset-key` is now `-` instead of `:-`.** This shouldn't actually affect anyone, unless you were specifically relying on that attribute in the DOM. This change greatly reduces the number of re-renders needed, since previously any additional characters would cause a cascading change in the `` and `` offsets of latter text ranges. +**`data-offset-key` is now `-` instead of `:-`.** This shouldn't actually affect anyone, unless you were specifically relying on that attribute in the DOM. This change greatly reduces the number of re-renders needed, since previously any additional characters would cause a cascading change in the `` and `` offsets of latter text ranges. --- @@ -442,9 +472,9 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **`node.getTextNodes()` is now `node.getTexts()`.** This is just for consistency with the other existing `Node` methods like `getBlocks()`, `getInlines()`, etc. And it's nicely shorter. :wink: +**`node.getTextNodes()` is now `node.getTexts()`.** This is just for consistency with the other existing `Node` methods like `getBlocks()`, `getInlines()`, etc. And it's nicely shorter. :wink: -* **`Node` methods now `throw` earlier during unexpected states.** This shouldn't break anything for most folks, unless a strange edge-case was going undetected previously. +**`Node` methods now `throw` earlier during unexpected states.** This shouldn't break anything for most folks, unless a strange edge-case was going undetected previously. --- @@ -452,7 +482,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **`renderMark(mark, state, editor)` is now `renderMark(mark, marks, state, editor)`.** This change allows you to render marks based on multiple `marks` presence at once on a given range of text, for example using a custom `BoldItalic.otf` font when text has both `bold` and `italic` marks. +**`renderMark(mark, state, editor)` is now `renderMark(mark, marks, state, editor)`.** This change allows you to render marks based on multiple `marks` presence at once on a given range of text, for example using a custom `BoldItalic.otf` font when text has both `bold` and `italic` marks. --- @@ -460,7 +490,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **`transform.unwrapBlock()` now unwraps selectively.** Previously, calling `unwrapBlock` with a range representing a middle sibling would unwrap _all_ of the siblings, removing the wrapping block entirely. Now, calling it with those same arguments will only move the middle sibling up a layer in the hierarchy, preserving the nesting on any of its siblings. This changes makes it much simpler to implement functionality like unwrapping a single list item, which previously would unwrap the entire list. +**`transform.unwrapBlock()` now unwraps selectively.** Previously, calling `unwrapBlock` with a range representing a middle sibling would unwrap _all_ of the siblings, removing the wrapping block entirely. Now, calling it with those same arguments will only move the middle sibling up a layer in the hierarchy, preserving the nesting on any of its siblings. This changes makes it much simpler to implement functionality like unwrapping a single list item, which previously would unwrap the entire list. --- @@ -468,7 +498,7 @@ function onKeyDown(e, data, change) { ###### BREAKING -* **`transform.mark()` is now `transform.addMark()` and `transform.unmark()` is now `transform.removeMark()`.** The new names make it clearer that the transforms are actions being performed, and it paves the way for adding a `toggleMark` convenience as well. +**`transform.mark()` is now `transform.addMark()` and `transform.unmark()` is now `transform.removeMark()`.** The new names make it clearer that the transforms are actions being performed, and it paves the way for adding a `toggleMark` convenience as well. --- diff --git a/packages/slate/src/changes/at-current-range.js b/packages/slate/src/changes/at-current-range.js index ef536e200..709be1983 100644 --- a/packages/slate/src/changes/at-current-range.js +++ b/packages/slate/src/changes/at-current-range.js @@ -162,7 +162,7 @@ Changes.insertFragment = (change, fragment) => { selection.hasEdgeAtEndOf(endText) const isInserting = - fragment.hasBlocks(firstChild.key) || fragment.hasBlocks(lastChild.key) + firstChild.hasBlockChildren() || lastChild.hasBlockChildren() change.insertFragmentAtRange(selection, fragment) value = change.value diff --git a/packages/slate/src/changes/at-range.js b/packages/slate/src/changes/at-range.js index 108aebc72..a7661979f 100644 --- a/packages/slate/src/changes/at-range.js +++ b/packages/slate/src/changes/at-range.js @@ -4,7 +4,7 @@ import Block from '../models/block' import Inline from '../models/inline' import Mark from '../models/mark' import Node from '../models/node' -import String from '../utils/string' +import TextUtils from '../utils/text-utils' /** * Changes. @@ -278,7 +278,7 @@ Changes.deleteCharBackwardAtRange = (change, range, options) => { const offset = startBlock.getOffset(startKey) const o = offset + startOffset const { text } = startBlock - const n = String.getCharOffsetBackward(text, o) + const n = TextUtils.getCharOffsetBackward(text, o) change.deleteBackwardAtRange(range, n, options) } @@ -318,7 +318,7 @@ Changes.deleteWordBackwardAtRange = (change, range, options) => { const offset = startBlock.getOffset(startKey) const o = offset + startOffset const { text } = startBlock - const n = String.getWordOffsetBackward(text, o) + const n = TextUtils.getWordOffsetBackward(text, o) change.deleteBackwardAtRange(range, n, options) } @@ -449,7 +449,7 @@ Changes.deleteCharForwardAtRange = (change, range, options) => { const offset = startBlock.getOffset(startKey) const o = offset + startOffset const { text } = startBlock - const n = String.getCharOffsetForward(text, o) + const n = TextUtils.getCharOffsetForward(text, o) change.deleteForwardAtRange(range, n, options) } @@ -489,7 +489,7 @@ Changes.deleteWordForwardAtRange = (change, range, options) => { const offset = startBlock.getOffset(startKey) const o = offset + startOffset const { text } = startBlock - const n = String.getWordOffsetForward(text, o) + const n = TextUtils.getWordOffsetForward(text, o) change.deleteForwardAtRange(range, n, options) } @@ -599,13 +599,6 @@ Changes.deleteForwardAtRange = (change, range, n = 1, options = {}) => { } } - // If the focus node is inside a void, go up until right before it. - if (document.hasVoidParent(node.key)) { - const parent = document.getClosestVoid(node.key) - node = document.getPreviousText(parent.key) - offset = node.text.length - } - range = range.merge({ focusKey: node.key, focusOffset: offset, @@ -734,7 +727,7 @@ Changes.insertFragmentAtRange = (change, range, fragment, options = {}) => { // If the fragment starts or ends with single nested block, (e.g., table), // do not merge this fragment with existing blocks. - if (fragment.hasBlocks(firstChild.key) || fragment.hasBlocks(lastChild.key)) { + if (firstChild.hasBlockChildren() || lastChild.hasBlockChildren()) { fragment.nodes.reverse().forEach(node => { change.insertBlockAtRange(range, node, options) }) @@ -750,7 +743,7 @@ Changes.insertFragmentAtRange = (change, range, fragment, options = {}) => { ) const lonelyChild = lonelyParent || firstBlock const startIndex = parent.nodes.indexOf(startBlock) - fragment = fragment.removeDescendant(lonelyChild.key) + fragment = fragment.removeNode(lonelyChild.key) fragment.nodes.forEach((node, i) => { const newIndex = startIndex + i + 1 diff --git a/packages/slate/src/changes/by-key.js b/packages/slate/src/changes/by-path.js similarity index 50% rename from packages/slate/src/changes/by-key.js rename to packages/slate/src/changes/by-path.js index e72a60bb6..af799ed78 100644 --- a/packages/slate/src/changes/by-key.js +++ b/packages/slate/src/changes/by-path.js @@ -2,6 +2,7 @@ import Block from '../models/block' import Inline from '../models/inline' import Mark from '../models/mark' import Node from '../models/node' +import PathUtils from '../utils/path-utils' import Range from '../models/range' /** @@ -13,24 +14,21 @@ import Range from '../models/range' const Changes = {} /** - * Add mark to text at `offset` and `length` in node by `key`. + * Add mark to text at `offset` and `length` in node by `path`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Number} offset * @param {Number} length * @param {Mixed} mark * @param {Object} options - * @property {Boolean} normalize */ -Changes.addMarkByKey = (change, key, offset, length, mark, options = {}) => { +Changes.addMarkByPath = (change, path, offset, length, mark, options) => { mark = Mark.create(mark) - const normalize = change.getFlag('normalize', options) const { value } = change const { document } = value - const path = document.getPath(key) - const node = document.getNode(key) + const node = document.assertNode(path) const leaves = node.getLeaves() const operations = [] @@ -65,84 +63,65 @@ Changes.addMarkByKey = (change, key, offset, length, mark, options = {}) => { }) change.applyOperations(operations) - - if (normalize) { - const parent = document.getParent(key) - change.normalizeNodeByKey(parent.key) - } + change.normalizeParentByPath(path, options) } /** - * Insert a `fragment` at `index` in a node by `key`. + * Insert a `fragment` at `index` in a node by `path`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Number} index * @param {Fragment} fragment * @param {Object} options - * @property {Boolean} normalize */ -Changes.insertFragmentByKey = (change, key, index, fragment, options = {}) => { - const normalize = change.getFlag('normalize', options) - +Changes.insertFragmentByPath = (change, path, index, fragment, options) => { fragment.nodes.forEach((node, i) => { - change.insertNodeByKey(key, index + i, node) + change.insertNodeByPath(path, index + i, node) }) - if (normalize) { - change.normalizeNodeByKey(key) - } + change.normalizeNodeByPath(path, options) } /** - * Insert a `node` at `index` in a node by `key`. + * Insert a `node` at `index` in a node by `path`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Number} index * @param {Node} node * @param {Object} options - * @property {Boolean} normalize */ -Changes.insertNodeByKey = (change, key, index, node, options = {}) => { - const normalize = change.getFlag('normalize', options) +Changes.insertNodeByPath = (change, path, index, node, options) => { const { value } = change - const { document } = value - const path = document.getPath(key) change.applyOperation({ type: 'insert_node', value, - path: [...path, index], + path: path.concat(index), node, }) - if (normalize) { - change.normalizeNodeByKey(key) - } + change.normalizeNodeByPath(path, options) } /** - * Insert `text` at `offset` in node by `key`. + * Insert `text` at `offset` in node by `path`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Number} offset * @param {String} text * @param {Set} marks (optional) * @param {Object} options - * @property {Boolean} normalize */ -Changes.insertTextByKey = (change, key, offset, text, marks, options = {}) => { - const normalize = change.getFlag('normalize', options) - +Changes.insertTextByPath = (change, path, offset, text, marks, options) => { const { value } = change const { document } = value - const path = document.getPath(key) - const node = document.getNode(key) + const node = document.assertNode(path) marks = marks || node.getMarksAtIndex(offset) change.applyOperation({ @@ -154,31 +133,27 @@ Changes.insertTextByKey = (change, key, offset, text, marks, options = {}) => { marks, }) - if (normalize) { - const parent = document.getParent(key) - change.normalizeNodeByKey(parent.key) - } + change.normalizeParentByPath(path, options) } /** - * Merge a node by `key` with the previous node. + * Merge a node by `path` with the previous node. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Object} options - * @property {Boolean} normalize */ -Changes.mergeNodeByKey = (change, key, options = {}) => { - const normalize = change.getFlag('normalize', options) +Changes.mergeNodeByPath = (change, path, options) => { const { value } = change const { document } = value - const path = document.getPath(key) - const original = document.getDescendant(key) - const previous = document.getPreviousSibling(key) + const original = document.getDescendant(path) + const previous = document.getPreviousSibling(path) if (!previous) { - throw new Error(`Unable to merge node with key "${key}", no previous key.`) + throw new Error( + `Unable to merge node with path "${path}", because it has no previous sibling.` + ) } const position = @@ -198,63 +173,49 @@ Changes.mergeNodeByKey = (change, key, options = {}) => { target: null, }) - if (normalize) { - const parent = document.getParent(key) - change.normalizeNodeByKey(parent.key) - } + change.normalizeParentByPath(path, options) } /** - * Move a node by `key` to a new parent by `newKey` and `index`. - * `newKey` is the key of the container (it can be the document itself) + * Move a node by `path` to a new parent by `newPath` and `index`. * * @param {Change} change - * @param {String} key - * @param {String} newKey + * @param {Array} path + * @param {String} newPath * @param {Number} index * @param {Object} options - * @property {Boolean} normalize */ -Changes.moveNodeByKey = (change, key, newKey, newIndex, options = {}) => { - const normalize = change.getFlag('normalize', options) +Changes.moveNodeByPath = (change, path, newPath, newIndex, options) => { const { value } = change - const { document } = value - const path = document.getPath(key) - const newPath = document.getPath(newKey) change.applyOperation({ type: 'move_node', value, path, - newPath: [...newPath, newIndex], + newPath: newPath.concat(newIndex), }) - if (normalize) { - const parent = document.getCommonAncestor(key, newKey) - change.normalizeNodeByKey(parent.key) - } + const ancestorPath = PathUtils.relate(path, newPath) + change.normalizeNodeByPath(ancestorPath, options) } /** - * Remove mark from text at `offset` and `length` in node by `key`. + * Remove mark from text at `offset` and `length` in node by `path`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Number} offset * @param {Number} length * @param {Mark} mark * @param {Object} options - * @property {Boolean} normalize */ -Changes.removeMarkByKey = (change, key, offset, length, mark, options = {}) => { +Changes.removeMarkByPath = (change, path, offset, length, mark, options) => { mark = Mark.create(mark) - const normalize = change.getFlag('normalize', options) const { value } = change const { document } = value - const path = document.getPath(key) - const node = document.getNode(key) + const node = document.assertNode(path) const leaves = node.getLeaves() const operations = [] @@ -289,26 +250,21 @@ Changes.removeMarkByKey = (change, key, offset, length, mark, options = {}) => { }) change.applyOperations(operations) - - if (normalize) { - const parent = document.getParent(key) - change.normalizeNodeByKey(parent.key) - } + change.normalizeParentByPath(path, options) } /** - * Remove all `marks` from node by `key`. + * Remove all `marks` from node by `path`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Object} options - * @property {Boolean} normalize */ -Changes.removeAllMarksByKey = (change, key, options = {}) => { +Changes.removeAllMarksByPath = (change, path, options) => { const { state } = change const { document } = state - const node = document.getNode(key) + const node = document.assertNode(path) const texts = node.object === 'text' ? [node] : node.getTextsAsArray() texts.forEach(text => { @@ -319,20 +275,17 @@ Changes.removeAllMarksByKey = (change, key, options = {}) => { } /** - * Remove a node by `key`. + * Remove a node by `path`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Object} options - * @property {Boolean} normalize */ -Changes.removeNodeByKey = (change, key, options = {}) => { - const normalize = change.getFlag('normalize', options) +Changes.removeNodeByPath = (change, path, options) => { const { value } = change const { document } = value - const path = document.getPath(key) - const node = document.getNode(key) + const node = document.assertNode(path) change.applyOperation({ type: 'remove_node', @@ -341,26 +294,25 @@ Changes.removeNodeByKey = (change, key, options = {}) => { node, }) - if (normalize) { - const parent = document.getParent(key) - change.normalizeNodeByKey(parent.key) - } + change.normalizeParentByPath(path, options) } /** - * Insert `text` at `offset` in node by `key`. + * Insert `text` at `offset` in node by `path`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {String} text * @param {Set} marks (optional) * @param {Object} options - * @property {Boolean} normalize */ -Changes.setTextByKey = (change, key, text, marks, options = {}) => { - const textNode = change.value.document.getDescendant(key) - change.replaceTextByKey(key, 0, textNode.text.length, text, marks, options) +Changes.setTextByPath = (change, path, text, marks, options) => { + const { value } = change + const { document } = value + const node = document.assertNode(path) + const end = node.text.length + change.replaceTextByPath(path, 0, end, text, marks, options) } /** @@ -372,13 +324,12 @@ Changes.setTextByKey = (change, key, text, marks, options = {}) => { * @param {string} text * @param {Set} marks (optional) * @param {Object} options - * @property {Boolean} normalize * */ -Changes.replaceTextByKey = ( +Changes.replaceTextByPath = ( change, - key, + path, offset, length, text, @@ -386,21 +337,22 @@ Changes.replaceTextByKey = ( options ) => { const { document } = change.value - const textNode = document.getDescendant(key) + const node = document.assertNode(path) - if (length + offset > textNode.text.length) { - length = textNode.text.length - offset + if (length + offset > node.text.length) { + length = node.text.length - offset } const range = Range.create({ - anchorKey: key, - focusKey: key, + anchorPath: path, + focusPath: path, anchorOffset: offset, focusOffset: offset + length, - }) + }).normalize(document) + let activeMarks = document.getActiveMarksAtRange(range) - change.removeTextByKey(key, offset, length, { normalize: false }) + change.removeTextByPath(path, offset, length, { normalize: false }) if (!marks) { // Do not use mark at index when marks and activeMarks are both empty @@ -414,26 +366,23 @@ Changes.replaceTextByKey = ( marks = activeMarks.merge(marks) } - change.insertTextByKey(key, offset, text, marks, options) + change.insertTextByPath(path, offset, text, marks, options) } /** - * Remove text at `offset` and `length` in node by `key`. + * Remove text at `offset` and `length` in node by `path`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Number} offset * @param {Number} length * @param {Object} options - * @property {Boolean} normalize */ -Changes.removeTextByKey = (change, key, offset, length, options = {}) => { - const normalize = change.getFlag('normalize', options) +Changes.removeTextByPath = (change, path, offset, length, options) => { const { value } = change const { document } = value - const path = document.getPath(key) - const node = document.getNode(key) + const node = document.assertNode(path) const leaves = node.getLeaves() const { text } = node @@ -469,65 +418,51 @@ Changes.removeTextByKey = (change, key, offset, length, options = {}) => { // Apply in reverse order, so subsequent removals don't impact previous ones. change.applyOperations(removals.reverse()) - if (normalize) { - const block = document.getClosestBlock(key) - change.normalizeNodeByKey(block.key) - } + const block = document.getClosestBlock(node.key) + change.normalizeNodeByKey(block.key, options) } /** `* Replace a `node` with another `node` * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Object|Node} node * @param {Object} options - * @property {Boolean} normalize */ -Changes.replaceNodeByKey = (change, key, newNode, options = {}) => { +Changes.replaceNodeByPath = (change, path, newNode, options) => { newNode = Node.create(newNode) - const normalize = change.getFlag('normalize', options) - const { value } = change - const { document } = value - const node = document.getNode(key) - const parent = document.getParent(key) - const index = parent.nodes.indexOf(node) - change.removeNodeByKey(key, { normalize: false }) - change.insertNodeByKey(parent.key, index, newNode, { normalize: false }) - - if (normalize) { - change.normalizeNodeByKey(parent.key) - } + const index = path.last() + const parentPath = PathUtils.lift(path) + change.removeNodeByPath(path, { normalize: false }) + change.insertNodeByPath(parentPath, index, newNode, { normalize: false }) + change.normalizeParentByPath(path, options) } /** - * Set `properties` on mark on text at `offset` and `length` in node by `key`. + * Set `properties` on mark on text at `offset` and `length` in node by `path`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Number} offset * @param {Number} length * @param {Mark} mark * @param {Object} options - * @property {Boolean} normalize */ -Changes.setMarkByKey = ( +Changes.setMarkByPath = ( change, - key, + path, offset, length, mark, properties, - options = {} + options ) => { mark = Mark.create(mark) properties = Mark.createProperties(properties) - const normalize = change.getFlag('normalize', options) const { value } = change - const { document } = value - const path = document.getPath(key) change.applyOperation({ type: 'set_mark', @@ -539,29 +474,23 @@ Changes.setMarkByKey = ( properties, }) - if (normalize) { - const parent = document.getParent(key) - change.normalizeNodeByKey(parent.key) - } + change.normalizeParentByPath(path, options) } /** - * Set `properties` on a node by `key`. + * Set `properties` on a node by `path`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Object|String} properties * @param {Object} options - * @property {Boolean} normalize */ -Changes.setNodeByKey = (change, key, properties, options = {}) => { +Changes.setNodeByPath = (change, path, properties, options) => { properties = Node.createProperties(properties) - const normalize = change.getFlag('normalize', options) const { value } = change const { document } = value - const path = document.getPath(key) - const node = document.getNode(key) + const node = document.assertNode(path) change.applyOperation({ type: 'set_node', @@ -571,27 +500,23 @@ Changes.setNodeByKey = (change, key, properties, options = {}) => { properties, }) - if (normalize) { - change.normalizeNodeByKey(node.key) - } + change.normalizeNodeByPath(path, options) } /** - * Split a node by `key` at `position`. + * Split a node by `path` at `position`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Number} position * @param {Object} options - * @property {Boolean} normalize */ -Changes.splitNodeByKey = (change, key, position, options = {}) => { - const { normalize = true, target = null } = options +Changes.splitNodeByPath = (change, path, position, options = {}) => { + const { target = null } = options const { value } = change const { document } = value - const path = document.getPath(key) - const node = document.getDescendantAtPath(path) + const node = document.getDescendant(path) change.applyOperation({ type: 'split_node', @@ -605,78 +530,71 @@ Changes.splitNodeByKey = (change, key, position, options = {}) => { target, }) - if (normalize) { - const parent = document.getParent(key) - change.normalizeNodeByKey(parent.key) - } + change.normalizeParentByPath(path, options) } /** - * Split a node deeply down the tree by `key`, `textKey` and `textOffset`. + * Split a node deeply down the tree by `path`, `textPath` and `textOffset`. * * @param {Change} change - * @param {String} key - * @param {Number} position + * @param {Array} path + * @param {Array} textPath + * @param {Number} textOffset * @param {Object} options - * @property {Boolean} normalize */ -Changes.splitDescendantsByKey = ( +Changes.splitDescendantsByPath = ( change, - key, - textKey, + path, + textPath, textOffset, - options = {} + options ) => { - if (key == textKey) { - change.splitNodeByKey(textKey, textOffset, options) + if (path.equals(textPath)) { + change.splitNodeByPath(textPath, textOffset, options) return } - const normalize = change.getFlag('normalize', options) const { value } = change const { document } = value - - const text = document.getNode(textKey) - const ancestors = document.getAncestors(textKey) + const node = document.assertNode(path) + const text = document.assertNode(textPath) + const ancestors = document.getAncestors(textPath) const nodes = ancestors - .skipUntil(a => a.key == key) + .skipUntil(a => a.key == node.key) .reverse() .unshift(text) + let previous let index - nodes.forEach(node => { + nodes.forEach(n => { const prevIndex = index == null ? null : index - index = previous ? node.nodes.indexOf(previous) + 1 : textOffset - previous = node + index = previous ? n.nodes.indexOf(previous) + 1 : textOffset + previous = n - change.splitNodeByKey(node.key, index, { + change.splitNodeByKey(n.key, index, { normalize: false, target: prevIndex, }) }) - if (normalize) { - const parent = document.getParent(key) - change.normalizeNodeByKey(parent.key) - } + change.normalizeParentByPath(path, options) } /** * Unwrap content from an inline parent with `properties`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Object|String} properties * @param {Object} options - * @property {Boolean} normalize */ -Changes.unwrapInlineByKey = (change, key, properties, options) => { +Changes.unwrapInlineByPath = (change, path, properties, options) => { const { value } = change const { document, selection } = value - const node = document.assertDescendant(key) + const node = document.assertNode(path) const first = node.getFirstText() const last = node.getLastText() const range = selection.moveToRangeOf(first, last) @@ -687,16 +605,15 @@ Changes.unwrapInlineByKey = (change, key, properties, options) => { * Unwrap content from a block parent with `properties`. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Object|String} properties * @param {Object} options - * @property {Boolean} normalize */ -Changes.unwrapBlockByKey = (change, key, properties, options) => { +Changes.unwrapBlockByPath = (change, path, properties, options) => { const { value } = change const { document, selection } = value - const node = document.assertDescendant(key) + const node = document.assertNode(path) const first = node.getFirstText() const last = node.getLastText() const range = selection.moveToRangeOf(first, last) @@ -711,49 +628,44 @@ Changes.unwrapBlockByKey = (change, key, properties, options) => { * simply replaced by the node itself. Cannot unwrap a root node. * * @param {Change} change - * @param {String} key + * @param {Array} path * @param {Object} options - * @property {Boolean} normalize */ -Changes.unwrapNodeByKey = (change, key, options = {}) => { - const normalize = change.getFlag('normalize', options) +Changes.unwrapNodeByPath = (change, path, options) => { const { value } = change const { document } = value - const parent = document.getParent(key) - const node = parent.getChild(key) + document.assertNode(path) - const index = parent.nodes.indexOf(node) + const parentPath = PathUtils.lift(path) + const parent = document.assertNode(parentPath) + const index = path.last() + const parentIndex = parentPath.last() + const grandPath = PathUtils.lift(parentPath) const isFirst = index === 0 const isLast = index === parent.nodes.size - 1 - const parentParent = document.getParent(parent.key) - const parentIndex = parentParent.nodes.indexOf(parent) - if (parent.nodes.size === 1) { - change.moveNodeByKey(key, parentParent.key, parentIndex, { + change.moveNodeByPath(path, grandPath, parentIndex + 1, { normalize: false, }) - change.removeNodeByKey(parent.key, options) + change.removeNodeByPath(parentPath, options) } else if (isFirst) { - // Just move the node before its parent. - change.moveNodeByKey(key, parentParent.key, parentIndex, options) + change.moveNodeByPath(path, grandPath, parentIndex, options) } else if (isLast) { - // Just move the node after its parent. - change.moveNodeByKey(key, parentParent.key, parentIndex + 1, options) + change.moveNodeByPath(path, grandPath, parentIndex + 1, options) } else { - // Split the parent. - change.splitNodeByKey(parent.key, index, { normalize: false }) + change.splitNodeByPath(parentPath, index, { normalize: false }) - // Extract the node in between the splitted parent. - change.moveNodeByKey(key, parentParent.key, parentIndex + 1, { + let updatedPath = PathUtils.increment(path, 1, parentPath.size - 1) + updatedPath = updatedPath.set(updatedPath.size - 1, 0) + + change.moveNodeByPath(updatedPath, grandPath, parentIndex + 1, { normalize: false, }) - if (normalize) { - change.normalizeNodeByKey(parentParent.key) - } + change.normalizeNodeByPath(grandPath, options) } } @@ -761,72 +673,118 @@ Changes.unwrapNodeByKey = (change, key, options = {}) => { * Wrap a node in a block with `properties`. * * @param {Change} change - * @param {String} key The node to wrap - * @param {Block|Object|String} block The wrapping block (its children are discarded) + * @param {Array} path + * @param {Block|Object|String} block * @param {Object} options - * @property {Boolean} normalize */ -Changes.wrapBlockByKey = (change, key, block, options) => { +Changes.wrapBlockByPath = (change, path, block, options) => { block = Block.create(block) block = block.set('nodes', block.nodes.clear()) - - const { document } = change.value - const node = document.assertDescendant(key) - const parent = document.getParent(node.key) - const index = parent.nodes.indexOf(node) - - change.insertNodeByKey(parent.key, index, block, { normalize: false }) - change.moveNodeByKey(node.key, block.key, 0, options) + const parentPath = PathUtils.lift(path) + const index = path.last() + const newPath = PathUtils.increment(path) + change.insertNodeByPath(parentPath, index, block, { normalize: false }) + change.moveNodeByPath(newPath, path, 0, options) } /** * Wrap a node in an inline with `properties`. * * @param {Change} change - * @param {String} key The node to wrap - * @param {Block|Object|String} inline The wrapping inline (its children are discarded) + * @param {Array} path + * @param {Block|Object|String} inline * @param {Object} options - * @property {Boolean} normalize */ -Changes.wrapInlineByKey = (change, key, inline, options) => { +Changes.wrapInlineByPath = (change, path, inline, options) => { inline = Inline.create(inline) inline = inline.set('nodes', inline.nodes.clear()) - - const { document } = change.value - const node = document.assertDescendant(key) - const parent = document.getParent(node.key) - const index = parent.nodes.indexOf(node) - - change.insertNodeByKey(parent.key, index, inline, { normalize: false }) - change.moveNodeByKey(node.key, inline.key, 0, options) + const parentPath = PathUtils.lift(path) + const index = path.last() + const newPath = PathUtils.increment(path) + change.insertNodeByPath(parentPath, index, inline, { normalize: false }) + change.moveNodeByPath(newPath, path, 0, options) } /** - * Wrap a node by `key` with `parent`. + * Wrap a node by `path` with `node`. * * @param {Change} change - * @param {String} key - * @param {Node|Object} parent + * @param {Array} path + * @param {Node|Object} node * @param {Object} options */ -Changes.wrapNodeByKey = (change, key, parent) => { - parent = Node.create(parent) - parent = parent.set('nodes', parent.nodes.clear()) +Changes.wrapNodeByPath = (change, path, node) => { + node = Node.create(node) - if (parent.object == 'block') { - change.wrapBlockByKey(key, parent) + if (node.object == 'block') { + change.wrapBlockByPath(path, node) return } - if (parent.object == 'inline') { - change.wrapInlineByKey(key, parent) + if (node.object == 'inline') { + change.wrapInlineByPath(path, node) return } } +/** + * Mix in `*ByKey` variants. + */ + +const CHANGES = [ + 'addMark', + 'insertFragment', + 'insertNode', + 'insertText', + 'mergeNode', + 'removeMark', + 'removeAllMarks', + 'removeNode', + 'setText', + 'replaceText', + 'removeText', + 'replaceNode', + 'setMark', + 'setNode', + 'splitNode', + 'unwrapInline', + 'unwrapBlock', + 'unwrapNode', + 'wrapBlock', + 'wrapInline', + 'wrapNode', +] + +for (const method of CHANGES) { + Changes[`${method}ByKey`] = (change, key, ...args) => { + const { value } = change + const { document } = value + const path = document.assertPath(key) + change[`${method}ByPath`](path, ...args) + } +} + +// Moving nodes takes two keys, so it's slightly different. +Changes.moveNodeByKey = (change, key, newKey, ...args) => { + const { value } = change + const { document } = value + const path = document.assertPath(key) + const newPath = document.assertPath(newKey) + change.moveNodeByPath(path, newPath, ...args) +} + +// Splitting descendants takes two keys, so it's slightly different. +Changes.splitDescendantsByKey = (change, key, textKey, ...args) => { + const { value } = change + const { document } = value + const path = document.assertPath(key) + const textPath = document.assertPath(textKey) + change.splitDescendantsByPath(path, textPath, ...args) +} + /** * Export. * diff --git a/packages/slate/src/changes/index.js b/packages/slate/src/changes/index.js index d9d49c615..89a1b3bd5 100644 --- a/packages/slate/src/changes/index.js +++ b/packages/slate/src/changes/index.js @@ -1,6 +1,6 @@ import AtCurrentRange from './at-current-range' import AtRange from './at-range' -import ByKey from './by-key' +import ByPath from './by-path' import OnHistory from './on-history' import OnSelection from './on-selection' import OnValue from './on-value' @@ -15,7 +15,7 @@ import WithSchema from './with-schema' export default { ...AtCurrentRange, ...AtRange, - ...ByKey, + ...ByPath, ...OnHistory, ...OnSelection, ...OnValue, diff --git a/packages/slate/src/changes/with-schema.js b/packages/slate/src/changes/with-schema.js index a9f0583b8..e0e61abdd 100644 --- a/packages/slate/src/changes/with-schema.js +++ b/packages/slate/src/changes/with-schema.js @@ -1,3 +1,5 @@ +import PathUtils from '../utils/path-utils' + /** * Changes. * @@ -12,8 +14,8 @@ const Changes = {} * @param {Change} change */ -Changes.normalize = change => { - change.normalizeDocument() +Changes.normalize = (change, options) => { + change.normalizeDocument(options) } /** @@ -22,10 +24,10 @@ Changes.normalize = change => { * @param {Change} change */ -Changes.normalizeDocument = change => { +Changes.normalizeDocument = (change, options) => { const { value } = change const { document } = value - change.normalizeNodeByKey(document.key) + change.normalizeNodeByKey(document.key, options) } /** @@ -35,7 +37,10 @@ Changes.normalizeDocument = change => { * @param {Node|String} key */ -Changes.normalizeNodeByKey = (change, key) => { +Changes.normalizeNodeByKey = (change, key, options = {}) => { + const normalize = change.getFlag('normalize', options) + if (!normalize) return + const { value } = change let { document, schema } = value const node = document.assertNode(key) @@ -53,6 +58,46 @@ Changes.normalizeNodeByKey = (change, key) => { }) } +Changes.normalizeParentByKey = (change, key, options) => { + const { value } = change + const { document } = value + const parent = document.getParent(key) + change.normalizeNodeByKey(parent.key, options) +} + +/** + * Normalize a `node` and its children with the value's schema. + * + * @param {Change} change + * @param {Array} path + */ + +Changes.normalizeNodeByPath = (change, path, options = {}) => { + const normalize = change.getFlag('normalize', options) + if (!normalize) return + + const { value } = change + let { document, schema } = value + const node = document.assertNode(path) + + normalizeNodeAndChildren(change, node, schema) + + document = change.value.document + const ancestors = document.getAncestors(path) + if (!ancestors) return + + ancestors.forEach(ancestor => { + if (change.value.document.getDescendant(ancestor.key)) { + normalizeNode(change, ancestor, schema) + } + }) +} + +Changes.normalizeParentByPath = (change, path, options) => { + const p = PathUtils.lift(path) + change.normalizeNodeByPath(p, options) +} + /** * Normalize a `node` and its children with a `schema`. * diff --git a/packages/slate/src/index.js b/packages/slate/src/index.js index fac43975e..a89fde144 100644 --- a/packages/slate/src/index.js +++ b/packages/slate/src/index.js @@ -6,15 +6,18 @@ import Data from './models/data' import Document from './models/document' import History from './models/history' import Inline from './models/inline' +import KeyUtils from './utils/key-utils' import Leaf from './models/leaf' import Mark from './models/mark' import Node from './models/node' import Operation from './models/operation' import Operations from './operations' +import PathUtils from './utils/path-utils' import Range from './models/range' import Schema from './models/schema' import Stack from './models/stack' import Text from './models/text' +import TextUtils from './utils/text-utils' import Value from './models/value' import { resetKeyGenerator, setKeyGenerator } from './utils/generate-key' import { resetMemoization, useMemoization } from './utils/memoize' @@ -34,20 +37,23 @@ export { Document, History, Inline, + KeyUtils, Leaf, Mark, Node, Operation, Operations, + PathUtils, Range, + resetKeyGenerator, + resetMemoization, Schema, + setKeyGenerator, Stack, Text, - Value, - resetKeyGenerator, - setKeyGenerator, - resetMemoization, + TextUtils, useMemoization, + Value, } export default { @@ -58,18 +64,21 @@ export default { Document, History, Inline, + KeyUtils, Leaf, Mark, Node, Operation, Operations, + PathUtils, Range, + resetKeyGenerator, + resetMemoization, Schema, + setKeyGenerator, Stack, Text, - Value, - resetKeyGenerator, - setKeyGenerator, - resetMemoization, + TextUtils, useMemoization, + Value, } diff --git a/packages/slate/src/models/block.js b/packages/slate/src/models/block.js index aa6b28bc8..3fdc2b8e1 100644 --- a/packages/slate/src/models/block.js +++ b/packages/slate/src/models/block.js @@ -7,7 +7,7 @@ import logger from 'slate-dev-logger' import { List, Map, Record } from 'immutable' import MODEL_TYPES, { isType } from '../constants/model-types' -import generateKey from '../utils/generate-key' +import KeyUtils from '../utils/key-utils' /** * Default properties. @@ -88,7 +88,7 @@ class Block extends Record(DEFAULTS) { const { data = {}, isVoid = false, - key = generateKey(), + key = KeyUtils.create(), nodes = [], type, } = object diff --git a/packages/slate/src/models/change.js b/packages/slate/src/models/change.js index a7404de9d..9e909e7be 100644 --- a/packages/slate/src/models/change.js +++ b/packages/slate/src/models/change.js @@ -144,24 +144,18 @@ class Change { } /** - * Applies a series of change mutations and defers normalization until the end. + * Applies a series of change mutations, deferring normalization to the end. * - * @param {Function} customChange - function that accepts a change object and executes change operations + * @param {Function} fn * @return {Change} */ - withoutNormalization(customChange) { + withoutNormalization(fn) { const original = this.flags.normalize this.setOperationFlag('normalize', false) - - try { - customChange(this) - // if the change function worked then run normalization - this.normalizeDocument() - } finally { - // restore the flag to whatever it was - this.setOperationFlag('normalize', original) - } + fn(this) + this.setOperationFlag('normalize', original) + this.normalizeDocument() return this } diff --git a/packages/slate/src/models/document.js b/packages/slate/src/models/document.js index c1b04b321..e6b1eec5c 100644 --- a/packages/slate/src/models/document.js +++ b/packages/slate/src/models/document.js @@ -7,7 +7,7 @@ import logger from 'slate-dev-logger' import { List, Map, Record } from 'immutable' import MODEL_TYPES, { isType } from '../constants/model-types' -import generateKey from '../utils/generate-key' +import KeyUtils from '../utils/key-utils' /** * Default properties. @@ -65,7 +65,7 @@ class Document extends Record(DEFAULTS) { return object } - const { data = {}, key = generateKey(), nodes = [] } = object + const { data = {}, key = KeyUtils.create(), nodes = [] } = object const document = new Document({ key, diff --git a/packages/slate/src/models/inline.js b/packages/slate/src/models/inline.js index 988a41b66..e399dfa01 100644 --- a/packages/slate/src/models/inline.js +++ b/packages/slate/src/models/inline.js @@ -7,7 +7,7 @@ import logger from 'slate-dev-logger' import { List, Map, Record } from 'immutable' import MODEL_TYPES, { isType } from '../constants/model-types' -import generateKey from '../utils/generate-key' +import KeyUtils from '../utils/key-utils' /** * Default properties. @@ -88,7 +88,7 @@ class Inline extends Record(DEFAULTS) { const { data = {}, isVoid = false, - key = generateKey(), + key = KeyUtils.create(), nodes = [], type, } = object diff --git a/packages/slate/src/models/node.js b/packages/slate/src/models/node.js index e71b9fc88..f34bec36e 100644 --- a/packages/slate/src/models/node.js +++ b/packages/slate/src/models/node.js @@ -3,15 +3,16 @@ import isPlainObject from 'is-plain-object' import logger from 'slate-dev-logger' import { List, OrderedSet, Set } from 'immutable' -import Data from './data' import Block from './block' -import Inline from './inline' +import Data from './data' import Document from './document' -import { isType } from '../constants/model-types' +import Inline from './inline' +import KeyUtils from '../utils/key-utils' +import memoize from '../utils/memoize' +import PathUtils from '../utils/path-utils' import Range from './range' import Text from './text' -import generateKey from '../utils/generate-key' -import memoize from '../utils/memoize' +import { isType } from '../constants/model-types' /** * Node. @@ -187,95 +188,21 @@ class Node { } /** - * True if the node has both descendants in that order, false otherwise. The - * order is depth-first, post-order. + * Add mark to text at `offset` and `length` in node by `path`. * - * @param {String} first - * @param {String} second - * @return {Boolean} - */ - - areDescendantsSorted(first, second) { - first = assertKey(first) - second = assertKey(second) - - const keys = this.getKeysAsArray() - const firstIndex = keys.indexOf(first) - const secondIndex = keys.indexOf(second) - if (firstIndex == -1 || secondIndex == -1) return null - - return firstIndex < secondIndex - } - - /** - * Assert that a node has a child by `key` and return it. - * - * @param {String} key + * @param {List|String} path + * @param {Number} offset + * @param {Number} length + * @param {Mark} mark * @return {Node} */ - assertChild(key) { - const child = this.getChild(key) - - if (!child) { - key = assertKey(key) - throw new Error(`Could not find a child node with key "${key}".`) - } - - return child - } - - /** - * Assert that a node has a descendant by `key` and return it. - * - * @param {String} key - * @return {Node} - */ - - assertDescendant(key) { - const descendant = this.getDescendant(key) - - if (!descendant) { - key = assertKey(key) - throw new Error(`Could not find a descendant node with key "${key}".`) - } - - return descendant - } - - /** - * Assert that a node's tree has a node by `key` and return it. - * - * @param {String} key - * @return {Node} - */ - - assertNode(key) { - const node = this.getNode(key) - - if (!node) { - key = assertKey(key) - throw new Error(`Could not find a node with key "${key}".`) - } - - return node - } - - /** - * Assert that a node exists at `path` and return it. - * - * @param {Array} path - * @return {Node} - */ - - assertPath(path) { - const descendant = this.getDescendantAtPath(path) - - if (!descendant) { - throw new Error(`Could not find a descendant at path "${path}".`) - } - - return descendant + addMark(path, offset, length, mark) { + let node = this.assertDescendant(path) + path = this.resolvePath(path) + node = node.addMark(offset, length, mark) + const ret = this.replaceNode(path, node) + return ret } /** @@ -340,778 +267,6 @@ class Node { return ret } - /** - * Get the path of ancestors of a descendant node by `key`. - * - * @param {String|Node} key - * @return {List|Null} - */ - - getAncestors(key) { - key = assertKey(key) - - if (key == this.key) return List() - if (this.hasChild(key)) return List([this]) - - let ancestors - - this.nodes.find(node => { - if (node.object == 'text') return false - ancestors = node.getAncestors(key) - return ancestors - }) - - if (ancestors) { - return ancestors.unshift(this) - } else { - return null - } - } - - /** - * Get the leaf block descendants of the node. - * - * @return {List} - */ - - getBlocks() { - const array = this.getBlocksAsArray() - return new List(array) - } - - /** - * Get the leaf block descendants of the node. - * - * @return {List} - */ - - getBlocksAsArray() { - return this.nodes.reduce((array, child) => { - if (child.object != 'block') return array - if (!child.isLeafBlock()) return array.concat(child.getBlocksAsArray()) - array.push(child) - return array - }, []) - } - - /** - * Get the leaf block descendants in a `range`. - * - * @param {Range} range - * @return {List} - */ - - getBlocksAtRange(range) { - const array = this.getBlocksAtRangeAsArray(range) - // Eliminate duplicates by converting to an `OrderedSet` first. - return new List(new OrderedSet(array)) - } - - /** - * Get the leaf block descendants in a `range` as an array - * - * @param {Range} range - * @return {Array} - */ - - getBlocksAtRangeAsArray(range) { - range = range.normalize(this) - if (range.isUnset) return [] - - const { startKey, endKey } = range - const startBlock = this.getClosestBlock(startKey) - - // PERF: the most common case is when the range is in a single block node, - // where we can avoid a lot of iterating of the tree. - if (startKey == endKey) return [startBlock] - - const endBlock = this.getClosestBlock(endKey) - const blocks = this.getBlocksAsArray() - const start = blocks.indexOf(startBlock) - const end = blocks.indexOf(endBlock) - return blocks.slice(start, end + 1) - } - - /** - * Get all of the leaf blocks that match a `type`. - * - * @param {String} type - * @return {List} - */ - - getBlocksByType(type) { - const array = this.getBlocksByTypeAsArray(type) - return new List(array) - } - - /** - * Get all of the leaf blocks that match a `type` as an array - * - * @param {String} type - * @return {Array} - */ - - getBlocksByTypeAsArray(type) { - return this.nodes.reduce((array, node) => { - if (node.object != 'block') { - return array - } else if (node.isLeafBlock() && node.type == type) { - array.push(node) - return array - } else { - return array.concat(node.getBlocksByTypeAsArray(type)) - } - }, []) - } - - /** - * Get all of the characters for every text node. - * - * @return {List} - */ - - getCharacters() { - return this.getTexts().flatMap(t => t.characters) - } - - /** - * Get a list of the characters in a `range`. - * - * @param {Range} range - * @return {List} - */ - - getCharactersAtRange(range) { - range = range.normalize(this) - if (range.isUnset) return List() - const { startKey, endKey, startOffset, endOffset } = range - - if (startKey === endKey) { - const endText = this.getDescendant(endKey) - return endText.characters.slice(startOffset, endOffset) - } - - return this.getTextsAtRange(range).flatMap(t => { - if (t.key === startKey) { - return t.characters.slice(startOffset) - } - - if (t.key === endKey) { - return t.characters.slice(0, endOffset) - } - return t.characters - }) - } - - /** - * Get a child node by `key`. - * - * @param {String} key - * @return {Node|Null} - */ - - getChild(key) { - key = assertKey(key) - return this.nodes.find(node => node.key == key) - } - - /** - * Get closest parent of node by `key` that matches `iterator`. - * - * @param {String} key - * @param {Function} iterator - * @return {Node|Null} - */ - - getClosest(key, iterator) { - key = assertKey(key) - const ancestors = this.getAncestors(key) - - if (!ancestors) { - throw new Error(`Could not find a descendant node with key "${key}".`) - } - - // Exclude this node itself. - return ancestors.rest().findLast(iterator) - } - - /** - * Get the closest block parent of a `node`. - * - * @param {String} key - * @return {Node|Null} - */ - - getClosestBlock(key) { - return this.getClosest(key, parent => parent.object == 'block') - } - - /** - * Get the closest inline parent of a `node`. - * - * @param {String} key - * @return {Node|Null} - */ - - getClosestInline(key) { - return this.getClosest(key, parent => parent.object == 'inline') - } - - /** - * Get the closest void parent of a `node`. - * - * @param {String} key - * @return {Node|Null} - */ - - getClosestVoid(key) { - return this.getClosest(key, parent => parent.isVoid) - } - - /** - * Get the common ancestor of nodes `one` and `two` by keys. - * - * @param {String} one - * @param {String} two - * @return {Node} - */ - - getCommonAncestor(one, two) { - one = assertKey(one) - two = assertKey(two) - - if (one == this.key) return this - if (two == this.key) return this - - this.assertDescendant(one) - this.assertDescendant(two) - let ancestors = new List() - let oneParent = this.getParent(one) - let twoParent = this.getParent(two) - - while (oneParent) { - ancestors = ancestors.push(oneParent) - oneParent = this.getParent(oneParent.key) - } - - while (twoParent) { - if (ancestors.includes(twoParent)) return twoParent - twoParent = this.getParent(twoParent.key) - } - } - - /** - * Get the decorations for the node from a `stack`. - * - * @param {Stack} stack - * @return {List} - */ - - getDecorations(stack) { - const decorations = stack.find('decorateNode', this) - const list = Range.createList(decorations || []) - return list - } - - /** - * Get the depth of a child node by `key`, with optional `startAt`. - * - * @param {String} key - * @param {Number} startAt (optional) - * @return {Number} depth - */ - - getDepth(key, startAt = 1) { - this.assertDescendant(key) - if (this.hasChild(key)) return startAt - return this.getFurthestAncestor(key).getDepth(key, startAt + 1) - } - - /** - * Get a descendant node by `key`. - * - * @param {String} key - * @return {Node|Null} - */ - - getDescendant(key) { - key = assertKey(key) - let descendantFound = null - - const found = this.nodes.find(node => { - if (node.key === key) { - return node - } else if (node.object !== 'text') { - descendantFound = node.getDescendant(key) - return descendantFound - } else { - return false - } - }) - - return descendantFound || found - } - - /** - * Get a descendant by `path`. - * - * @param {Array} path - * @return {Node|Null} - */ - - getDescendantAtPath(path) { - let descendant = this - - for (const index of path) { - if (!descendant) return - if (!descendant.nodes) return - descendant = descendant.nodes.get(index) - } - - return descendant - } - - /** - * Get the first child text node. - * - * @return {Node|Null} - */ - - getFirstText() { - let descendantFound = null - - const found = this.nodes.find(node => { - if (node.object == 'text') return true - descendantFound = node.getFirstText() - return descendantFound - }) - - return descendantFound || found - } - - /** - * Get a fragment of the node at a `range`. - * - * @param {Range} range - * @return {Document} - */ - - getFragmentAtRange(range) { - range = range.normalize(this) - if (range.isUnset) return Document.create() - - let node = this - - // Make sure the children exist. - const { startKey, startOffset, endKey, endOffset } = range - const startText = node.assertDescendant(startKey) - const endText = node.assertDescendant(endKey) - - // Split at the start and end. - let child = startText - let previous - let parent - - while ((parent = node.getParent(child.key))) { - const index = parent.nodes.indexOf(child) - const position = - child.object == 'text' ? startOffset : child.nodes.indexOf(previous) - - parent = parent.splitNode(index, position) - node = node.updateNode(parent) - previous = parent.nodes.get(index + 1) - child = parent - } - - child = startKey == endKey ? node.getNextText(startKey) : endText - - while ((parent = node.getParent(child.key))) { - const index = parent.nodes.indexOf(child) - const position = - child.object == 'text' - ? startKey == endKey ? endOffset - startOffset : endOffset - : child.nodes.indexOf(previous) - - parent = parent.splitNode(index, position) - node = node.updateNode(parent) - previous = parent.nodes.get(index + 1) - child = parent - } - - // Get the start and end nodes. - const startNode = node.getNextSibling( - node.getFurthestAncestor(startKey).key - ) - const endNode = - startKey == endKey - ? node.getNextSibling( - node.getNextSibling(node.getFurthestAncestor(endKey).key).key - ) - : node.getNextSibling(node.getFurthestAncestor(endKey).key) - - // Get children range of nodes from start to end nodes - const startIndex = node.nodes.indexOf(startNode) - const endIndex = node.nodes.indexOf(endNode) - const nodes = node.nodes.slice(startIndex, endIndex) - - // Return a new document fragment. - return Document.create({ nodes }) - } - - /** - * Get the furthest parent of a node by `key` that matches an `iterator`. - * - * @param {String} key - * @param {Function} iterator - * @return {Node|Null} - */ - - getFurthest(key, iterator) { - const ancestors = this.getAncestors(key) - - if (!ancestors) { - key = assertKey(key) - throw new Error(`Could not find a descendant node with key "${key}".`) - } - - // Exclude this node itself - return ancestors.rest().find(iterator) - } - - /** - * Get the furthest block parent of a node by `key`. - * - * @param {String} key - * @return {Node|Null} - */ - - getFurthestBlock(key) { - return this.getFurthest(key, node => node.object == 'block') - } - - /** - * Get the furthest inline parent of a node by `key`. - * - * @param {String} key - * @return {Node|Null} - */ - - getFurthestInline(key) { - return this.getFurthest(key, node => node.object == 'inline') - } - - /** - * Get the furthest ancestor of a node by `key`. - * - * @param {String} key - * @return {Node|Null} - */ - - getFurthestAncestor(key) { - key = assertKey(key) - return this.nodes.find(node => { - if (node.key == key) return true - if (node.object == 'text') return false - return node.hasDescendant(key) - }) - } - - /** - * Get the furthest ancestor of a node by `key` that has only one child. - * - * @param {String} key - * @return {Node|Null} - */ - - getFurthestOnlyChildAncestor(key) { - const ancestors = this.getAncestors(key) - - if (!ancestors) { - key = assertKey(key) - throw new Error(`Could not find a descendant node with key "${key}".`) - } - - const result = ancestors - // Skip this node... - .shift() - // Take parents until there are more than one child... - .reverse() - .takeUntil(p => p.nodes.size > 1) - // And pick the highest. - .last() - if (!result) return null - return result - } - - /** - * Get the closest inline nodes for each text node in the node. - * - * @return {List} - */ - - getInlines() { - const array = this.getInlinesAsArray() - return new List(array) - } - - /** - * Get the closest inline nodes for each text node in the node, as an array. - * - * @return {List} - */ - - getInlinesAsArray() { - let array = [] - - this.nodes.forEach(child => { - if (child.object == 'text') return - - if (child.isLeafInline()) { - array.push(child) - } else { - array = array.concat(child.getInlinesAsArray()) - } - }) - - return array - } - - /** - * Get the closest inline nodes for each text node in a `range`. - * - * @param {Range} range - * @return {List} - */ - - getInlinesAtRange(range) { - const array = this.getInlinesAtRangeAsArray(range) - // Remove duplicates by converting it to an `OrderedSet` first. - return new List(new OrderedSet(array)) - } - - /** - * Get the closest inline nodes for each text node in a `range` as an array. - * - * @param {Range} range - * @return {Array} - */ - - getInlinesAtRangeAsArray(range) { - range = range.normalize(this) - if (range.isUnset) return [] - - return this.getTextsAtRangeAsArray(range) - .map(text => this.getClosestInline(text.key)) - .filter(exists => exists) - } - - /** - * Get all of the leaf inline nodes that match a `type`. - * - * @param {String} type - * @return {List} - */ - - getInlinesByType(type) { - const array = this.getInlinesByTypeAsArray(type) - return new List(array) - } - - /** - * Get all of the leaf inline nodes that match a `type` as an array. - * - * @param {String} type - * @return {Array} - */ - - getInlinesByTypeAsArray(type) { - return this.nodes.reduce((inlines, node) => { - if (node.object == 'text') { - return inlines - } else if (node.isLeafInline() && node.type == type) { - inlines.push(node) - return inlines - } else { - return inlines.concat(node.getInlinesByTypeAsArray(type)) - } - }, []) - } - - /** - * Return a set of all keys in the node as an array. - * - * @return {Array} - */ - - getKeysAsArray() { - const keys = [] - - this.forEachDescendant(desc => { - keys.push(desc.key) - }) - - return keys - } - - /** - * Return a set of all keys in the node. - * - * @return {Set} - */ - - getKeys() { - const keys = this.getKeysAsArray() - return new Set(keys) - } - - /** - * Get the last child text node. - * - * @return {Node|Null} - */ - - getLastText() { - let descendantFound = null - - const found = this.nodes.findLast(node => { - if (node.object == 'text') return true - descendantFound = node.getLastText() - return descendantFound - }) - - return descendantFound || found - } - - /** - * Get all of the marks for all of the characters of every text node. - * - * @return {Set} - */ - - getMarks() { - const array = this.getMarksAsArray() - return new Set(array) - } - - /** - * Get all of the marks for all of the characters of every text node. - * - * @return {OrderedSet} - */ - - getOrderedMarks() { - const array = this.getMarksAsArray() - return new OrderedSet(array) - } - - /** - * Get all of the marks as an array. - * - * @return {Array} - */ - - getMarksAsArray() { - // PERF: use only one concat rather than multiple concat - // becuase one concat is faster - const result = [] - - this.nodes.forEach(node => { - result.push(node.getMarksAsArray()) - }) - return Array.prototype.concat.apply([], result) - } - - /** - * Get a set of the marks in a `range`. - * - * @param {Range} range - * @return {Set} - */ - - getMarksAtRange(range) { - return new Set(this.getOrderedMarksAtRange(range)) - } - - /** - * Get a set of the marks in a `range`. - * - * @param {Range} range - * @return {Set} - */ - - getInsertMarksAtRange(range) { - range = range.normalize(this) - if (range.isUnset) return Set() - - if (range.isCollapsed) { - // PERF: range is not cachable, use key and offset as proxies for cache - return this.getMarksAtPosition(range.startKey, range.startOffset) - } - - const { startKey, startOffset } = range - const text = this.getDescendant(startKey) - return text.getMarksAtIndex(startOffset + 1) - } - - /** - * Get a set of the marks in a `range`. - * - * @param {Range} range - * @return {OrderedSet} - */ - - getOrderedMarksAtRange(range) { - range = range.normalize(this) - if (range.isUnset) return OrderedSet() - - if (range.isCollapsed) { - // PERF: range is not cachable, use key and offset as proxies for cache - return this.getMarksAtPosition(range.startKey, range.startOffset) - } - - const { startKey, startOffset, endKey, endOffset } = range - return this.getOrderedMarksBetweenPositions( - startKey, - startOffset, - endKey, - endOffset - ) - } - - /** - * Get a set of the marks in a `range`. - * PERF: arguments use key and offset for utilizing cache - * - * @param {string} startKey - * @param {number} startOffset - * @param {string} endKey - * @param {number} endOffset - * @returns {OrderedSet} - */ - - getOrderedMarksBetweenPositions(startKey, startOffset, endKey, endOffset) { - if (startKey === endKey) { - const startText = this.getDescendant(startKey) - return startText.getMarksBetweenOffsets(startOffset, endOffset) - } - - const texts = this.getTextsBetweenPositionsAsArray(startKey, endKey) - - return OrderedSet().withMutations(result => { - texts.forEach(text => { - if (text.key === startKey) { - result.union( - text.getMarksBetweenOffsets(startOffset, text.text.length) - ) - } else if (text.key === endKey) { - result.union(text.getMarksBetweenOffsets(0, endOffset)) - } else { - result.union(text.getMarks()) - } - }) - }) - } - /** * Get a set of the active marks in a `range`. * @@ -1173,6 +328,688 @@ class Node { return marks } + /** + * Get a list of the ancestors of a descendant. + * + * @param {List|String} path + * @return {List|Null} + */ + + getAncestors(path) { + path = this.resolvePath(path) + if (!path) return null + + const ancestors = [] + + path.forEach((p, i) => { + const current = path.slice(0, i) + const parent = this.getNode(current) + ancestors.push(parent) + }) + + return List(ancestors) + } + + /** + * Get the leaf block descendants of the node. + * + * @return {List} + */ + + getBlocks() { + const array = this.getBlocksAsArray() + return new List(array) + } + + /** + * Get the leaf block descendants of the node. + * + * @return {List} + */ + + getBlocksAsArray() { + return this.nodes.reduce((array, child) => { + if (child.object != 'block') return array + if (!child.isLeafBlock()) return array.concat(child.getBlocksAsArray()) + array.push(child) + return array + }, []) + } + + /** + * Get the leaf block descendants in a `range`. + * + * @param {Range} range + * @return {List} + */ + + getBlocksAtRange(range) { + const array = this.getBlocksAtRangeAsArray(range) + // Eliminate duplicates by converting to an `OrderedSet` first. + return new List(new OrderedSet(array)) + } + + /** + * Get the leaf block descendants in a `range` as an array + * + * @param {Range} range + * @return {Array} + */ + + getBlocksAtRangeAsArray(range) { + range = range.normalize(this) + if (range.isUnset) return [] + + const { startKey, endKey } = range + const startBlock = this.getClosestBlock(startKey) + + // PERF: the most common case is when the range is in a single block node, + // where we can avoid a lot of iterating of the tree. + if (startKey === endKey) return [startBlock] + + const endBlock = this.getClosestBlock(endKey) + const blocks = this.getBlocksAsArray() + const start = blocks.indexOf(startBlock) + const end = blocks.indexOf(endBlock) + return blocks.slice(start, end + 1) + } + + /** + * Get all of the leaf blocks that match a `type`. + * + * @param {String} type + * @return {List} + */ + + getBlocksByType(type) { + const array = this.getBlocksByTypeAsArray(type) + return new List(array) + } + + /** + * Get all of the leaf blocks that match a `type` as an array + * + * @param {String} type + * @return {Array} + */ + + getBlocksByTypeAsArray(type) { + return this.nodes.reduce((array, node) => { + if (node.object != 'block') { + return array + } else if (node.isLeafBlock() && node.type == type) { + array.push(node) + return array + } else { + return array.concat(node.getBlocksByTypeAsArray(type)) + } + }, []) + } + + /** + * Get all of the characters for every text node. + * + * @return {List} + */ + + getCharacters() { + const characters = this.getTexts().flatMap(t => t.characters) + return characters + } + + /** + * Get a list of the characters in a `range`. + * + * @param {Range} range + * @return {List} + */ + + getCharactersAtRange(range) { + range = range.normalize(this) + if (range.isUnset) return List() + const { startKey, endKey, startOffset, endOffset } = range + + if (startKey === endKey) { + const endText = this.getDescendant(endKey) + return endText.characters.slice(startOffset, endOffset) + } + + return this.getTextsAtRange(range).flatMap(t => { + if (t.key === startKey) { + return t.characters.slice(startOffset) + } + + if (t.key === endKey) { + return t.characters.slice(0, endOffset) + } + return t.characters + }) + } + + /** + * Get a child node. + * + * @param {List|String} path + * @return {Node|Null} + */ + + getChild(path) { + path = this.resolvePath(path) + if (!path) return null + const child = path.size === 1 ? this.nodes.get(path.first()) : null + return child + } + + /** + * Get closest parent of node that matches an `iterator`. + * + * @param {List|String} path + * @param {Function} iterator + * @return {Node|Null} + */ + + getClosest(path, iterator) { + const ancestors = this.getAncestors(path) + if (!ancestors) return null + + const closest = ancestors.findLast((node, ...args) => { + // We never want to include the top-level node. + if (node === this) return false + return iterator(node, ...args) + }) + + return closest || null + } + + /** + * Get the closest block parent of a node. + * + * @param {List|String} path + * @return {Node|Null} + */ + + getClosestBlock(path) { + const closest = this.getClosest(path, n => n.object === 'block') + return closest + } + + /** + * Get the closest inline parent of a node by `path`. + * + * @param {List|String} path + * @return {Node|Null} + */ + + getClosestInline(path) { + const closest = this.getClosest(path, n => n.object === 'inline') + return closest + } + + /** + * Get the closest void parent of a node by `path`. + * + * @param {List|String} path + * @return {Node|Null} + */ + + getClosestVoid(path) { + const closest = this.getClosest(path, p => p.isVoid) + return closest + } + + /** + * Get the common ancestor of nodes `a` and `b`. + * + * @param {List} a + * @param {List} b + * @return {Node} + */ + + getCommonAncestor(a, b) { + a = this.resolvePath(a) + b = this.resolvePath(b) + if (!a || !b) return null + + const path = PathUtils.relate(a, b) + const node = this.getNode(path) + return node + } + + /** + * Get the decorations for the node from a `stack`. + * + * @param {Stack} stack + * @return {List} + */ + + getDecorations(stack) { + const decorations = stack.find('decorateNode', this) + const list = Range.createList(decorations || []) + return list + } + + /** + * Get the depth of a descendant, with optional `startAt`. + * + * @param {List|String} path + * @param {Number} startAt + * @return {Number|Null} + */ + + getDepth(path, startAt = 1) { + path = this.resolvePath(path) + if (!path) return null + + const node = this.getNode(path) + const depth = node ? path.size - 1 + startAt : null + return depth + } + + /** + * Get a descendant node. + * + * @param {List|String} path + * @return {Node|Null} + */ + + getDescendant(path) { + path = this.resolvePath(path) + if (!path) return null + + const array = path.toArray() + let descendant = this + + for (const index of array) { + if (!descendant) return null + if (!descendant.nodes) return null + descendant = descendant.nodes.get(index) + } + + return descendant + } + + /** + * Get the first invalid descendant + * + * @param {Schema} schema + * @return {Node|Text|Null} + */ + + getFirstInvalidDescendant(schema) { + let result = null + + this.nodes.find(n => { + result = n.validate(schema) ? n : n.getFirstInvalidDescendant(schema) + return result + }) + + return result + } + + /** + * Get the first child text node. + * + * @return {Node|Null} + */ + + getFirstText() { + let descendant = null + + const found = this.nodes.find(node => { + if (node.object === 'text') return true + descendant = node.getFirstText() + return !!descendant + }) + + return descendant || found + } + + /** + * Get a fragment of the node at a `range`. + * + * @param {Range} range + * @return {Document} + */ + + getFragmentAtRange(range) { + range = range.normalize(this) + if (range.isUnset) return Document.create() + + let node = this + + // Make sure the children exist. + const { startKey, startOffset, endKey, endOffset } = range + const startText = node.assertDescendant(startKey) + const endText = node.assertDescendant(endKey) + + // Split at the start and end. + let child = startText + let previous + let parent + + while ((parent = node.getParent(child.key))) { + const index = parent.nodes.indexOf(child) + const position = + child.object == 'text' ? startOffset : child.nodes.indexOf(previous) + + parent = parent.splitNode(index, position) + node = node.replaceNode(parent.key, parent) + previous = parent.nodes.get(index + 1) + child = parent + } + + child = startKey == endKey ? node.getNextText(startKey) : endText + + while ((parent = node.getParent(child.key))) { + const index = parent.nodes.indexOf(child) + const position = + child.object == 'text' + ? startKey == endKey ? endOffset - startOffset : endOffset + : child.nodes.indexOf(previous) + + parent = parent.splitNode(index, position) + node = node.replaceNode(parent.key, parent) + previous = parent.nodes.get(index + 1) + child = parent + } + + // Get the start and end nodes. + const startNode = node.getNextSibling( + node.getFurthestAncestor(startKey).key + ) + const endNode = + startKey == endKey + ? node.getNextSibling( + node.getNextSibling(node.getFurthestAncestor(endKey).key).key + ) + : node.getNextSibling(node.getFurthestAncestor(endKey).key) + + // Get children range of nodes from start to end nodes + const startIndex = node.nodes.indexOf(startNode) + const endIndex = node.nodes.indexOf(endNode) + const nodes = node.nodes.slice(startIndex, endIndex) + + // Return a new document fragment. + return Document.create({ nodes }) + } + + /** + * Get the furthest parent of a node that matches an `iterator`. + * + * @param {Path} path + * @param {Function} iterator + * @return {Node|Null} + */ + + getFurthest(path, iterator) { + const ancestors = this.getAncestors(path) + if (!ancestors) return null + + const furthest = ancestors.find((node, ...args) => { + // We never want to include the top-level node. + if (node === this) return false + return iterator(node, ...args) + }) + + return furthest || null + } + + /** + * Get the furthest ancestor of a node. + * + * @param {Path} path + * @return {Node|Null} + */ + + getFurthestAncestor(path) { + path = this.resolvePath(path) + if (!path) return null + const furthest = path.size ? this.nodes.get(path.first()) : null + return furthest + } + + /** + * Get the furthest block parent of a node. + * + * @param {Path} path + * @return {Node|Null} + */ + + getFurthestBlock(path) { + const furthest = this.getFurthest(path, n => n.object === 'block') + return furthest + } + + /** + * Get the furthest inline parent of a node. + * + * @param {Path} path + * @return {Node|Null} + */ + + getFurthestInline(path) { + const furthest = this.getFurthest(path, n => n.object === 'inline') + return furthest + } + + /** + * Get the furthest ancestor of a node that has only one child. + * + * @param {Path} path + * @return {Node|Null} + */ + + getFurthestOnlyChildAncestor(path) { + const ancestors = this.getAncestors(path) + if (!ancestors) return null + + const furthest = ancestors + .rest() + .reverse() + .takeUntil(p => p.nodes.size > 1) + .last() + + return furthest || null + } + + /** + * Get the closest inline nodes for each text node in the node. + * + * @return {List} + */ + + getInlines() { + const array = this.getInlinesAsArray() + const list = new List(array) + return list + } + + /** + * Get the closest inline nodes for each text node in the node, as an array. + * + * @return {List} + */ + + getInlinesAsArray() { + let array = [] + + this.nodes.forEach(child => { + if (child.object == 'text') return + + if (child.isLeafInline()) { + array.push(child) + } else { + array = array.concat(child.getInlinesAsArray()) + } + }) + + return array + } + + /** + * Get the closest inline nodes for each text node in a `range`. + * + * @param {Range} range + * @return {List} + */ + + getInlinesAtRange(range) { + const array = this.getInlinesAtRangeAsArray(range) + // Remove duplicates by converting it to an `OrderedSet` first. + const list = new List(new OrderedSet(array)) + return list + } + + /** + * Get the closest inline nodes for each text node in a `range` as an array. + * + * @param {Range} range + * @return {Array} + */ + + getInlinesAtRangeAsArray(range) { + range = range.normalize(this) + if (range.isUnset) return [] + + const array = this.getTextsAtRangeAsArray(range) + .map(text => this.getClosestInline(text.key)) + .filter(exists => exists) + + return array + } + + /** + * Get all of the leaf inline nodes that match a `type`. + * + * @param {String} type + * @return {List} + */ + + getInlinesByType(type) { + const array = this.getInlinesByTypeAsArray(type) + const list = new List(array) + return list + } + + /** + * Get all of the leaf inline nodes that match a `type` as an array. + * + * @param {String} type + * @return {Array} + */ + + getInlinesByTypeAsArray(type) { + const array = this.nodes.reduce((inlines, node) => { + if (node.object == 'text') { + return inlines + } else if (node.isLeafInline() && node.type == type) { + inlines.push(node) + return inlines + } else { + return inlines.concat(node.getInlinesByTypeAsArray(type)) + } + }, []) + + return array + } + + /** + * Get a set of the marks in a `range`. + * + * @param {Range} range + * @return {Set} + */ + + getInsertMarksAtRange(range) { + range = range.normalize(this) + if (range.isUnset) return Set() + + if (range.isCollapsed) { + // PERF: range is not cachable, use key and offset as proxies for cache + return this.getMarksAtPosition(range.startKey, range.startOffset) + } + + const { startKey, startOffset } = range + const text = this.getDescendant(startKey) + const marks = text.getMarksAtIndex(startOffset + 1) + return marks + } + + /** + * Get an object mapping all the keys in the node to their paths. + * + * @return {Object} + */ + + getKeysToPathsTable() { + const ret = { + [this.key]: [], + } + + this.nodes.forEach((node, i) => { + ret[node.key] = [i] + + if (node.object !== 'text') { + const nested = node.getKeysToPathsTable() + + for (const key in nested) { + const path = nested[key] + ret[key] = [i, ...path] + } + } + }) + + return ret + } + + /** + * Get the last child text node. + * + * @return {Node|Null} + */ + + getLastText() { + let descendant = null + + const found = this.nodes.findLast(node => { + if (node.object == 'text') return true + descendant = node.getLastText() + return descendant + }) + + return descendant || found + } + + /** + * Get all of the marks for all of the characters of every text node. + * + * @return {Set} + */ + + getMarks() { + const array = this.getMarksAsArray() + const set = new Set(array) + return set + } + + /** + * Get all of the marks as an array. + * + * @return {Array} + */ + + getMarksAsArray() { + const result = [] + + this.nodes.forEach(node => { + result.push(node.getMarksAsArray()) + }) + + // PERF: use only one concat rather than multiple for speed. + const array = [].concat(...result) + return array + } + /** * Get a set of marks in a `position`, the equivalent of a collapsed range * @@ -1202,6 +1039,18 @@ class Node { return currentMarks } + /** + * Get a set of the marks in a `range`. + * + * @param {Range} range + * @return {Set} + */ + + getMarksAtRange(range) { + const marks = new Set(this.getOrderedMarksAtRange(range)) + return marks + } + /** * Get all of the marks that match a `type`. * @@ -1211,19 +1060,8 @@ class Node { getMarksByType(type) { const array = this.getMarksByTypeAsArray(type) - return new Set(array) - } - - /** - * Get all of the marks that match a `type`. - * - * @param {String} type - * @return {OrderedSet} - */ - - getOrderedMarksByType(type) { - const array = this.getMarksByTypeAsArray(type) - return new OrderedSet(array) + const set = new Set(array) + return set } /** @@ -1234,11 +1072,13 @@ class Node { */ getMarksByTypeAsArray(type) { - return this.nodes.reduce((array, node) => { + const array = this.nodes.reduce((memo, node) => { return node.object == 'text' - ? array.concat(node.getMarksAsArray().filter(m => m.type == type)) - : array.concat(node.getMarksByTypeAsArray(type)) + ? memo.concat(node.getMarksAsArray().filter(m => m.type == type)) + : memo.concat(node.getMarksByTypeAsArray(type)) }, []) + + return array } /** @@ -1262,63 +1102,80 @@ class Node { const next = this.getNextText(last.key) if (!next) return null - return this.getClosestBlock(next.key) + const closest = this.getClosestBlock(next.key) + return closest } /** - * Get the node after a descendant by `key`. + * Get the next node in the tree from a node. * - * @param {String} key + * This will not only check for siblings but instead move up the tree + * returning the next ancestor if no sibling is found. + * + * @param {List|String} path * @return {Node|Null} */ - getNextSibling(key) { - key = assertKey(key) + getNextNode(path) { + path = this.resolvePath(path) + if (!path) return null + if (!path.size) return null - const parent = this.getParent(key) - const after = parent.nodes.skipUntil(child => child.key == key) - - if (after.size == 0) { - throw new Error(`Could not find a child node with key "${key}".`) + for (let i = path.size; i > 0; i--) { + const p = path.slice(0, i) + const target = PathUtils.increment(p) + const node = this.getNode(target) + if (node) return node } - return after.get(1) + + return null } /** - * Get the text node after a descendant text node by `key`. + * Get the next sibling of a node. * - * @param {String} key + * @param {List|String} path * @return {Node|Null} */ - getNextText(key) { - key = assertKey(key) - return this.getTexts() - .skipUntil(text => text.key == key) - .get(1) + getNextSibling(path) { + path = this.resolvePath(path) + if (!path) return null + if (!path.size) return null + const p = PathUtils.increment(path) + const sibling = this.getNode(p) + return sibling } /** - * Get a node in the tree by `key`. + * Get the text node after a descendant text node. * - * @param {String} key + * @param {List|String} path * @return {Node|Null} */ - getNode(key) { - key = assertKey(key) - return this.key == key ? this : this.getDescendant(key) + getNextText(path) { + path = this.resolvePath(path) + if (!path) return null + if (!path.size) return null + const next = this.getNextNode(path) + if (!next) return null + const text = next.getFirstText() + return text } /** - * Get a node in the tree by `path`. + * Get a node in the tree. * - * @param {Array} path + * @param {List|String} path * @return {Node|Null} */ - getNodeAtPath(path) { - return path.length ? this.getDescendantAtPath(path) : this + getNode(path) { + path = this.resolvePath(path) + if (!path) return null + const node = path.size ? this.getDescendant(path) : this + return node } /** @@ -1338,7 +1195,8 @@ class Node { .reduce((memo, n) => memo + n.text.length, 0) // Recurse if need be. - return this.hasChild(key) ? offset : offset + child.getOffset(key) + const ret = this.hasChild(key) ? offset : offset + child.getOffset(key) + return ret } /** @@ -1360,100 +1218,126 @@ class Node { } const { startKey, startOffset } = range - return this.getOffset(startKey) + startOffset + const offset = this.getOffset(startKey) + startOffset + return offset } /** - * Get the parent of a child node by `key`. + * Get all of the marks for all of the characters of every text node. * - * @param {String} key + * @return {OrderedSet} + */ + + getOrderedMarks() { + const array = this.getMarksAsArray() + const set = new OrderedSet(array) + return set + } + + /** + * Get a set of the marks in a `range`. + * + * @param {Range} range + * @return {OrderedSet} + */ + + getOrderedMarksAtRange(range) { + range = range.normalize(this) + if (range.isUnset) return OrderedSet() + + if (range.isCollapsed) { + // PERF: range is not cachable, use key and offset as proxies for cache + return this.getMarksAtPosition(range.startKey, range.startOffset) + } + + const { startKey, startOffset, endKey, endOffset } = range + const marks = this.getOrderedMarksBetweenPositions( + startKey, + startOffset, + endKey, + endOffset + ) + + return marks + } + + /** + * Get a set of the marks in a `range`. + * PERF: arguments use key and offset for utilizing cache + * + * @param {string} startKey + * @param {number} startOffset + * @param {string} endKey + * @param {number} endOffset + * @returns {OrderedSet} + */ + + getOrderedMarksBetweenPositions(startKey, startOffset, endKey, endOffset) { + if (startKey === endKey) { + const startText = this.getDescendant(startKey) + return startText.getMarksBetweenOffsets(startOffset, endOffset) + } + + const texts = this.getTextsBetweenPositionsAsArray(startKey, endKey) + + return OrderedSet().withMutations(result => { + texts.forEach(text => { + if (text.key === startKey) { + result.union( + text.getMarksBetweenOffsets(startOffset, text.text.length) + ) + } else if (text.key === endKey) { + result.union(text.getMarksBetweenOffsets(0, endOffset)) + } else { + result.union(text.getMarks()) + } + }) + }) + } + + /** + * Get all of the marks that match a `type`. + * + * @param {String} type + * @return {OrderedSet} + */ + + getOrderedMarksByType(type) { + const array = this.getMarksByTypeAsArray(type) + const set = new OrderedSet(array) + return set + } + + /** + * Get the parent of a descendant node. + * + * @param {List|String} path * @return {Node|Null} */ - getParent(key) { - if (this.hasChild(key)) return this - - let node = null - - this.nodes.find(child => { - if (child.object == 'text') { - return false - } else { - node = child.getParent(key) - return node - } - }) - - return node + getParent(path) { + path = this.resolvePath(path) + if (!path) return null + if (!path.size) return null + const parentPath = PathUtils.lift(path) + const parent = this.getNode(parentPath) + return parent } /** - * Get the path of a descendant node by `key`. + * Find the path to a node. * - * @param {String|Node} key - * @return {Array} + * @param {String|List} key + * @return {List} */ getPath(key) { - let child = this.assertNode(key) - const ancestors = this.getAncestors(key) - const path = [] + // Handle the case of passing in a path directly, to match other methods. + if (List.isList(key)) return key - ancestors.reverse().forEach(ancestor => { - const index = ancestor.nodes.indexOf(child) - path.unshift(index) - child = ancestor - }) - - return path - } - - /** - * Refind the path of node if path is changed. - * - * @param {Array} path - * @param {String} key - * @return {Array} - */ - - refindPath(path, key) { - const node = this.getDescendantAtPath(path) - - if (node && node.key === key) { - return path - } - - return this.getPath(key) - } - - /** - * - * Refind the node with the same node.key after change. - * - * @param {Array} path - * @param {String} key - * @return {Node|Void} - */ - - refindNode(path, key) { - const node = this.getDescendantAtPath(path) - - if (node && node.key === key) { - return node - } - - return this.getDescendant(key) - } - - /** - * Get the placeholder for the node from a `schema`. - * - * @param {Schema} schema - * @return {Component|Void} - */ - - getPlaceholder(schema) { - return schema.__getPlaceholder(this) + const dict = this.getKeysToPathsTable() + const path = dict[key] + return path ? List(path) : null } /** @@ -1477,40 +1361,69 @@ class Node { const previous = this.getPreviousText(first.key) if (!previous) return null - return this.getClosestBlock(previous.key) + const closest = this.getClosestBlock(previous.key) + return closest } /** - * Get the node before a descendant node by `key`. + * Get the previous node from a node in the tree. * - * @param {String} key + * This will not only check for siblings but instead move up the tree + * returning the previous ancestor if no sibling is found. + * + * @param {List|String} path * @return {Node|Null} */ - getPreviousSibling(key) { - key = assertKey(key) - const parent = this.getParent(key) - const before = parent.nodes.takeUntil(child => child.key == key) + getPreviousNode(path) { + path = this.resolvePath(path) + if (!path) return null + if (!path.size) return null - if (before.size == parent.nodes.size) { - throw new Error(`Could not find a child node with key "${key}".`) + for (let i = path.size; i > 0; i--) { + const p = path.slice(0, i) + if (p.last() === 0) continue + + const target = PathUtils.decrement(p) + const node = this.getNode(target) + if (node) return node } - return before.last() + return null } /** - * Get the text node before a descendant text node by `key`. + * Get the previous sibling of a node. * - * @param {String} key + * @param {List|String} path * @return {Node|Null} */ - getPreviousText(key) { - key = assertKey(key) - return this.getTexts() - .takeUntil(text => text.key == key) - .last() + getPreviousSibling(path) { + path = this.resolvePath(path) + if (!path) return null + if (!path.size) return null + if (path.last() === 0) return null + const p = PathUtils.decrement(path) + const sibling = this.getNode(p) + return sibling + } + + /** + * Get the text node after a descendant text node. + * + * @param {List|String} path + * @return {Node|Null} + */ + + getPreviousText(path) { + path = this.resolvePath(path) + if (!path) return null + if (!path.size) return null + const previous = this.getPreviousNode(path) + if (!previous) return null + const text = previous.getLastText() + return text } /** @@ -1573,9 +1486,11 @@ class Node { */ getText() { - return this.nodes.reduce((string, node) => { + const text = this.nodes.reduce((string, node) => { return string + node.text }, '') + + return text } /** @@ -1587,16 +1502,17 @@ class Node { getTextAtOffset(offset) { // PERF: Add a few shortcuts for the obvious cases. - if (offset == 0) return this.getFirstText() - if (offset == this.text.length) return this.getLastText() + if (offset === 0) return this.getFirstText() + if (offset === this.text.length) return this.getLastText() if (offset < 0 || offset > this.text.length) return null let length = 0 - - return this.getTexts().find((node, i, nodes) => { + const text = this.getTexts().find((node, i, nodes) => { length += node.text.length return length > offset }) + + return text } /** @@ -1607,7 +1523,7 @@ class Node { getTextDirection() { const dir = direction(this.text) - return dir == 'neutral' ? undefined : dir + return dir === 'neutral' ? null : dir } /** @@ -1618,7 +1534,8 @@ class Node { getTexts() { const array = this.getTextsAsArray() - return new List(array) + const list = new List(array) + return list } /** @@ -1652,7 +1569,26 @@ class Node { range = range.normalize(this) if (range.isUnset) return List() const { startKey, endKey } = range - return new List(this.getTextsBetweenPositionsAsArray(startKey, endKey)) + const list = new List( + this.getTextsBetweenPositionsAsArray(startKey, endKey) + ) + + return list + } + + /** + * Get all of the text nodes in a `range` as an array. + * + * @param {Range} range + * @return {Array} + */ + + getTextsAtRangeAsArray(range) { + range = range.normalize(this) + if (range.isUnset) return [] + const { startKey, endKey } = range + const texts = this.getTextsBetweenPositionsAsArray(startKey, endKey) + return texts } /** @@ -1675,126 +1611,551 @@ class Node { const texts = this.getTextsAsArray() const start = texts.indexOf(startText) const end = texts.indexOf(endText, start) - return texts.slice(start, end + 1) + const ret = texts.slice(start, end + 1) + return ret } /** - * Get all of the text nodes in a `range` as an array. + * Check if the node has block children. * - * @param {Range} range - * @return {Array} - */ - - getTextsAtRangeAsArray(range) { - range = range.normalize(this) - if (range.isUnset) return [] - const { startKey, endKey } = range - return this.getTextsBetweenPositionsAsArray(startKey, endKey) - } - - /** - * Check if a child node exists by `key`. - * - * @param {String} key * @return {Boolean} */ - hasChild(key) { - return !!this.getChild(key) + hasBlockChildren() { + return !!(this.nodes && this.nodes.find(n => n.object === 'block')) } /** - * Check if a node has block node children. + * Check if a child node exists. * - * @param {String} key + * @param {List|String} path * @return {Boolean} */ - hasBlocks(key) { - const node = this.assertNode(key) - return !!(node.nodes && node.nodes.find(n => n.object === 'block')) + hasChild(path) { + const child = this.getChild(path) + return !!child } /** - * Check if a node has inline node children. + * Check if a node has inline children. * - * @param {String} key * @return {Boolean} */ - hasInlines(key) { - const node = this.assertNode(key) + hasInlineChildren() { return !!( - node.nodes && node.nodes.find(n => Inline.isInline(n) || Text.isText(n)) + this.nodes && + this.nodes.find(n => n.object === 'inline' || n.object === 'text') ) } /** - * Recursively check if a child node exists by `key`. + * Recursively check if a child node exists. * - * @param {String} key + * @param {List|String} path * @return {Boolean} */ - hasDescendant(key) { - return !!this.getDescendant(key) + hasDescendant(path) { + const descendant = this.getDescendant(path) + return !!descendant } /** - * Recursively check if a node exists by `key`. + * Recursively check if a node exists. * - * @param {String} key + * @param {List|String} path * @return {Boolean} */ - hasNode(key) { - return !!this.getNode(key) + hasNode(path) { + const node = this.getNode(path) + return !!node } /** - * Check if a node has a void parent by `key`. + * Check if a node has a void parent. * - * @param {String} key + * @param {List|String} path * @return {Boolean} */ - hasVoidParent(key) { - return !!this.getClosestVoid(key) + hasVoidParent(path) { + const closest = this.getClosestVoid(path) + return !!closest } /** - * Insert a `node` at `index`. + * Insert a `node`. * - * @param {Number} index + * @param {List|String} path * @param {Node} node * @return {Node} */ - insertNode(index, node) { - const keys = this.getKeysAsArray() - - if (keys.includes(node.key)) { - node = node.regenerateKey() - } - - if (node.object != 'text') { - node = node.mapDescendants(desc => { - return keys.includes(desc.key) ? desc.regenerateKey() : desc - }) - } - - const nodes = this.nodes.insert(index, node) - return this.set('nodes', nodes) + insertNode(path, node) { + path = this.resolvePath(path) + const index = path.last() + const parentPath = PathUtils.lift(path) + let parent = this.assertNode(parentPath) + const nodes = parent.nodes.splice(index, 0, node) + parent = parent.set('nodes', nodes) + const ret = this.replaceNode(parentPath, parent) + return ret } /** - * Check whether the node is in a `range`. + * Insert `text` at `offset` in node by `path`. + * + * @param {List|String} path + * @param {Number} offset + * @param {String} text + * @param {Set} marks + * @return {Node} + */ + + insertText(path, offset, text, marks) { + let node = this.assertDescendant(path) + path = this.resolvePath(path) + node = node.insertText(offset, text, marks) + const ret = this.replaceNode(path, node) + return ret + } + + /** + * Check whether the node is a leaf block. * - * @param {Range} range * @return {Boolean} */ + isLeafBlock() { + return ( + this.object === 'block' && this.nodes.every(n => n.object !== 'block') + ) + } + + /** + * Check whether the node is a leaf inline. + * + * @return {Boolean} + */ + + isLeafInline() { + return ( + this.object === 'inline' && this.nodes.every(n => n.object !== 'inline') + ) + } + + /** + * Map all child nodes, updating them in their parents. This method is + * optimized to not return a new node if no changes are made. + * + * @param {Function} iterator + * @return {Node} + */ + + mapChildren(iterator) { + let { nodes } = this + + nodes.forEach((node, i) => { + const ret = iterator(node, i, this.nodes) + if (ret !== node) nodes = nodes.set(ret.key, ret) + }) + + const ret = this.set('nodes', nodes) + return ret + } + + /** + * Map all descendant nodes, updating them in their parents. This method is + * optimized to not return a new node if no changes are made. + * + * @param {Function} iterator + * @return {Node} + */ + + mapDescendants(iterator) { + let { nodes } = this + + nodes.forEach((node, index) => { + let ret = node + if (ret.object !== 'text') ret = ret.mapDescendants(iterator) + ret = iterator(ret, index, this.nodes) + if (ret === node) return + + nodes = nodes.set(index, ret) + }) + + const ret = this.set('nodes', nodes) + return ret + } + + /** + * Merge a node backwards its previous sibling. + * + * @param {List|Key} path + * @return {Node} + */ + + mergeNode(path) { + const b = this.assertNode(path) + path = this.resolvePath(path) + + if (path.last() === 0) { + throw new Error( + `Unable to merge node because it has no previous sibling: ${b}` + ) + } + + const withPath = PathUtils.decrement(path) + const a = this.assertNode(withPath) + + if (a.object !== b.object) { + throw new Error( + `Unable to merge two different kinds of nodes: ${a} and ${b}` + ) + } + + const newNode = + a.object === 'text' + ? a.mergeText(b) + : a.set('nodes', a.nodes.concat(b.nodes)) + + let ret = this + ret = ret.removeNode(path) + ret = ret.removeNode(withPath) + ret = ret.insertNode(withPath, newNode) + return ret + } + + /** + * Move a node by `path` to `newPath`. + * + * A `newIndex` can be provided when move nodes by `key`, to account for not + * being able to have a key for a location in the tree that doesn't exist yet. + * + * @param {List|Key} path + * @param {List|Key} newPath + * @param {Number} newIndex + * @return {Node} + */ + + moveNode(path, newPath, newIndex = 0) { + const node = this.assertNode(path) + path = this.resolvePath(path) + newPath = this.resolvePath(newPath, newIndex) + + const newParentPath = PathUtils.lift(newPath) + this.assertNode(newParentPath) + + const [p, np] = PathUtils.crop(path, newPath) + const position = PathUtils.compare(p, np) + + // If the old path ends above and before a node in the new path, then + // removing it will alter the target, so we need to adjust the new path. + if (path.size < newPath.size && position === -1) { + newPath = PathUtils.decrement(newPath, 1, p.size - 1) + } + + let ret = this + ret = ret.removeNode(path) + ret = ret.insertNode(newPath, node) + return ret + } + + /** + * Attempt to "refind" a node by a previous `path`, falling back to looking + * it up by `key` again. + * + * @param {List|String} path + * @param {String} key + * @return {Node|Null} + */ + + refindNode(path, key) { + const node = this.getDescendant(path) + const found = node && node.key === key ? node : this.getDescendant(key) + return found + } + + /** + * Attempt to "refind" the path to a node by a previous `path`, falling back + * to looking it up by `key`. + * + * @param {List|String} path + * @param {String} key + * @return {List|Null} + */ + + refindPath(path, key) { + const node = this.getDescendant(path) + const found = node && node.key === key ? path : this.getPath(key) + return found + } + + /** + * Regenerate the node's key. + * + * @return {Node} + */ + + regenerateKey() { + const key = KeyUtils.create() + const node = this.set('key', key) + return node + } + + /** + * Remove mark from text at `offset` and `length` in node. + * + * @param {List} path + * @param {Number} offset + * @param {Number} length + * @param {Mark} mark + * @return {Node} + */ + + removeMark(path, offset, length, mark) { + let node = this.assertDescendant(path) + path = this.resolvePath(path) + node = node.removeMark(offset, length, mark) + const ret = this.replaceNode(path, node) + return ret + } + + /** + * Remove a node. + * + * @param {List|String} path + * @return {Node} + */ + + removeNode(path) { + this.assertDescendant(path) + path = this.resolvePath(path) + const deep = path.flatMap(x => List(['nodes', x])) + const ret = this.deleteIn(deep) + return ret + } + + /** + * Remove `text` at `offset` in node. + * + * @param {List|Key} path + * @param {Number} offset + * @param {String} text + * @return {Node} + */ + + removeText(path, offset, text) { + let node = this.assertDescendant(path) + node = node.removeText(offset, text.length) + const ret = this.replaceNode(path, node) + return ret + } + + /** + * Replace a `node` in the tree. + * + * @param {List|Key} path + * @param {Node} node + * @return {Node} + */ + + replaceNode(path, node) { + path = this.resolvePath(path) + + if (!path) { + throw new Error( + `Unable to replace a node because it could not be found in the first place: ${path}` + ) + } + + if (!path.size) return node + this.assertNode(path) + const deep = path.flatMap(x => List(['nodes', x])) + const ret = this.setIn(deep, node) + return ret + } + + /** + * Resolve a path from a path list or key string. + * + * An `index` can be provided, in which case paths created from a key string + * will have the index pushed onto them. This is helpful in cases where you + * want to accept either a `path` or a `key, index` combination for targeting + * a location in the tree that doesn't exist yet, like when inserting. + * + * @param {List|String} value + * @param {Number} index + * @return {List} + */ + + resolvePath(path, index) { + if (typeof path === 'string') { + path = this.getPath(path) + + if (index != null) { + path = path.concat(index) + } + } else { + path = PathUtils.create(path) + } + + return path + } + + /** + * Set `properties` on a node. + * + * @param {List|String} path + * @param {Object} properties + * @return {Node} + */ + + setNode(path, properties) { + let node = this.assertNode(path) + node = node.merge(properties) + const ret = this.replaceNode(path, node) + return ret + } + + /** + * Set `properties` on `mark` on text at `offset` and `length` in node. + * + * @param {List|String} path + * @param {Number} offset + * @param {Number} length + * @param {Mark} mark + * @param {Object} properties + * @return {Node} + */ + + setMark(path, offset, length, mark, properties) { + let node = this.assertNode(path) + node = node.updateMark(offset, length, mark, properties) + const ret = this.replaceNode(path, node) + return ret + } + + /** + * Split a node by `path` at `position` with optional `properties` to apply + * to the newly split node. + * + * @param {List|String} path + * @param {Number} position + * @param {Object} properties + * @return {Node} + */ + + splitNode(path, position, properties) { + const child = this.assertNode(path) + path = this.resolvePath(path) + let a + let b + + if (child.object === 'text') { + ;[a, b] = child.splitText(position) + } else { + const befores = child.nodes.take(position) + const afters = child.nodes.skip(position) + a = child.set('nodes', befores) + b = child.set('nodes', afters).regenerateKey() + } + + if (properties && child.object !== 'text') { + b = b.merge(properties) + } + + let ret = this + ret = ret.removeNode(path) + ret = ret.insertNode(path, b) + ret = ret.insertNode(path, a) + return ret + } + + /** + * Validate the node against a `schema`. + * + * @param {Schema} schema + * @return {Function|Null} + */ + + validate(schema) { + return schema.validateNode(this) + } + + /** + * Deprecated. + */ + + getNodeAtPath(path) { + logger.deprecate( + `0.35.0`, + 'The `Node.getNodeAtPath` method has been combined into `Node.getNode`.' + ) + + return this.getNode(path) + } + + getDescendantAtPath(path) { + logger.deprecate( + `0.35.0`, + 'The `Node.getDescendantAtPath` has been combined into `Node.getDescendant`.' + ) + + return this.getDescendant(path) + } + + getKeys() { + logger.deprecate(`0.35.0`, 'The `Node.getKeys` method is deprecated.') + + const keys = this.getKeysAsArray() + return new Set(keys) + } + + getKeysAsArray() { + logger.deprecate( + `0.35.0`, + 'The `Node.getKeysAsArray` method is deprecated.' + ) + + const dict = this.getKeysToPathsTable() + const keys = [] + + for (const key in dict) { + if (this.key !== key) { + keys.push(key) + } + } + + return keys + } + + areDescendantsSorted(first, second) { + logger.deprecate( + `0.35.0`, + 'The `Node.areDescendantsSorted` method is deprecated. Use the new `PathUtils.compare` helper instead.' + ) + + first = KeyUtils.create(first) + second = KeyUtils.create(second) + + const keys = this.getKeysAsArray().filter(k => k !== this.key) + const firstIndex = keys.indexOf(first) + const secondIndex = keys.indexOf(second) + if (firstIndex == -1 || secondIndex == -1) return null + + return firstIndex < secondIndex + } + isInRange(range) { + logger.deprecate( + `0.35.0`, + 'The `Node.isInRange` method is deprecated. Use the new `PathUtils.compare` helper instead.' + ) + range = range.normalize(this) const node = this @@ -1829,257 +2190,26 @@ class Node { return memo } - - /** - * Check whether the node is a leaf block. - * - * @return {Boolean} - */ - - isLeafBlock() { - return this.object == 'block' && this.nodes.every(n => n.object != 'block') - } - - /** - * Check whether the node is a leaf inline. - * - * @return {Boolean} - */ - - isLeafInline() { - return ( - this.object == 'inline' && this.nodes.every(n => n.object != 'inline') - ) - } - - /** - * Merge a children node `first` with another children node `second`. - * `first` and `second` will be concatenated in that order. - * `first` and `second` must be two Nodes or two Text. - * - * @param {Node} first - * @param {Node} second - * @return {Node} - */ - - mergeNode(withIndex, index) { - let node = this - let one = node.nodes.get(withIndex) - const two = node.nodes.get(index) - - if (one.object != two.object) { - throw new Error( - `Tried to merge two nodes of different objects: "${one.object}" and "${ - two.object - }".` - ) - } - - // If the nodes are text nodes, concatenate their leaves together - if (one.object == 'text') { - one = one.mergeText(two) - } else { - // Otherwise, concatenate their child nodes together. - const nodes = one.nodes.concat(two.nodes) - one = one.set('nodes', nodes) - } - - node = node.removeNode(index) - node = node.removeNode(withIndex) - node = node.insertNode(withIndex, one) - return node - } - - /** - * Map all child nodes, updating them in their parents. This method is - * optimized to not return a new node if no changes are made. - * - * @param {Function} iterator - * @return {Node} - */ - - mapChildren(iterator) { - let { nodes } = this - - nodes.forEach((node, i) => { - const ret = iterator(node, i, this.nodes) - if (ret != node) nodes = nodes.set(ret.key, ret) - }) - - return this.set('nodes', nodes) - } - - /** - * Map all descendant nodes, updating them in their parents. This method is - * optimized to not return a new node if no changes are made. - * - * @param {Function} iterator - * @return {Node} - */ - - mapDescendants(iterator) { - let { nodes } = this - - nodes.forEach((node, index) => { - let ret = node - if (ret.object != 'text') ret = ret.mapDescendants(iterator) - ret = iterator(ret, index, this.nodes) - if (ret == node) return - - nodes = nodes.set(index, ret) - }) - - return this.set('nodes', nodes) - } - - /** - * Regenerate the node's key. - * - * @return {Node} - */ - - regenerateKey() { - const key = generateKey() - return this.set('key', key) - } - - /** - * Remove a `node` from the children node map. - * - * @param {String} key - * @return {Node} - */ - - removeDescendant(key) { - key = assertKey(key) - - let node = this - let parent = node.getParent(key) - if (!parent) - throw new Error(`Could not find a descendant node with key "${key}".`) - - const index = parent.nodes.findIndex(n => n.key === key) - const nodes = parent.nodes.delete(index) - - parent = parent.set('nodes', nodes) - node = node.updateNode(parent) - return node - } - - /** - * Remove a node at `index`. - * - * @param {Number} index - * @return {Node} - */ - - removeNode(index) { - const nodes = this.nodes.delete(index) - return this.set('nodes', nodes) - } - - /** - * Split a child node by `index` at `position`. - * - * @param {Number} index - * @param {Number} position - * @return {Node} - */ - - splitNode(index, position) { - let node = this - const child = node.nodes.get(index) - let one - let two - - // If the child is a text node, the `position` refers to the text offset at - // which to split it. - if (child.object == 'text') { - ;[one, two] = child.splitText(position) - } else { - // Otherwise, if the child is not a text node, the `position` refers to the - // index at which to split its children. - const befores = child.nodes.take(position) - const afters = child.nodes.skip(position) - one = child.set('nodes', befores) - two = child.set('nodes', afters).regenerateKey() - } - - // Remove the old node and insert the newly split children. - node = node.removeNode(index) - node = node.insertNode(index, two) - node = node.insertNode(index, one) - return node - } - - /** - * Set a new value for a child node by `key`. - * - * @param {Node} node - * @return {Node} - */ - - updateNode(node) { - if (node.key == this.key) { - return node - } - - let child = this.assertDescendant(node.key) - const ancestors = this.getAncestors(node.key) - - ancestors.reverse().forEach(parent => { - let { nodes } = parent - const index = nodes.indexOf(child) - child = parent - nodes = nodes.set(index, node) - parent = parent.set('nodes', nodes) - node = parent - }) - - return node - } - - /** - * Validate the node against a `schema`. - * - * @param {Schema} schema - * @return {Function|Null} - */ - - validate(schema) { - return schema.validateNode(this) - } - - /** - * Get the first invalid descendant - * - * @param {Schema} schema - * @return {Node|Text|Null} - */ - - getFirstInvalidDescendant(schema) { - let result = null - - this.nodes.find(n => { - result = n.validate(schema) ? n : n.getFirstInvalidDescendant(schema) - return result - }) - return result - } } /** - * Assert a key `arg`. - * - * @param {String} arg - * @return {String} + * Mix in assertion variants. */ -function assertKey(arg) { - if (typeof arg == 'string') return arg - throw new Error( - `Invalid \`key\` argument! It must be a key string, but you passed: ${arg}` - ) +const ASSERTS = ['Child', 'Depth', 'Descendant', 'Node', 'Parent', 'Path'] + +for (const method of ASSERTS) { + Node.prototype[`assert${method}`] = function(path, ...args) { + const ret = this[`get${method}`](path, ...args) + + if (ret == null) { + throw new Error( + `\`Node.assert${method}\` could not find node with path or key: ${path}` + ) + } + + return ret + } } /** @@ -2087,26 +2217,13 @@ function assertKey(arg) { */ memoize(Node.prototype, [ - 'areDescendantsSorted', - 'getAncestors', 'getBlocksAsArray', 'getBlocksAtRangeAsArray', 'getBlocksByTypeAsArray', - 'getChild', - 'getClosestBlock', - 'getClosestInline', - 'getClosestVoid', - 'getCommonAncestor', 'getDecorations', - 'getDepth', - 'getDescendant', - 'getDescendantAtPath', + 'getFirstInvalidDescendant', 'getFirstText', 'getFragmentAtRange', - 'getFurthestBlock', - 'getFurthestInline', - 'getFurthestAncestor', - 'getFurthestOnlyChildAncestor', 'getInlinesAsArray', 'getInlinesAtRangeAsArray', 'getInlinesByTypeAsArray', @@ -2114,22 +2231,13 @@ memoize(Node.prototype, [ 'getMarksAtPosition', 'getOrderedMarksBetweenPositions', 'getInsertMarksAtRange', - 'getKeysAsArray', + 'getKeysToPathsTable', 'getLastText', 'getMarksByTypeAsArray', 'getNextBlock', - 'getNextSibling', - 'getNextText', - 'getNode', - 'getNodeAtPath', 'getOffset', 'getOffsetAtRange', - 'getParent', - 'getPath', - 'getPlaceholder', 'getPreviousBlock', - 'getPreviousSibling', - 'getPreviousText', 'getText', 'getTextAtOffset', 'getTextDirection', @@ -2138,7 +2246,6 @@ memoize(Node.prototype, [ 'isLeafBlock', 'isLeafInline', 'validate', - 'getFirstInvalidDescendant', ]) /** @@ -2146,7 +2253,7 @@ memoize(Node.prototype, [ */ Object.getOwnPropertyNames(Node.prototype).forEach(method => { - if (method == 'constructor') return + if (method === 'constructor') return Block.prototype[method] = Node.prototype[method] Inline.prototype[method] = Node.prototype[method] Document.prototype[method] = Node.prototype[method] diff --git a/packages/slate/src/models/operation.js b/packages/slate/src/models/operation.js index 50807f0bb..2564c67a0 100644 --- a/packages/slate/src/models/operation.js +++ b/packages/slate/src/models/operation.js @@ -6,6 +6,7 @@ import MODEL_TYPES from '../constants/model-types' import OPERATION_ATTRIBUTES from '../constants/operation-attributes' import Mark from './mark' import Node from './node' +import PathUtils from '../utils/path-utils' import Range from './range' import Value from './value' @@ -90,7 +91,7 @@ class Operation extends Record(DEFAULTS) { return object } - const { type, value } = object + const { type } = object const ATTRIBUTES = OPERATION_ATTRIBUTES[type] const attrs = { type } @@ -116,58 +117,51 @@ class Operation extends Record(DEFAULTS) { ) } - if (key == 'mark') { + if (key === 'path' || key === 'newPath') { + v = PathUtils.create(v) + } + + if (key === 'mark') { v = Mark.create(v) } - if (key == 'marks' && v != null) { + if (key === 'marks' && v != null) { v = Mark.createSet(v) } - if (key == 'node') { + if (key === 'node') { v = Node.create(v) } - if (key == 'selection') { + if (key === 'selection') { v = Range.create(v) } - if (key == 'value') { + if (key === 'value') { v = Value.create(v) } - if (key == 'properties' && type == 'merge_node') { + if (key === 'properties' && type === 'merge_node') { v = Node.createProperties(v) } - if (key == 'properties' && type == 'set_mark') { + if (key === 'properties' && type === 'set_mark') { v = Mark.createProperties(v) } - if (key == 'properties' && type == 'set_node') { + if (key === 'properties' && type === 'set_node') { v = Node.createProperties(v) } - if (key == 'properties' && type == 'set_selection') { - const { anchorKey, focusKey, ...rest } = v - v = Range.createProperties(rest) - - if (anchorKey !== undefined) { - v.anchorPath = - anchorKey === null ? null : value.document.getPath(anchorKey) - } - - if (focusKey !== undefined) { - v.focusPath = - focusKey === null ? null : value.document.getPath(focusKey) - } + if (key === 'properties' && type === 'set_selection') { + v = Range.createProperties(v) } - if (key == 'properties' && type == 'set_value') { + if (key === 'properties' && type === 'set_value') { v = Value.createProperties(v) } - if (key == 'properties' && type == 'split_node') { + if (key === 'properties' && type === 'split_node') { v = Node.createProperties(v) } @@ -275,13 +269,14 @@ class Operation extends Record(DEFAULTS) { if (key == 'properties' && type == 'set_selection') { const v = {} if ('anchorOffset' in value) v.anchorOffset = value.anchorOffset - if ('anchorPath' in value) v.anchorPath = value.anchorPath + if ('anchorPath' in value) + v.anchorPath = value.anchorPath && value.anchorPath.toJSON() if ('focusOffset' in value) v.focusOffset = value.focusOffset - if ('focusPath' in value) v.focusPath = value.focusPath + if ('focusPath' in value) + v.focusPath = value.focusPath && value.focusPath.toJSON() if ('isBackward' in value) v.isBackward = value.isBackward if ('isFocused' in value) v.isFocused = value.isFocused - if ('marks' in value) - v.marks = value.marks == null ? null : value.marks.toJSON() + if ('marks' in value) v.marks = value.marks && value.marks.toJSON() value = v } diff --git a/packages/slate/src/models/range.js b/packages/slate/src/models/range.js index 090c4878c..b4d193901 100644 --- a/packages/slate/src/models/range.js +++ b/packages/slate/src/models/range.js @@ -2,6 +2,7 @@ import isPlainObject from 'is-plain-object' import logger from 'slate-dev-logger' import { List, Record, Set } from 'immutable' +import PathUtils from '../utils/path-utils' import MODEL_TYPES from '../constants/model-types' import Mark from './mark' @@ -14,12 +15,14 @@ import Mark from './mark' const DEFAULTS = { anchorKey: null, anchorOffset: 0, + anchorPath: null, focusKey: null, focusOffset: 0, + focusPath: null, + isAtomic: false, isBackward: null, isFocused: false, marks: null, - isAtomic: false, } /** @@ -75,38 +78,49 @@ class Range extends Record(DEFAULTS) { * @return {Object} */ - static createProperties(attrs = {}) { - if (Range.isRange(attrs)) { + static createProperties(a = {}) { + if (Range.isRange(a)) { return { - anchorKey: attrs.anchorKey, - anchorOffset: attrs.anchorOffset, - focusKey: attrs.focusKey, - focusOffset: attrs.focusOffset, - isBackward: attrs.isBackward, - isFocused: attrs.isFocused, - marks: attrs.marks, - isAtomic: attrs.isAtomic, + anchorKey: a.anchorKey, + anchorOffset: a.anchorOffset, + anchorPath: a.anchorPath, + focusKey: a.focusKey, + focusOffset: a.focusOffset, + focusPath: a.focusPath, + isAtomic: a.isAtomic, + isBackward: a.isBackward, + isFocused: a.isFocused, + marks: a.marks, } } - if (isPlainObject(attrs)) { - const props = {} - if ('anchorKey' in attrs) props.anchorKey = attrs.anchorKey - if ('anchorOffset' in attrs) props.anchorOffset = attrs.anchorOffset - if ('anchorPath' in attrs) props.anchorPath = attrs.anchorPath - if ('focusKey' in attrs) props.focusKey = attrs.focusKey - if ('focusOffset' in attrs) props.focusOffset = attrs.focusOffset - if ('focusPath' in attrs) props.focusPath = attrs.focusPath - if ('isBackward' in attrs) props.isBackward = attrs.isBackward - if ('isFocused' in attrs) props.isFocused = attrs.isFocused - if ('marks' in attrs) - props.marks = attrs.marks == null ? null : Mark.createSet(attrs.marks) - if ('isAtomic' in attrs) props.isAtomic = attrs.isAtomic - return props + if (isPlainObject(a)) { + const p = {} + if ('anchorKey' in a) p.anchorKey = a.anchorKey + if ('anchorOffset' in a) p.anchorOffset = a.anchorOffset + if ('anchorPath' in a) p.anchorPath = PathUtils.create(a.anchorPath) + if ('focusKey' in a) p.focusKey = a.focusKey + if ('focusOffset' in a) p.focusOffset = a.focusOffset + if ('focusPath' in a) p.focusPath = PathUtils.create(a.focusPath) + if ('isAtomic' in a) p.isAtomic = a.isAtomic + if ('isBackward' in a) p.isBackward = a.isBackward + if ('isFocused' in a) p.isFocused = a.isFocused + if ('marks' in a) + p.marks = a.marks == null ? null : Mark.createSet(a.marks) + + // If only a path is set, or only a key is set, ensure that the other is + // set to null so that it can be normalized back to the right value. + // Otherwise we won't realize that the path and key don't match anymore. + if ('anchorPath' in a && !('anchorKey' in a)) p.anchorKey = null + if ('anchorKey' in a && !('anchorPath' in a)) p.anchorPath = null + if ('focusPath' in a && !('focusKey' in a)) p.focusKey = null + if ('focusKey' in a && !('focusPath' in a)) p.focusPath = null + + return p } throw new Error( - `\`Range.createProperties\` only accepts objects or ranges, but you passed it: ${attrs}` + `\`Range.createProperties\` only accepts objects or ranges, but you passed it: ${a}` ) } @@ -121,23 +135,27 @@ class Range extends Record(DEFAULTS) { const { anchorKey = null, anchorOffset = 0, + anchorPath = null, focusKey = null, focusOffset = 0, + focusPath = null, + isAtomic = false, isBackward = null, isFocused = false, marks = null, - isAtomic = false, } = object const range = new Range({ anchorKey, anchorOffset, + anchorPath: PathUtils.create(anchorPath), focusKey, focusOffset, + focusPath: PathUtils.create(focusPath), + isAtomic, isBackward, isFocused, marks: marks == null ? null : new Set(marks.map(Mark.fromJSON)), - isAtomic, }) return range @@ -227,7 +245,10 @@ class Range extends Record(DEFAULTS) { */ get isSet() { - return this.anchorKey != null && this.focusKey != null + return ( + (this.anchorKey != null && this.focusKey != null) || + (this.anchorPath != null && this.focusPath != null) + ) } /** @@ -260,6 +281,16 @@ class Range extends Record(DEFAULTS) { return this.isBackward ? this.focusOffset : this.anchorOffset } + /** + * Get the start path. + * + * @return {String} + */ + + get startPath() { + return this.isBackward ? this.focusPath : this.anchorPath + } + /** * Get the end key. * @@ -280,6 +311,16 @@ class Range extends Record(DEFAULTS) { return this.isBackward ? this.anchorOffset : this.focusOffset } + /** + * Get the end path. + * + * @return {String} + */ + + get endPath() { + return this.isBackward ? this.anchorPath : this.focusPath + } + /** * Check whether anchor point of the range is at the start of a `node`. * @@ -290,7 +331,7 @@ class Range extends Record(DEFAULTS) { hasAnchorAtStartOf(node) { // PERF: Do a check for a `0` offset first since it's quickest. if (this.anchorOffset != 0) return false - const first = getFirst(node) + const first = getFirstText(node) return this.anchorKey == first.key } @@ -302,7 +343,7 @@ class Range extends Record(DEFAULTS) { */ hasAnchorAtEndOf(node) { - const last = getLast(node) + const last = getLastText(node) return this.anchorKey == last.key && this.anchorOffset == last.text.length } @@ -345,7 +386,7 @@ class Range extends Record(DEFAULTS) { */ hasFocusAtEndOf(node) { - const last = getLast(node) + const last = getLastText(node) return this.focusKey == last.key && this.focusOffset == last.text.length } @@ -358,7 +399,7 @@ class Range extends Record(DEFAULTS) { hasFocusAtStartOf(node) { if (this.focusOffset != 0) return false - const first = getFirst(node) + const first = getFirstText(node) return this.focusKey == first.key } @@ -449,8 +490,10 @@ class Range extends Record(DEFAULTS) { return this.merge({ anchorKey: null, anchorOffset: 0, + anchorPath: null, focusKey: null, focusOffset: 0, + focusPath: null, isFocused: false, isBackward: false, }) @@ -466,8 +509,10 @@ class Range extends Record(DEFAULTS) { return this.merge({ anchorKey: this.focusKey, anchorOffset: this.focusOffset, + anchorPath: this.focusPath, focusKey: this.anchorKey, focusOffset: this.anchorOffset, + focusPath: this.anchorPath, isBackward: this.isBackward == null ? null : !this.isBackward, }) } @@ -507,43 +552,91 @@ class Range extends Record(DEFAULTS) { } /** - * Move the range's anchor point to a `key` and `offset`. + * Move the range's anchor point to a new `key` or `path` and `offset`. * - * @param {String} key + * @param {String|List} key or path * @param {Number} offset * @return {Range} */ moveAnchorTo(key, offset) { - const { anchorKey, focusKey, focusOffset, isBackward } = this - return this.merge({ - anchorKey: key, - anchorOffset: offset, - isBackward: - key == focusKey + const { + anchorKey, + focusKey, + focusOffset, + anchorPath, + focusPath, + isBackward, + } = this + + if (typeof key === 'string') { + const isAnchor = key === anchorKey + const isFocus = key === focusKey + return this.merge({ + anchorKey: key, + anchorPath: isFocus ? focusPath : isAnchor ? anchorPath : null, + anchorOffset: offset, + isBackward: isFocus ? offset > focusOffset - : key == anchorKey ? isBackward : null, - }) + : isAnchor ? isBackward : null, + }) + } else { + const path = key + const isAnchor = path && path.equals(anchorPath) + const isFocus = path && path.equals(focusPath) + return this.merge({ + anchorPath: path, + anchorKey: isAnchor ? anchorKey : isFocus ? focusKey : null, + anchorOffset: offset, + isBackward: isFocus + ? offset > focusOffset + : isAnchor ? isBackward : null, + }) + } } /** - * Move the range's focus point to a `key` and `offset`. + * Move the range's focus point to a new `key` or `path` and `offset`. * - * @param {String} key + * @param {String|List} key or path * @param {Number} offset * @return {Range} */ moveFocusTo(key, offset) { - const { focusKey, anchorKey, anchorOffset, isBackward } = this - return this.merge({ - focusKey: key, - focusOffset: offset, - isBackward: - key == anchorKey - ? anchorOffset > offset - : key == focusKey ? isBackward : null, - }) + const { + focusKey, + anchorKey, + anchorOffset, + anchorPath, + focusPath, + isBackward, + } = this + + if (typeof key === 'string') { + const isAnchor = key === anchorKey + const isFocus = key === focusKey + return this.merge({ + focusKey: key, + focusPath: isAnchor ? anchorPath : isFocus ? focusPath : null, + focusOffset: offset, + isBackward: isAnchor + ? offset < anchorOffset + : isFocus ? isBackward : null, + }) + } else { + const path = key + const isAnchor = path && path.equals(anchorPath) + const isFocus = path && path.equals(focusPath) + return this.merge({ + focusPath: path, + focusKey: isFocus ? focusKey : isAnchor ? anchorKey : null, + focusOffset: offset, + isBackward: isAnchor + ? offset < anchorOffset + : isFocus ? isBackward : null, + }) + } } /** @@ -620,7 +713,7 @@ class Range extends Record(DEFAULTS) { */ moveAnchorToStartOf(node) { - node = getFirst(node) + node = getFirstText(node) return this.moveAnchorTo(node.key, 0) } @@ -632,7 +725,7 @@ class Range extends Record(DEFAULTS) { */ moveAnchorToEndOf(node) { - node = getLast(node) + node = getLastText(node) return this.moveAnchorTo(node.key, node.text.length) } @@ -644,7 +737,7 @@ class Range extends Record(DEFAULTS) { */ moveFocusToStartOf(node) { - node = getFirst(node) + node = getFirstText(node) return this.moveFocusTo(node.key, 0) } @@ -656,7 +749,7 @@ class Range extends Record(DEFAULTS) { */ moveFocusToEndOf(node) { - node = getLast(node) + node = getLastText(node) return this.moveFocusTo(node.key, node.text.length) } @@ -683,7 +776,15 @@ class Range extends Record(DEFAULTS) { normalize(node) { const range = this - let { anchorKey, anchorOffset, focusKey, focusOffset, isBackward } = range + let { + anchorKey, + anchorOffset, + anchorPath, + focusKey, + focusOffset, + focusPath, + isBackward, + } = range const anchorOffsetType = typeof anchorOffset const focusOffsetType = typeof focusOffset @@ -694,20 +795,25 @@ class Range extends Record(DEFAULTS) { ) } - // If the range is unset, make sure it is properly zeroed out. - if (anchorKey == null || focusKey == null) { + // If either point in the range is unset, make sure it is fully unset. + if ( + (anchorKey == null && anchorPath == null) || + (focusKey == null && focusPath == null) + ) { return range.merge({ anchorKey: null, anchorOffset: 0, + anchorPath: null, focusKey: null, focusOffset: 0, + focusPath: null, isBackward: false, }) } // Get the anchor and focus nodes. - let anchorNode = node.getDescendant(anchorKey) - let focusNode = node.getDescendant(focusKey) + let anchorNode = node.getNode(anchorKey || anchorPath) + let focusNode = node.getNode(focusKey || focusPath) // If the range is malformed, warn and zero it out. if (!anchorNode || !focusNode) { @@ -717,11 +823,14 @@ class Range extends Record(DEFAULTS) { ) const first = node.getFirstText() + const path = first && node.getPath(first.key) return range.merge({ anchorKey: first ? first.key : null, anchorOffset: 0, + anchorPath: first ? path : null, focusKey: first ? first.key : null, focusOffset: 0, + focusPath: first ? path : null, isBackward: false, }) } @@ -752,21 +861,25 @@ class Range extends Record(DEFAULTS) { focusNode = focusText } + anchorKey = anchorNode.key + focusKey = focusNode.key + anchorPath = node.getPath(anchorKey) + focusPath = node.getPath(focusKey) + // If `isBackward` is not set, derive it. if (isBackward == null) { - if (anchorNode.key === focusNode.key) { - isBackward = anchorOffset > focusOffset - } else { - isBackward = !node.areDescendantsSorted(anchorNode.key, focusNode.key) - } + const result = PathUtils.compare(anchorPath, focusPath) + isBackward = result === 0 ? anchorOffset > focusOffset : result === 1 } // Merge in any updated properties. return range.merge({ - anchorKey: anchorNode.key, + anchorKey, anchorOffset, - focusKey: focusNode.key, + anchorPath, + focusKey, focusOffset, + focusPath, isBackward, }) } @@ -774,21 +887,29 @@ class Range extends Record(DEFAULTS) { /** * Return a JSON representation of the range. * + * @param {Object} options * @return {Object} */ - toJSON() { + toJSON(options = {}) { const object = { object: this.object, anchorKey: this.anchorKey, anchorOffset: this.anchorOffset, + anchorPath: this.anchorPath && this.anchorPath.toArray(), focusKey: this.focusKey, focusOffset: this.focusOffset, + focusPath: this.focusPath && this.focusPath.toArray(), + isAtomic: this.isAtomic, isBackward: this.isBackward, isFocused: this.isFocused, marks: this.marks == null ? null : this.marks.toArray().map(m => m.toJSON()), - isAtomic: this.isAtomic, + } + + if (!options.preserveKeys) { + delete object.anchorKey + delete object.focusKey } return object @@ -892,7 +1013,7 @@ ALIAS_METHODS.forEach(([alias, method]) => { * @return {Text} */ -function getFirst(node) { +function getFirstText(node) { return node.object == 'text' ? node : node.getFirstText() } @@ -903,7 +1024,7 @@ function getFirst(node) { * @return {Text} */ -function getLast(node) { +function getLastText(node) { return node.object == 'text' ? node : node.getLastText() } diff --git a/packages/slate/src/models/text.js b/packages/slate/src/models/text.js index f0d0958fb..07f1b2068 100644 --- a/packages/slate/src/models/text.js +++ b/packages/slate/src/models/text.js @@ -4,7 +4,7 @@ import { List, OrderedSet, Record, Set } from 'immutable' import Leaf from './leaf' import MODEL_TYPES, { isType } from '../constants/model-types' -import generateKey from '../utils/generate-key' +import KeyUtils from '../utils/key-utils' import memoize from '../utils/memoize' /** @@ -85,7 +85,7 @@ class Text extends Record(DEFAULTS) { return object } - const { key = generateKey() } = object + const { key = KeyUtils.create() } = object let { leaves = List() } = object if (Array.isArray(leaves)) { @@ -387,6 +387,14 @@ class Text extends Record(DEFAULTS) { }) } + getFirstText() { + return this + } + + getLastText() { + return this + } + /** * Get all of the marks on between two offsets * Corner Cases: @@ -545,7 +553,7 @@ class Text extends Record(DEFAULTS) { */ regenerateKey() { - const key = generateKey() + const key = KeyUtils.create() return this.set('key', key) } diff --git a/packages/slate/src/models/value.js b/packages/slate/src/models/value.js index dd778a440..16355e8f7 100644 --- a/packages/slate/src/models/value.js +++ b/packages/slate/src/models/value.js @@ -3,6 +3,7 @@ import logger from 'slate-dev-logger' import { Record, Set, List, Map } from 'immutable' import MODEL_TYPES from '../constants/model-types' +import PathUtils from '../utils/path-utils' import Change from './change' import Data from './data' import Document from './document' @@ -61,26 +62,25 @@ class Value extends Record(DEFAULTS) { * @return {Object} */ - static createProperties(attrs = {}) { - if (Value.isValue(attrs)) { + static createProperties(a = {}) { + if (Value.isValue(a)) { return { - data: attrs.data, - decorations: attrs.decorations, - schema: attrs.schema, + data: a.data, + decorations: a.decorations, + schema: a.schema, } } - if (isPlainObject(attrs)) { - const props = {} - if ('data' in attrs) props.data = Data.create(attrs.data) - if ('decorations' in attrs) - props.decorations = Range.createList(attrs.decorations) - if ('schema' in attrs) props.schema = Schema.create(attrs.schema) - return props + if (isPlainObject(a)) { + const p = {} + if ('data' in a) p.data = Data.create(a.data) + if ('decorations' in a) p.decorations = Range.createList(a.decorations) + if ('schema' in a) p.schema = Schema.create(a.schema) + return p } throw new Error( - `\`Value.createProperties\` only accepts objects or values, but you passed it: ${attrs}` + `\`Value.createProperties\` only accepts objects or values, but you passed it: ${a}` ) } @@ -96,22 +96,8 @@ class Value extends Record(DEFAULTS) { static fromJSON(object, options = {}) { let { document = {}, selection = {}, schema = {}, history = {} } = object - let data = new Map() - document = Document.fromJSON(document) - - // rebuild selection from anchorPath and focusPath if keys were dropped - const { anchorPath, focusPath, anchorKey, focusKey } = selection - - if (anchorPath !== undefined && anchorKey === undefined) { - selection.anchorKey = document.assertPath(anchorPath).key - } - - if (focusPath !== undefined && focusKey === undefined) { - selection.focusKey = document.assertPath(focusPath).key - } - selection = Range.fromJSON(selection) schema = Schema.fromJSON(schema) history = History.fromJSON(history) @@ -133,6 +119,8 @@ class Value extends Record(DEFAULTS) { if (text) selection = selection.collapseToStartOf(text) } + selection = selection.normalize(document) + let value = new Value({ data, document, @@ -283,6 +271,26 @@ class Value extends Record(DEFAULTS) { return this.selection.endKey } + /** + * Get the current start path. + * + * @return {String} + */ + + get startPath() { + return this.selection.startPath + } + + /** + * Get the current end path. + * + * @return {String} + */ + + get endPath() { + return this.selection.endPath + } + /** * Get the current start offset. * @@ -323,6 +331,26 @@ class Value extends Record(DEFAULTS) { return this.selection.focusKey } + /** + * Get the current anchor path. + * + * @return {String} + */ + + get anchorPath() { + return this.selection.anchorPath + } + + /** + * Get the current focus path. + * + * @return {String} + */ + + get focusPath() { + return this.selection.focusPath + } + /** * Get the current anchor offset. * @@ -642,6 +670,422 @@ class Value extends Record(DEFAULTS) { return new Change({ ...attrs, value: this }) } + /** + * Add mark to text at `offset` and `length` in node by `path`. + * + * @param {List|String} path + * @param {Number} offset + * @param {Number} length + * @param {Mark} mark + * @return {Value} + */ + + addMark(path, offset, length, mark) { + let value = this + let { document } = value + document = document.addMark(path, offset, length, mark) + value = this.set('document', document) + return value + } + + /** + * Insert a `node`. + * + * @param {List|String} path + * @param {Node} node + * @return {Value} + */ + + insertNode(path, node) { + let value = this + let { document } = value + document = document.insertNode(path, node) + value = value.set('document', document) + + value = value.mapRanges(range => { + return range.merge({ anchorPath: null, focusPath: null }) + }) + + return value + } + + /** + * Insert `text` at `offset` in node by `path`. + * + * @param {List|String} path + * @param {Number} offset + * @param {String} text + * @param {Set} marks + * @return {Value} + */ + + insertText(path, offset, text, marks) { + let value = this + let { document } = value + document = document.insertText(path, offset, text, marks) + value = value.set('document', document) + + // Update any ranges that were affected. + const node = document.assertNode(path) + value = value.clearAtomicRanges(node.key, offset) + + value = value.mapRanges(range => { + const { anchorKey, anchorOffset, isBackward, isAtomic } = range + + if ( + anchorKey === node.key && + (anchorOffset > offset || + (anchorOffset === offset && (!isAtomic || !isBackward))) + ) { + return range.moveAnchor(text.length) + } + + return range + }) + + value = value.mapRanges(range => { + const { focusKey, focusOffset, isBackward, isAtomic } = range + + if ( + focusKey === node.key && + (focusOffset > offset || + (focusOffset == offset && (!isAtomic || isBackward))) + ) { + return range.moveFocus(text.length) + } + + return range + }) + + return value + } + + /** + * Merge a node backwards its previous sibling. + * + * @param {List|Key} path + * @return {Value} + */ + + mergeNode(path) { + let value = this + const { document } = value + const newDocument = document.mergeNode(path) + path = document.resolvePath(path) + const withPath = PathUtils.decrement(path) + const one = document.getNode(withPath) + const two = document.getNode(path) + value = value.set('document', newDocument) + + value = value.mapRanges(range => { + if (two.object === 'text') { + const max = one.text.length + + if (range.anchorKey === two.key) { + range = range.moveAnchorTo(one.key, max + range.anchorOffset) + } + + if (range.focusKey === two.key) { + range = range.moveFocusTo(one.key, max + range.focusOffset) + } + } + + range = range.merge({ anchorPath: null, focusPath: null }) + return range + }) + + return value + } + + /** + * Move a node by `path` to `newPath`. + * + * A `newIndex` can be provided when move nodes by `key`, to account for not + * being able to have a key for a location in the tree that doesn't exist yet. + * + * @param {List|Key} path + * @param {List|Key} newPath + * @param {Number} newIndex + * @return {Value} + */ + + moveNode(path, newPath, newIndex = 0) { + let value = this + let { document } = value + document = document.moveNode(path, newPath, newIndex) + value = value.set('document', document) + + value = value.mapRanges(range => { + return range.merge({ anchorPath: null, focusPath: null }) + }) + + return value + } + + /** + * Remove mark from text at `offset` and `length` in node. + * + * @param {List|String} path + * @param {Number} offset + * @param {Number} length + * @param {Mark} mark + * @return {Value} + */ + + removeMark(path, offset, length, mark) { + let value = this + let { document } = value + document = document.removeMark(path, offset, length, mark) + value = this.set('document', document) + return value + } + + /** + * Remove a node by `path`. + * + * @param {List|String} path + * @return {Value} + */ + + removeNode(path) { + let value = this + let { document } = value + const node = document.assertNode(path) + const first = node.object == 'text' ? node : node.getFirstText() || node + const last = node.object == 'text' ? node : node.getLastText() || node + const prev = document.getPreviousText(first.key) + const next = document.getNextText(last.key) + + document = document.removeNode(path) + value = value.set('document', document) + + value = value.mapRanges(range => { + const { startKey, endKey } = range + + if (node.hasNode(startKey)) { + range = prev + ? range.moveStartTo(prev.key, prev.text.length) + : next ? range.moveStartTo(next.key, 0) : range.deselect() + } + + if (node.hasNode(endKey)) { + range = prev + ? range.moveEndTo(prev.key, prev.text.length) + : next ? range.moveEndTo(next.key, 0) : range.deselect() + } + + range = range.merge({ anchorPath: null, focusPath: null }) + return range + }) + + return value + } + + /** + * Remove `text` at `offset` in node by `path`. + * + * @param {List|Key} path + * @param {Number} offset + * @param {String} text + * @return {Value} + */ + + removeText(path, offset, text) { + let value = this + let { document } = value + document = document.removeText(path, offset, text) + value = value.set('document', document) + + const node = document.assertNode(path) + const { length } = text + const rangeOffset = offset + length + value = value.clearAtomicRanges(node.key, offset, offset + length) + + value = value.mapRanges(range => { + const { anchorKey } = range + + if (anchorKey === node.key) { + return range.anchorOffset >= rangeOffset + ? range.moveAnchor(-length) + : range.anchorOffset > offset + ? range.moveAnchorTo(range.anchorKey, offset) + : range + } + + return range + }) + + value = value.mapRanges(range => { + const { focusKey } = range + + if (focusKey === node.key) { + return range.focusOffset >= rangeOffset + ? range.moveFocus(-length) + : range.focusOffset > offset + ? range.moveFocusTo(range.focusKey, offset) + : range + } + + return range + }) + + return value + } + + /** + * Set `properties` on a node. + * + * @param {List|String} path + * @param {Object} properties + * @return {Value} + */ + + setNode(path, properties) { + let value = this + let { document } = value + document = document.setNode(path, properties) + value = value.set('document', document) + return value + } + + /** + * Set `properties` on `mark` on text at `offset` and `length` in node. + * + * @param {List|String} path + * @param {Number} offset + * @param {Number} length + * @param {Mark} mark + * @param {Object} properties + * @return {Value} + */ + + setMark(path, offset, length, mark, properties) { + let value = this + let { document } = value + document = document.setMark(path, offset, length, mark, properties) + value = value.set('document', document) + return value + } + + /** + * Set `properties` on the selection. + * + * @param {Value} value + * @param {Operation} operation + * @return {Value} + */ + + setSelection(properties) { + let value = this + let { document, selection } = value + selection = selection.merge(properties) + selection = selection.normalize(document) + value = value.set('selection', selection) + return value + } + + /** + * Split a node by `path` at `position` with optional `properties` to apply + * to the newly split node. + * + * @param {List|String} path + * @param {Number} position + * @param {Object} properties + * @return {Value} + */ + + splitNode(path, position, properties) { + let value = this + const { document } = value + const newDocument = document.splitNode(path, position, properties) + const node = document.assertNode(path) + value = value.set('document', newDocument) + + value = value.mapRanges(range => { + const next = newDocument.getNextText(node.key) + const { startKey, startOffset, endKey, endOffset } = range + + // If the start was after the split, move it to the next node. + if (node.key === startKey && position <= startOffset) { + range = range.moveStartTo(next.key, startOffset - position) + } + + // If the end was after the split, move it to the next node. + if (node.key === endKey && position <= endOffset) { + range = range.moveEndTo(next.key, endOffset - position) + } + + range = range.merge({ anchorPath: null, focusPath: null }) + return range + }) + + return value + } + + /** + * Map all range objects to apply adjustments with an `iterator`. + * + * @param {Function} iterator + * @return {Value} + */ + + mapRanges(iterator) { + let value = this + const { document, selection, decorations } = value + + if (selection) { + let next = selection.isSet ? iterator(selection) : selection + if (!next) next = selection.deselect() + if (next !== selection) next = next.normalize(document) + value = value.set('selection', next) + } + + if (decorations) { + let next = decorations.map(decoration => { + let n = decoration.isSet ? iterator(decoration) : decoration + if (n && n !== decoration) n = n.normalize(document) + return n + }) + + next = next.filter(decoration => !!decoration) + next = next.size ? next : null + value = value.set('decorations', next) + } + + return value + } + + /** + * Remove any atomic ranges inside a `key`, `offset` and `length`. + * + * @param {String} key + * @param {Number} start + * @param {Number?} end + * @return {Value} + */ + + clearAtomicRanges(key, start, end = null) { + return this.mapRanges(range => { + const { isAtomic, startKey, startOffset, endKey, endOffset } = range + if (!isAtomic) return range + if (startKey !== key) return range + + if (startOffset < start && (endKey !== key || endOffset > start)) { + return null + } + + if ( + end != null && + startOffset < end && + (endKey !== key || endOffset > end) + ) { + return null + } + + return range + }) + } + /** * Return a JSON representation of the value. * @@ -656,59 +1100,25 @@ class Value extends Record(DEFAULTS) { } if (options.preserveData) { - object.data = this.data.toJSON() + object.data = this.data.toJSON(options) } if (options.preserveDecorations) { object.decorations = this.decorations - ? this.decorations.toArray().map(d => d.toJSON()) + ? this.decorations.toArray().map(d => d.toJSON(options)) : null } if (options.preserveHistory) { - object.history = this.history.toJSON() + object.history = this.history.toJSON(options) } if (options.preserveSelection) { - object.selection = this.selection.toJSON() + object.selection = this.selection.toJSON(options) } if (options.preserveSchema) { - object.schema = this.schema.toJSON() - } - - if (options.preserveSelection && !options.preserveKeys) { - const { document, selection } = this - - object.selection.anchorPath = selection.isSet - ? document.getPath(selection.anchorKey) - : null - - object.selection.focusPath = selection.isSet - ? document.getPath(selection.focusKey) - : null - - delete object.selection.anchorKey - delete object.selection.focusKey - } - - if ( - options.preserveDecorations && - object.decorations && - !options.preserveKeys - ) { - const { document } = this - - object.decorations = object.decorations.map(decoration => { - const withPath = { - ...decoration, - anchorPath: document.getPath(decoration.anchorKey), - focusPath: document.getPath(decoration.focusKey), - } - delete withPath.anchorKey - delete withPath.focusKey - return withPath - }) + object.schema = this.schema.toJSON(options) } return object diff --git a/packages/slate/src/operations/apply.js b/packages/slate/src/operations/apply.js index f20d14bb1..9b34ee554 100644 --- a/packages/slate/src/operations/apply.js +++ b/packages/slate/src/operations/apply.js @@ -11,552 +11,101 @@ import Operation from '../models/operation' const debug = Debug('slate:operation:apply') /** - * Apply adjustments to affected ranges (selections, decorations); - * accepts (value, checking function(range) -> bool, applying function(range) -> range) - * returns value with affected ranges updated + * Apply an `op` to a `value`. * * @param {Value} value - * @param {Function} checkAffected - * @param {Function} adjustRange - * @return {Value} - */ - -function applyRangeAdjustments(value, checkAffected, adjustRange) { - // check selection, apply adjustment if affected - if (value.selection && checkAffected(value.selection)) { - value = value.set('selection', adjustRange(value.selection)) - } - - if (!value.decorations) return value - - // check all ranges, apply adjustment if affected - const decorations = value.decorations - .map( - decoration => - checkAffected(decoration) ? adjustRange(decoration) : decoration - ) - .filter(decoration => decoration.anchorKey !== null) - return value.set('decorations', decorations) -} - -/** - * clear any atomic ranges (in decorations) if they contain the point (key, offset, offset-end?) - * specified - * - * @param {Value} value - * @param {String} key - * @param {Number} offset - * @param {Number?} offsetEnd - * @return {Value} - */ - -function clearAtomicRangesIfContains(value, key, offset, offsetEnd = null) { - return applyRangeAdjustments( - value, - range => { - if (!range.isAtomic) return false - const { startKey, startOffset, endKey, endOffset } = range - return ( - (startKey == key && - startOffset < offset && - (endKey != key || endOffset > offset)) || - (offsetEnd && - startKey == key && - startOffset < offsetEnd && - (endKey != key || endOffset > offsetEnd)) - ) - }, - range => range.deselect() - ) -} - -/** - * Applying functions. - * - * @type {Object} - */ - -const APPLIERS = { - /** - * Add mark to text at `offset` and `length` in node by `path`. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - add_mark(value, operation) { - const { path, offset, length, mark } = operation - let { document } = value - let node = document.assertPath(path) - node = node.addMark(offset, length, mark) - document = document.updateNode(node) - value = value.set('document', document) - return value - }, - - /** - * Insert a `node` at `index` in a node by `path`. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - insert_node(value, operation) { - const { path, node } = operation - const index = path[path.length - 1] - const rest = path.slice(0, -1) - let { document } = value - let parent = document.assertPath(rest) - parent = parent.insertNode(index, node) - document = document.updateNode(parent) - value = value.set('document', document) - return value - }, - - /** - * Insert `text` at `offset` in node by `path`. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - insert_text(value, operation) { - const { path, offset, text, marks } = operation - let { document } = value - let node = document.assertPath(path) - - // Update the document - node = node.insertText(offset, text, marks) - document = document.updateNode(node) - - value = value.set('document', document) - - // if insert happens within atomic ranges, clear - value = clearAtomicRangesIfContains(value, node.key, offset) - - // Update the selection, decorations - value = applyRangeAdjustments( - value, - ({ anchorKey, anchorOffset, isBackward, isAtomic }) => - anchorKey == node.key && - (anchorOffset > offset || - (anchorOffset == offset && (!isAtomic || !isBackward))), - range => range.moveAnchor(text.length) - ) - - value = applyRangeAdjustments( - value, - ({ focusKey, focusOffset, isBackward, isAtomic }) => - focusKey == node.key && - (focusOffset > offset || - (focusOffset == offset && (!isAtomic || isBackward))), - range => range.moveFocus(text.length) - ) - - return value - }, - - /** - * Merge a node at `path` with the previous node. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - merge_node(value, operation) { - const { path } = operation - const withPath = path - .slice(0, path.length - 1) - .concat([path[path.length - 1] - 1]) - let { document } = value - const one = document.assertPath(withPath) - const two = document.assertPath(path) - let parent = document.getParent(one.key) - const oneIndex = parent.nodes.indexOf(one) - const twoIndex = parent.nodes.indexOf(two) - - // Perform the merge in the document. - parent = parent.mergeNode(oneIndex, twoIndex) - document = document.updateNode(parent) - value = value.set('document', document) - - if (one.object == 'text') { - value = applyRangeAdjustments( - value, - // If the nodes are text nodes and the range is inside the second node: - ({ anchorKey, focusKey }) => - anchorKey == two.key || focusKey == two.key, - // update it to refer to the first node instead: - range => { - if (range.anchorKey == two.key) - range = range.moveAnchorTo( - one.key, - one.text.length + range.anchorOffset - ) - if (range.focusKey == two.key) - range = range.moveFocusTo( - one.key, - one.text.length + range.focusOffset - ) - return range.normalize(document) - } - ) - } - - return value - }, - - /** - * Move a node by `path` to `newPath`. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - move_node(value, operation) { - const { path, newPath } = operation - const newIndex = newPath[newPath.length - 1] - const newParentPath = newPath.slice(0, -1) - const oldParentPath = path.slice(0, -1) - const oldIndex = path[path.length - 1] - let { document } = value - const node = document.assertPath(path) - - // Remove the node from its current parent. - let parent = document.getParent(node.key) - parent = parent.removeNode(oldIndex) - document = document.updateNode(parent) - - // Find the new target... - let target - - // If the old path and the rest of the new path are the same, then the new - // target is the old parent. - if ( - oldParentPath.every((x, i) => x === newParentPath[i]) && - oldParentPath.length === newParentPath.length - ) { - target = parent - } else if ( - oldParentPath.every((x, i) => x === newParentPath[i]) && - oldIndex < newParentPath[oldParentPath.length] - ) { - // Otherwise, if the old path removal resulted in the new path being no longer - // correct, we need to decrement the new path at the old path's last index. - newParentPath[oldParentPath.length]-- - target = document.assertPath(newParentPath) - } else { - // Otherwise, we can just grab the target normally... - target = document.assertPath(newParentPath) - } - - // Insert the new node to its new parent. - target = target.insertNode(newIndex, node) - document = document.updateNode(target) - value = value.set('document', document) - return value - }, - - /** - * Remove mark from text at `offset` and `length` in node by `path`. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - remove_mark(value, operation) { - const { path, offset, length, mark } = operation - let { document } = value - let node = document.assertPath(path) - node = node.removeMark(offset, length, mark) - document = document.updateNode(node) - value = value.set('document', document) - return value - }, - - /** - * Remove a node by `path`. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - remove_node(value, operation) { - const { path } = operation - let { document, selection } = value - const node = document.assertPath(path) - - if (selection.isSet || value.decorations !== null) { - const first = node.object == 'text' ? node : node.getFirstText() || node - const last = node.object == 'text' ? node : node.getLastText() || node - const prev = document.getPreviousText(first.key) - const next = document.getNextText(last.key) - - value = applyRangeAdjustments( - value, - // If the start or end point was in this node - ({ startKey, endKey }) => - node.hasNode(startKey) || node.hasNode(endKey), - // update it to be just before/after - range => { - const { startKey, endKey } = range - - if (node.hasNode(startKey)) { - range = prev - ? range.moveStartTo(prev.key, prev.text.length) - : next ? range.moveStartTo(next.key, 0) : range.deselect() - } - - if (node.hasNode(endKey)) { - range = prev - ? range.moveEndTo(prev.key, prev.text.length) - : next ? range.moveEndTo(next.key, 0) : range.deselect() - } - - // If the range wasn't deselected, normalize it. - if (range.isSet) return range.normalize(document) - return range - } - ) - } - - // Remove the node from the document. - let parent = document.getParent(node.key) - const index = parent.nodes.indexOf(node) - parent = parent.removeNode(index) - document = document.updateNode(parent) - - // Update the document and range. - value = value.set('document', document) - return value - }, - - /** - * Remove `text` at `offset` in node by `path`. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - remove_text(value, operation) { - const { path, offset, text } = operation - const { length } = text - const rangeOffset = offset + length - let { document } = value - - let node = document.assertPath(path) - - // if insert happens within atomic ranges, clear - value = clearAtomicRangesIfContains( - value, - node.key, - offset, - offset + length - ) - - value = applyRangeAdjustments( - value, - // if anchor of range is here - ({ anchorKey }) => anchorKey == node.key, - // adjust if it is in or past the removal range - range => - range.anchorOffset >= rangeOffset - ? range.moveAnchor(-length) - : range.anchorOffset > offset - ? range.moveAnchorTo(range.anchorKey, offset) - : range - ) - - value = applyRangeAdjustments( - value, - // if focus of range is here - ({ focusKey }) => focusKey == node.key, - // adjust if it is in or past the removal range - range => - range.focusOffset >= rangeOffset - ? range.moveFocus(-length) - : range.focusOffset > offset - ? range.moveFocusTo(range.focusKey, offset) - : range - ) - - node = node.removeText(offset, length) - document = document.updateNode(node) - value = value.set('document', document) - return value - }, - - /** - * Set `properties` on mark on text at `offset` and `length` in node by `path`. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - set_mark(value, operation) { - const { path, offset, length, mark, properties } = operation - let { document } = value - let node = document.assertPath(path) - node = node.updateMark(offset, length, mark, properties) - document = document.updateNode(node) - value = value.set('document', document) - return value - }, - - /** - * Set `properties` on a node by `path`. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - set_node(value, operation) { - const { path, properties } = operation - let { document } = value - let node = document.assertPath(path) - node = node.merge(properties) - document = document.updateNode(node) - value = value.set('document', document) - return value - }, - - /** - * Set `properties` on the selection. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - set_selection(value, operation) { - const { properties } = operation - const { anchorPath, focusPath, ...props } = properties - let { document, selection } = value - - if (anchorPath !== undefined) { - props.anchorKey = - anchorPath === null ? null : document.assertPath(anchorPath).key - } - - if (focusPath !== undefined) { - props.focusKey = - focusPath === null ? null : document.assertPath(focusPath).key - } - - selection = selection.merge(props) - selection = selection.normalize(document) - value = value.set('selection', selection) - return value - }, - - /** - * Set `properties` on `value`. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - set_value(value, operation) { - const { properties } = operation - value = value.merge(properties) - return value - }, - - /** - * Split a node by `path` at `offset`. - * - * @param {Value} value - * @param {Operation} operation - * @return {Value} - */ - - split_node(value, operation) { - const { path, position, properties } = operation - let { document } = value - - // Calculate a few things... - const node = document.assertPath(path) - let parent = document.getParent(node.key) - const index = parent.nodes.indexOf(node) - - // Split the node by its parent. - parent = parent.splitNode(index, position) - - if (properties) { - const splitNode = parent.nodes.get(index + 1) - - if (splitNode.object !== 'text') { - parent = parent.updateNode(splitNode.merge(properties)) - } - } - - document = document.updateNode(parent) - const next = document.getNextText(node.key) - - value = applyRangeAdjustments( - value, - // check if range is affected - ({ startKey, startOffset, endKey, endOffset }) => - (node.key == startKey && position <= startOffset) || - (node.key == endKey && position <= endOffset), - // update its start / end as needed - range => { - const { startKey, startOffset, endKey, endOffset } = range - let normalize = false - - if (node.key == startKey && position <= startOffset) { - range = range.moveStartTo(next.key, startOffset - position) - normalize = true - } - - if (node.key == endKey && position <= endOffset) { - range = range.moveEndTo(next.key, endOffset - position) - normalize = true - } - - // Normalize the selection if we changed it - if (normalize) return range.normalize(document) - return range - } - ) - - // Return the updated value. - value = value.set('document', document) - return value - }, -} - -/** - * Apply an `operation` to a `value`. - * - * @param {Value} value - * @param {Object|Operation} operation + * @param {Object|Operation} op * @return {Value} value */ -function applyOperation(value, operation) { - operation = Operation.create(operation) - const { type } = operation - const apply = APPLIERS[type] +function applyOperation(value, op) { + op = Operation.create(op) + const { type } = op + debug(type, op) - if (!apply) { - throw new Error(`Unknown operation type: "${type}".`) + switch (type) { + case 'add_mark': { + const { path, offset, length, mark } = op + const next = value.addMark(path, offset, length, mark) + return next + } + + case 'insert_node': { + const { path, node } = op + const next = value.insertNode(path, node) + return next + } + + case 'insert_text': { + const { path, offset, text, marks } = op + const next = value.insertText(path, offset, text, marks) + return next + } + + case 'merge_node': { + const { path } = op + const next = value.mergeNode(path) + return next + } + + case 'move_node': { + const { path, newPath } = op + const next = value.moveNode(path, newPath) + return next + } + + case 'remove_mark': { + const { path, offset, length, mark } = op + const next = value.removeMark(path, offset, length, mark) + return next + } + + case 'remove_node': { + const { path } = op + const next = value.removeNode(path) + return next + } + + case 'remove_text': { + const { path, offset, text } = op + const next = value.removeText(path, offset, text) + return next + } + + case 'set_mark': { + const { path, offset, length, mark, properties } = op + const next = value.setMark(path, offset, length, mark, properties) + return next + } + + case 'set_node': { + const { path, properties } = op + const next = value.setNode(path, properties) + return next + } + + case 'set_selection': { + const { properties } = op + const next = value.setSelection(properties) + return next + } + + case 'set_value': { + const { properties } = op + const next = value.merge(properties) + return next + } + + case 'split_node': { + const { path, position, properties } = op + const next = value.splitNode(path, position, properties) + return next + } + + default: { + throw new Error(`Unknown operation type: "${type}".`) + } } - - debug(type, operation) - value = apply(value, operation) - return value } /** diff --git a/packages/slate/src/operations/invert.js b/packages/slate/src/operations/invert.js index 50107923c..fb68a175d 100644 --- a/packages/slate/src/operations/invert.js +++ b/packages/slate/src/operations/invert.js @@ -2,6 +2,7 @@ import Debug from 'debug' import pick from 'lodash/pick' import Operation from '../models/operation' +import PathUtils from '../utils/path-utils' /** * Debug. @@ -23,215 +24,135 @@ function invertOperation(op) { const { type } = op debug(type, op) - /** - * Insert node. - */ - - if (type == 'insert_node') { - const inverse = op.set('type', 'remove_node') - return inverse - } - - /** - * Remove node. - */ - - if (type == 'remove_node') { - const inverse = op.set('type', 'insert_node') - return inverse - } - - /** - * Move node. - */ - - if (type == 'move_node') { - const { newPath, path } = op - let inversePath = newPath - let inverseNewPath = path - - const pathLast = path.length - 1 - const newPathLast = newPath.length - 1 - - // If the node's old position was a left sibling of an ancestor of - // its new position, we need to adjust part of the path by -1. - if ( - path.length < inversePath.length && - path.slice(0, pathLast).every((e, i) => e == inversePath[i]) && - path[pathLast] < inversePath[pathLast] - ) { - inversePath = inversePath - .slice(0, pathLast) - .concat([inversePath[pathLast] - 1]) - .concat(inversePath.slice(pathLast + 1, inversePath.length)) + switch (type) { + case 'insert_node': { + const inverse = op.set('type', 'remove_node') + return inverse } - // If the node's new position is an ancestor of the old position, - // or a left sibling of an ancestor of its old position, we need - // to adjust part of the path by 1. - if ( - newPath.length < inverseNewPath.length && - newPath.slice(0, newPathLast).every((e, i) => e == inverseNewPath[i]) && - newPath[newPathLast] <= inverseNewPath[newPathLast] - ) { - inverseNewPath = inverseNewPath - .slice(0, newPathLast) - .concat([inverseNewPath[newPathLast] + 1]) - .concat(inverseNewPath.slice(newPathLast + 1, inverseNewPath.length)) + case 'remove_node': { + const inverse = op.set('type', 'insert_node') + return inverse } - const inverse = op.set('path', inversePath).set('newPath', inverseNewPath) - return inverse - } + case 'move_node': { + const { newPath, path } = op + let inversePath = newPath + let inverseNewPath = path - /** - * Merge node. - */ + const pathLast = path.size - 1 + const newPathLast = newPath.size - 1 - if (type == 'merge_node') { - const { path } = op - const { length } = path - const last = length - 1 - const inversePath = path.slice(0, last).concat([path[last] - 1]) - const inverse = op.set('type', 'split_node').set('path', inversePath) - return inverse - } + // If the node's old position was a left sibling of an ancestor of + // its new position, we need to adjust part of the path by -1. + if ( + path.size < inversePath.size && + path.slice(0, pathLast).every((e, i) => e == inversePath.get(i)) && + path.last() < inversePath.get(pathLast) + ) { + inversePath = inversePath + .slice(0, pathLast) + .concat(inversePath.get(pathLast) - 1) + .concat(inversePath.slice(pathLast + 1, inversePath.size)) + } - /** - * Split node. - */ + // If the node's new position is an ancestor of the old position, + // or a left sibling of an ancestor of its old position, we need + // to adjust part of the path by 1. + if ( + newPath.size < inverseNewPath.size && + newPath + .slice(0, newPathLast) + .every((e, i) => e == inverseNewPath.get(i)) && + newPath.last() <= inverseNewPath.get(newPathLast) + ) { + inverseNewPath = inverseNewPath + .slice(0, newPathLast) + .concat(inverseNewPath.get(newPathLast) + 1) + .concat(inverseNewPath.slice(newPathLast + 1, inverseNewPath.size)) + } - if (type == 'split_node') { - const { path } = op - const { length } = path - const last = length - 1 - const inversePath = path.slice(0, last).concat([path[last] + 1]) - const inverse = op.set('type', 'merge_node').set('path', inversePath) - return inverse - } - - /** - * Set node. - */ - - if (type == 'set_node') { - const { properties, node } = op - const inverseNode = node.merge(properties) - const inverseProperties = pick(node, Object.keys(properties)) - const inverse = op - .set('node', inverseNode) - .set('properties', inverseProperties) - return inverse - } - - /** - * Insert text. - */ - - if (type == 'insert_text') { - const inverse = op.set('type', 'remove_text') - return inverse - } - - /** - * Remove text. - */ - - if (type == 'remove_text') { - const inverse = op.set('type', 'insert_text') - return inverse - } - - /** - * Add mark. - */ - - if (type == 'add_mark') { - const inverse = op.set('type', 'remove_mark') - return inverse - } - - /** - * Remove mark. - */ - - if (type == 'remove_mark') { - const inverse = op.set('type', 'add_mark') - return inverse - } - - /** - * Set mark. - */ - - if (type == 'set_mark') { - const { properties, mark } = op - const inverseMark = mark.merge(properties) - const inverseProperties = pick(mark, Object.keys(properties)) - const inverse = op - .set('mark', inverseMark) - .set('properties', inverseProperties) - return inverse - } - - /** - * Set selection. - */ - - if (type == 'set_selection') { - const { properties, selection, value } = op - const { anchorPath, focusPath, ...props } = properties - const { document } = value - - if (anchorPath !== undefined) { - props.anchorKey = - anchorPath === null ? null : document.assertPath(anchorPath).key + const inverse = op.set('path', inversePath).set('newPath', inverseNewPath) + return inverse } - if (focusPath !== undefined) { - props.focusKey = - focusPath === null ? null : document.assertPath(focusPath).key + case 'merge_node': { + const { path } = op + const inversePath = PathUtils.decrement(path) + const inverse = op.set('type', 'split_node').set('path', inversePath) + return inverse } - const inverseSelection = selection.merge(props) - const inverseProps = pick(selection, Object.keys(props)) - - if (anchorPath !== undefined) { - inverseProps.anchorPath = - inverseProps.anchorKey === null - ? null - : document.getPath(inverseProps.anchorKey) - - delete inverseProps.anchorKey + case 'split_node': { + const { path } = op + const inversePath = PathUtils.increment(path) + const inverse = op.set('type', 'merge_node').set('path', inversePath) + return inverse } - if (focusPath !== undefined) { - inverseProps.focusPath = - inverseProps.focusKey === null - ? null - : document.getPath(inverseProps.focusKey) - - delete inverseProps.focusKey + case 'set_node': { + const { properties, node } = op + const inverseNode = node.merge(properties) + const inverseProperties = pick(node, Object.keys(properties)) + const inverse = op + .set('node', inverseNode) + .set('properties', inverseProperties) + return inverse } - const inverse = op - .set('selection', inverseSelection) - .set('properties', inverseProps) - return inverse - } + case 'insert_text': { + const inverse = op.set('type', 'remove_text') + return inverse + } - /** - * Set value. - */ + case 'remove_text': { + const inverse = op.set('type', 'insert_text') + return inverse + } - if (type == 'set_value') { - const { properties, value } = op - const inverseValue = value.merge(properties) - const inverseProperties = pick(value, Object.keys(properties)) - const inverse = op - .set('value', inverseValue) - .set('properties', inverseProperties) - return inverse + case 'add_mark': { + const inverse = op.set('type', 'remove_mark') + return inverse + } + + case 'remove_mark': { + const inverse = op.set('type', 'add_mark') + return inverse + } + + case 'set_mark': { + const { properties, mark } = op + const inverseMark = mark.merge(properties) + const inverseProperties = pick(mark, Object.keys(properties)) + const inverse = op + .set('mark', inverseMark) + .set('properties', inverseProperties) + return inverse + } + + case 'set_selection': { + const { properties, selection } = op + const inverseSelection = selection.merge(properties) + const inverseProps = pick(selection, Object.keys(properties)) + const inverse = op + .set('selection', inverseSelection) + .set('properties', inverseProps) + return inverse + } + + case 'set_value': { + const { properties, value } = op + const inverseValue = value.merge(properties) + const inverseProperties = pick(value, Object.keys(properties)) + const inverse = op + .set('value', inverseValue) + .set('properties', inverseProperties) + return inverse + } + + default: { + throw new Error(`Unknown operation type: "${type}".`) + } } } diff --git a/packages/slate/src/utils/generate-key.js b/packages/slate/src/utils/generate-key.js index ffd5a7506..eb8c35592 100644 --- a/packages/slate/src/utils/generate-key.js +++ b/packages/slate/src/utils/generate-key.js @@ -1,59 +1,32 @@ -/** - * An auto-incrementing index for generating keys. - * - * @type {Number} - */ - -let n - -/** - * The global key generating function. - * - * @type {Function} - */ - -let generate - -/** - * Generate a key. - * - * @return {String} - */ +import KeyUtils from './key-utils' +import logger from 'slate-dev-logger' function generateKey() { - return generate() + logger.deprecate( + `0.35.0`, + 'The `generateKey()` util is deprecrated. Use the `KeyUtils.create()` helper instead.' + ) + + return KeyUtils.create() } -/** - * Set a different unique ID generating `function`. - * - * @param {Function} func - */ +function setKeyGenerator(fn) { + logger.deprecate( + `0.35.0`, + 'The `setKeyGenerator()` util is deprecrated. Use the `KeyUtils.setGenerator()` helper instead.' + ) -function setKeyGenerator(func) { - generate = func + return KeyUtils.setGenerator(fn) } -/** - * Reset the key generating function to its initial state. - */ - function resetKeyGenerator() { - n = 0 - generate = () => `${n++}` + logger.deprecate( + `0.35.0`, + 'The `resetKeyGenerator()` util is deprecrated. Use the `KeyUtils.resetGenerator()` helper instead.' + ) + + return KeyUtils.resetGenerator() } -/** - * Set the initial state. - */ - -resetKeyGenerator() - -/** - * Export. - * - * @type {Object} - */ - export default generateKey export { setKeyGenerator, resetKeyGenerator } diff --git a/packages/slate/src/utils/is-react-component.js b/packages/slate/src/utils/is-react-component.js deleted file mode 100644 index 21e2bdcf1..000000000 --- a/packages/slate/src/utils/is-react-component.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Check if an `object` is a React component. - * - * @param {Object} object - * @return {Boolean} - */ - -function isReactComponent(object) { - return object && object.prototype && object.prototype.isReactComponent -} - -/** - * Export. - * - * @type {Function} - */ - -export default isReactComponent diff --git a/packages/slate/src/utils/key-utils.js b/packages/slate/src/utils/key-utils.js new file mode 100644 index 000000000..60ece95ce --- /dev/null +++ b/packages/slate/src/utils/key-utils.js @@ -0,0 +1,71 @@ +/** + * An auto-incrementing index for generating keys. + * + * @type {Number} + */ + +let n + +/** + * The global key generating function. + * + * @type {Function} + */ + +let generate + +/** + * Create a key, using a provided key if available. + * + * @param {String|Void} key + * @return {String} + */ + +function create(key) { + if (key == null) { + return generate() + } + + if (typeof key === 'string') { + return key + } + + throw new Error(`Keys must be strings, but you passed: ${key}`) +} + +/** + * Set a different unique ID generating `function`. + * + * @param {Function} func + */ + +function setGenerator(func) { + generate = func +} + +/** + * Reset the key generating function to its initial state. + */ + +function resetGenerator() { + n = 0 + generate = () => `${n++}` +} + +/** + * Set the initial state. + */ + +resetGenerator() + +/** + * Export. + * + * @type {Object} + */ + +export default { + create, + setGenerator, + resetGenerator, +} diff --git a/packages/slate/src/utils/path-utils.js b/packages/slate/src/utils/path-utils.js new file mode 100644 index 000000000..2148f55e7 --- /dev/null +++ b/packages/slate/src/utils/path-utils.js @@ -0,0 +1,217 @@ +import { List } from 'immutable' + +/** + * Compare paths `a` and `b` to see which is before or after. + * + * @param {List} a + * @param {List} b + * @return {Number|Null} + */ + +function compare(a, b) { + // PERF: if the paths are the same we can exit early. + if (a.size !== b.size) return null + + for (let i = 0; i < a.size; i++) { + const av = a.get(i) + const bv = b.get(i) + + // If a's value is ever less than b's, it's before. + if (av < bv) return -1 + + // If b's value is ever less than a's, it's after. + if (av > bv) return 1 + } + + // Otherwise they were equal the whole way, it's the same. + return 0 +} + +/** + * Create a path from `attrs`. + * + * @param {Array|List} attrs + * @return {List} + */ + +function create(attrs) { + if (attrs == null) { + return null + } + + if (List.isList(attrs)) { + return attrs + } + + if (Array.isArray(attrs)) { + return List(attrs) + } + + throw new Error( + `Paths can only be created from arrays or lists, but you passed: ${attrs}` + ) +} + +/** + * Crop paths `a` and `b` to an equal size, defaulting to the shortest. + * + * @param {List} a + * @param {List} b + */ + +function crop(a, b, size = min(a, b)) { + const ca = a.slice(0, size) + const cb = b.slice(0, size) + return [ca, cb] +} + +/** + * Decrement a `path` by `n` at `index`, defaulting to the last index. + * + * @param {List} path + * @param {Number} n + * @param {Number} index + */ + +function decrement(path, n = 1, index = path.size - 1) { + return increment(path, 0 - n, index) +} + +/** + * Increment a `path` by `n` at `index`, defaulting to the last index. + * + * @param {List} path + * @param {Number} n + * @param {Number} index + */ + +function increment(path, n = 1, index = path.size - 1) { + const value = path.get(index) + const newValue = value + n + const newPath = path.set(index, newValue) + return newPath +} + +/** + * Is a `path` above another `target` path? + * + * @param {List} path + * @param {List} target + * @return {Boolean} + */ + +function isAbove(path, target) { + const [p, t] = crop(path, target) + return path.size < target.size && compare(p, t) === 0 +} + +/** + * Is a `path` after another `target` path in a document? + * + * @param {List} path + * @param {List} target + * @return {Boolean} + */ + +function isAfter(path, target) { + const [p, t] = crop(path, target) + return compare(p, t) === 1 +} + +/** + * Is a `path` before another `target` path in a document? + * + * @param {List} path + * @param {List} target + * @return {Boolean} + */ + +function isBefore(path, target) { + const [p, t] = crop(path, target) + return compare(p, t) === -1 +} + +/** + * Lift a `path` to refer to its parent. + * + * @param {List} path + * @return {Array} + */ + +function lift(path) { + const parent = path.slice(0, -1) + return parent +} + +/** + * Get the maximum length of paths `a` and `b`. + * + * @param {List} path + * @param {List} path + * @return {Number} + */ + +function max(a, b) { + const n = Math.max(a.size, b.size) + return n +} + +/** + * Get the minimum length of paths `a` and `b`. + * + * @param {List} path + * @param {List} path + * @return {Number} + */ + +function min(a, b) { + const n = Math.min(a.size, b.size) + return n +} + +/** + * Get the common ancestor path of path `a` and path `b`. + * + * @param {List} a + * @param {List} b + * @return {List} + */ + +function relate(a, b) { + const array = [] + + for (let i = 0; i < a.size && i < b.size; i++) { + const av = a.get(i) + const bv = b.get(i) + + // If the values aren't equal, they've diverged and don't share an ancestor. + if (av !== bv) break + + // Otherwise, the current value is still a common ancestor. + array.push(av) + } + + const path = create(array) + return path +} + +/** + * Export. + * + * @type {Object} + */ + +export default { + compare, + create, + crop, + decrement, + increment, + isAbove, + isAfter, + isBefore, + lift, + max, + min, + relate, +} diff --git a/packages/slate/src/utils/string.js b/packages/slate/src/utils/text-utils.js similarity index 98% rename from packages/slate/src/utils/string.js rename to packages/slate/src/utils/text-utils.js index 060c57942..70083f11a 100644 --- a/packages/slate/src/utils/string.js +++ b/packages/slate/src/utils/text-utils.js @@ -188,8 +188,13 @@ function getWordOffsetForward(text, offset) { */ export default { - getCharOffsetForward, + getCharLength, + getCharOffset, getCharOffsetBackward, + getCharOffsetForward, + getWordOffset, getWordOffsetBackward, getWordOffsetForward, + isSurrogate, + isWord, } diff --git a/packages/slate/test/index.js b/packages/slate/test/index.js index 3924d6b13..56003530a 100644 --- a/packages/slate/test/index.js +++ b/packages/slate/test/index.js @@ -2,7 +2,7 @@ * Dependencies. */ -import { resetKeyGenerator } from '..' +import { KeyUtils } from '..' /** * Tests. @@ -22,5 +22,5 @@ describe('slate', () => { */ beforeEach(() => { - resetKeyGenerator() + KeyUtils.resetGenerator() }) diff --git a/packages/slate/test/models/change/without-normalization-normalize-flag-false.js b/packages/slate/test/models/change/without-normalization-normalize-flag-false.js index 26b5d6618..c7d078ea1 100644 --- a/packages/slate/test/models/change/without-normalization-normalize-flag-false.js +++ b/packages/slate/test/models/change/without-normalization-normalize-flag-false.js @@ -38,7 +38,9 @@ export const output = ( - + + + diff --git a/packages/slate/test/operations/apply/remove_text/cursor-inside-removed-text.js b/packages/slate/test/operations/apply/remove-text/anchor-after.js similarity index 72% rename from packages/slate/test/operations/apply/remove_text/cursor-inside-removed-text.js rename to packages/slate/test/operations/apply/remove-text/anchor-after.js index 2e86f4e79..321e5b164 100644 --- a/packages/slate/test/operations/apply/remove_text/cursor-inside-removed-text.js +++ b/packages/slate/test/operations/apply/remove-text/anchor-after.js @@ -6,8 +6,8 @@ export default [ { type: 'remove_text', path: [0, 0], - offset: 2, - text: 'is is some text inside ', + offset: 1, + text: 'or', marks: [], }, ] @@ -16,7 +16,7 @@ export const input = ( - This is some text inside a paragraph. + word @@ -26,7 +26,7 @@ export const output = ( - Tha paragraph. + wd diff --git a/packages/slate/test/operations/apply/update-decorations/delete-before.js b/packages/slate/test/operations/apply/remove-text/anchor-before.js similarity index 70% rename from packages/slate/test/operations/apply/update-decorations/delete-before.js rename to packages/slate/test/operations/apply/remove-text/anchor-before.js index 68395d856..a8629a4d9 100644 --- a/packages/slate/test/operations/apply/update-decorations/delete-before.js +++ b/packages/slate/test/operations/apply/remove-text/anchor-before.js @@ -6,8 +6,8 @@ export default [ { type: 'remove_text', path: [0, 0], - offset: 2, - text: ' there', + offset: 1, + text: 'or', marks: [], }, ] @@ -16,7 +16,7 @@ export const input = ( - Hi there you person + word @@ -26,7 +26,7 @@ export const output = ( - Hi you person + wd diff --git a/packages/slate/test/operations/apply/remove-text/anchor-middle.js b/packages/slate/test/operations/apply/remove-text/anchor-middle.js new file mode 100644 index 000000000..409c29476 --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/anchor-middle.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'or', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wd + + + +) diff --git a/packages/slate/test/operations/apply/remove-text/cursor-after.js b/packages/slate/test/operations/apply/remove-text/cursor-after.js new file mode 100644 index 000000000..b098766c7 --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/cursor-after.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'or', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wd + + + +) diff --git a/packages/slate/test/operations/apply/remove-text/cursor-before.js b/packages/slate/test/operations/apply/remove-text/cursor-before.js new file mode 100644 index 000000000..621e86f44 --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/cursor-before.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'or', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wd + + + +) diff --git a/packages/slate/test/operations/apply/remove-text/cursor-middle.js b/packages/slate/test/operations/apply/remove-text/cursor-middle.js new file mode 100644 index 000000000..26ab18dd8 --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/cursor-middle.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'or', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wd + + + +) diff --git a/packages/slate/test/operations/apply/remove-text/decoration-after.js b/packages/slate/test/operations/apply/remove-text/decoration-after.js new file mode 100644 index 000000000..b206b06fe --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/decoration-after.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'or', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wd + + + +) diff --git a/packages/slate/test/operations/apply/remove-text/decoration-atomic-after.js b/packages/slate/test/operations/apply/remove-text/decoration-atomic-after.js new file mode 100644 index 000000000..3155e4ee2 --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/decoration-atomic-after.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'or', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wd + + + +) diff --git a/packages/slate/test/operations/apply/remove-text/decoration-atomic-before.js b/packages/slate/test/operations/apply/remove-text/decoration-atomic-before.js new file mode 100644 index 000000000..d22bbbaf1 --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/decoration-atomic-before.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'or', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wd + + + +) diff --git a/packages/slate/test/operations/apply/remove-text/decoration-atomic-middle.js b/packages/slate/test/operations/apply/remove-text/decoration-atomic-middle.js new file mode 100644 index 000000000..0d6375f5b --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/decoration-atomic-middle.js @@ -0,0 +1,31 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'o', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + wrd + + +) diff --git a/packages/slate/test/operations/apply/remove-text/decoration-before.js b/packages/slate/test/operations/apply/remove-text/decoration-before.js new file mode 100644 index 000000000..67c1130f0 --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/decoration-before.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'or', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wd + + + +) diff --git a/packages/slate/test/operations/apply/remove-text/decoration-middle.js b/packages/slate/test/operations/apply/remove-text/decoration-middle.js new file mode 100644 index 000000000..1df3e69bc --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/decoration-middle.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'o', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wrd + + + +) diff --git a/packages/slate/test/operations/apply/remove-text/focus-after.js b/packages/slate/test/operations/apply/remove-text/focus-after.js new file mode 100644 index 000000000..09414da66 --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/focus-after.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'or', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wd + + + +) diff --git a/packages/slate/test/operations/apply/remove-text/focus-before.js b/packages/slate/test/operations/apply/remove-text/focus-before.js new file mode 100644 index 000000000..c88f3a3fa --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/focus-before.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'or', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wd + + + +) diff --git a/packages/slate/test/operations/apply/remove-text/focus-middle.js b/packages/slate/test/operations/apply/remove-text/focus-middle.js new file mode 100644 index 000000000..aa669515b --- /dev/null +++ b/packages/slate/test/operations/apply/remove-text/focus-middle.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export default [ + { + type: 'remove_text', + path: [0, 0], + offset: 1, + text: 'or', + marks: [], + }, +] + +export const input = ( + + + + word + + + +) + +export const output = ( + + + + wd + + + +) diff --git a/packages/slate/test/operations/apply/update-decorations/atomic-invalidation-delete.js b/packages/slate/test/operations/apply/update-decorations/atomic-invalidation-delete.js deleted file mode 100644 index b0f39bdfe..000000000 --- a/packages/slate/test/operations/apply/update-decorations/atomic-invalidation-delete.js +++ /dev/null @@ -1,61 +0,0 @@ -/** @jsx h */ - -import h from '../../../helpers/h' - -export default [ - { - type: 'remove_text', - path: [0, 0], - offset: 13, - text: 'on', - marks: [], - }, - { - type: 'remove_text', - path: [1, 0], - offset: 0, - text: 'This ', - marks: [], - }, - { - type: 'remove_text', - path: [2, 0], - offset: 10, - text: 'ation', - marks: [], - }, -] - -export const input = ( - - - - This decoration should be invalid,{' '} - this one shouldn't. - - - This decoration will be fine. - - - This decoration can be altered, since non-atomic. - - - -) - -export const output = ( - - - - This decorati should be invalid, this one - shouldn't. - - - decoration will be fine. - - - This decor can be altered, since non-atomic. - - - -) diff --git a/packages/slate/test/operations/apply/update-decorations/atomic-invalidation-insert.js b/packages/slate/test/operations/apply/update-decorations/atomic-invalidation-insert.js deleted file mode 100644 index 07abd5bb6..000000000 --- a/packages/slate/test/operations/apply/update-decorations/atomic-invalidation-insert.js +++ /dev/null @@ -1,61 +0,0 @@ -/** @jsx h */ - -import h from '../../../helpers/h' - -export default [ - { - type: 'insert_text', - path: [0, 0], - offset: 6, - text: 'x', - marks: [], - }, - { - type: 'insert_text', - path: [1, 0], - offset: 5, - text: 'small ', - marks: [], - }, - { - type: 'remove_text', - path: [2, 0], - offset: 10, - text: 'ation', - marks: [], - }, -] - -export const input = ( - - - - This decoration should be invalid,{' '} - this one shouldn't. - - - This decoration will be fine. - - - This decoration can be altered, since non-atomic. - - - -) - -export const output = ( - - - - This dxecoration should be invalid, this{' '} - one shouldn't. - - - This small decoration will be fine. - - - This decor can be altered, since non-atomic. - - - -) diff --git a/packages/slate/test/operations/apply/update-decorations/insert-before.js b/packages/slate/test/operations/apply/update-decorations/insert-text-before.js similarity index 100% rename from packages/slate/test/operations/apply/update-decorations/insert-before.js rename to packages/slate/test/operations/apply/update-decorations/insert-text-before.js diff --git a/packages/slate/test/operations/index.js b/packages/slate/test/operations/index.js index 68b10586d..1fc5a006b 100644 --- a/packages/slate/test/operations/index.js +++ b/packages/slate/test/operations/index.js @@ -1,6 +1,6 @@ import assert from 'assert' import fs from 'fs-promise' // eslint-disable-line import/no-extraneous-dependencies -import toCamel from 'to-camel-case' // eslint-disable-line import/no-extraneous-dependencies +import toSnake from 'to-snake-case' // eslint-disable-line import/no-extraneous-dependencies import { basename, extname, resolve } from 'path' /** @@ -19,7 +19,7 @@ describe('operations', async () => { const methods = fs.readdirSync(categoryDir).filter(c => c[0] != '.') for (const method of methods) { - describe(toCamel(method), () => { + describe(toSnake(method), () => { const testDir = resolve(categoryDir, method) const tests = fs .readdirSync(testDir) @@ -40,7 +40,6 @@ describe('operations', async () => { } const actual = change.value.toJSON(opts) const expected = output.toJSON(opts) - assert.deepEqual(actual, expected) }) } diff --git a/packages/slate/test/serializers/raw/serialize/preserve-selection-and-keys.js b/packages/slate/test/serializers/raw/serialize/preserve-selection-and-keys.js index 49a4f2c37..320ab29b6 100644 --- a/packages/slate/test/serializers/raw/serialize/preserve-selection-and-keys.js +++ b/packages/slate/test/serializers/raw/serialize/preserve-selection-and-keys.js @@ -42,8 +42,10 @@ export const output = { selection: { object: 'range', anchorKey: '0', + anchorPath: [0, 0], anchorOffset: 0, focusKey: '0', + focusPath: [0, 0], focusOffset: 0, isBackward: false, isFocused: false, diff --git a/yarn.lock b/yarn.lock index 58c883bbf..86f0362f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8007,6 +8007,12 @@ to-regex@^3.0.1: extend-shallow "^2.0.1" regex-not "^1.0.0" +to-snake-case@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-snake-case/-/to-snake-case-1.0.0.tgz#ce746913897946019a87e62edfaeaea4c608ab8c" + dependencies: + to-space-case "^1.0.0" + to-space-case@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/to-space-case/-/to-space-case-1.0.0.tgz#b052daafb1b2b29dc770cea0163e5ec0ebc9fc17"