From e6d611d726b1ff76cd3c41982436175208eb3051 Mon Sep 17 00:00:00 2001 From: Dundercover Date: Thu, 8 Nov 2018 20:31:54 +0100 Subject: [PATCH] Add `node.getNodesAtRange` (#2398) * Add `Node.getNodesAtRange` * Add tests for `Node.getNodesAtRange` --- docs/reference/slate/node.md | 6 ++ packages/slate/src/interfaces/element.js | 72 ++++++++++++++++++- .../get-nodes-at-range/multiple-blocks.js | 38 ++++++++++ ...ks-cursor-in-first-leaf-of-first-parent.js | 31 ++++++++ ...s-cursor-in-first-leaf-of-second-parent.js | 37 ++++++++++ ...s-cursor-in-second-leaf-of-first-parent.js | 35 +++++++++ ...s-selection-overlapping-multiple-blocks.js | 46 ++++++++++++ ...tion-overlapping-texts-in-second-parent.js | 44 ++++++++++++ ...ed-blocks-selection-spanning-first-text.js | 31 ++++++++ .../single-block-cursor-beginning-of-text.js | 28 ++++++++ .../single-block-cursor-end-of-text.js | 31 ++++++++ .../single-block-cursor-middle-of-text.js | 27 +++++++ .../single-block-with-inline.js | 27 +++++++ .../get-nodes-at-range/single-void-block.js | 26 +++++++ 14 files changed, 478 insertions(+), 1 deletion(-) create mode 100644 packages/slate/test/models/node/get-nodes-at-range/multiple-blocks.js create mode 100644 packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-first-leaf-of-first-parent.js create mode 100644 packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-first-leaf-of-second-parent.js create mode 100644 packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-second-leaf-of-first-parent.js create mode 100644 packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-overlapping-multiple-blocks.js create mode 100644 packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-overlapping-texts-in-second-parent.js create mode 100644 packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-spanning-first-text.js create mode 100644 packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-beginning-of-text.js create mode 100644 packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-end-of-text.js create mode 100644 packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-middle-of-text.js create mode 100644 packages/slate/test/models/node/get-nodes-at-range/single-block-with-inline.js create mode 100644 packages/slate/test/models/node/get-nodes-at-range/single-void-block.js diff --git a/docs/reference/slate/node.md b/docs/reference/slate/node.md index 7d262cf7c..250773695 100644 --- a/docs/reference/slate/node.md +++ b/docs/reference/slate/node.md @@ -253,6 +253,12 @@ Get the next [`Text`](./text.md) node after a descendant by `path` or `key`. Get a node in the tree by `path` or `key`. +### `getNodesAtRange` + +`getNodesAtRange(range: Range) => List` + +Get all of the nodes in a `range`. This includes all of the [`Text`](./text.md) nodes inside the range and all ancestors of those [`Text`](./text.md) nodes up to this node. + ### `getOffset` `getOffset(path: List|Array) => Number` diff --git a/packages/slate/src/interfaces/element.js b/packages/slate/src/interfaces/element.js index 58a392d6d..d443be5db 100644 --- a/packages/slate/src/interfaces/element.js +++ b/packages/slate/src/interfaces/element.js @@ -1,6 +1,6 @@ import direction from 'direction' import invariant from 'tiny-invariant' -import { List, OrderedSet, Set } from 'immutable' +import { List, OrderedSet, Set, Stack } from 'immutable' import mixin from '../utils/mixin' import Block from '../models/block' @@ -913,6 +913,75 @@ class ElementInterface { return text } + /** + * Get all of the nodes in a `range`. This includes all of the + * text nodes inside the range and all ancestors of those text + * nodes up to this node. + * + * @param {Range} range + * @return {List} + */ + + getNodesAtRange(range) { + range = this.resolveRange(range) + if (range.isUnset) return List() + const { start, end } = range + + // Do a depth-first stack-based search for all nodes in the range + // Nodes that are pushed to the stack are inside the range + + // Start with the nodes that are on the highest level in the tree + let stack = Stack( + this.nodes + .slice(start.path.get(0), end.path.get(0) + 1) + .map((node, index) => ({ + node, + onStartEdge: index === 0, + onEndEdge: index === end.path.get(0) - start.path.get(0), + relativeStartPath: start.path.slice(1), + relativeEndPath: end.path.slice(1), + })) + ) + + const result = [] + + while (stack.size > 0) { + const { + node, + onStartEdge, + onEndEdge, + relativeStartPath, + relativeEndPath, + } = stack.peek() + + stack = stack.shift() + result.push(node) + + if (node.object === 'text') continue + + // Modify indexes to exclude children that are outside of the range + const startIndex = onStartEdge ? relativeStartPath.get(0) : 0 + const endIndex = onEndEdge ? relativeEndPath.get(0) : node.nodes.size - 1 + + // Push children that are inside the range to the stack + stack = stack.pushAll( + node.nodes.slice(startIndex, endIndex + 1).map((n, i) => ({ + node: n, + onStartEdge: onStartEdge && i === 0, + onEndEdge: onEndEdge && i === endIndex - startIndex, + relativeStartPath: + onStartEdge && i === 0 ? relativeStartPath.slice(1) : null, + relativeEndPath: + onEndEdge && i === endIndex - startIndex + ? relativeEndPath.slice(1) + : null, + })) + ) + } + + return List(result) + } + /** * Get the offset for a descendant text node by `key`. * @@ -1797,6 +1866,7 @@ memoize(ElementInterface.prototype, [ 'getInlinesByTypeAsArray', 'getMarksAsArray', 'getMarksAtPosition', + 'getNodesAtRange', 'getOrderedMarksBetweenPositions', 'getInsertMarksAtRange', 'getMarksByTypeAsArray', diff --git a/packages/slate/test/models/node/get-nodes-at-range/multiple-blocks.js b/packages/slate/test/models/node/get-nodes-at-range/multiple-blocks.js new file mode 100644 index 000000000..39289d0d3 --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/multiple-blocks.js @@ -0,0 +1,38 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + one + + + + two + + + + + + + + three + + + four + + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] diff --git a/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-first-leaf-of-first-parent.js b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-first-leaf-of-first-parent.js new file mode 100644 index 000000000..3daf0a861 --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-first-leaf-of-first-parent.js @@ -0,0 +1,31 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + + + + one + + + + + + two + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['a', 'b', 'c', 'd'] diff --git a/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-first-leaf-of-second-parent.js b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-first-leaf-of-second-parent.js new file mode 100644 index 000000000..2d9dc6255 --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-first-leaf-of-second-parent.js @@ -0,0 +1,37 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + + + one + + + two + + + + + + three + + + + + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['a', 'g', 'h', 'i'] diff --git a/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-second-leaf-of-first-parent.js b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-second-leaf-of-first-parent.js new file mode 100644 index 000000000..a8315eb63 --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-cursor-in-second-leaf-of-first-parent.js @@ -0,0 +1,35 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + + + one + + + + + two + + + + + + three + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['a', 'b', 'e', 'f'] diff --git a/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-overlapping-multiple-blocks.js b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-overlapping-multiple-blocks.js new file mode 100644 index 000000000..ada57f8a7 --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-overlapping-multiple-blocks.js @@ -0,0 +1,46 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + + + one + + + + + + two + + + + + three + + + + + four + + + + five + + + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'] diff --git a/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-overlapping-texts-in-second-parent.js b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-overlapping-texts-in-second-parent.js new file mode 100644 index 000000000..c89b2eafc --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-overlapping-texts-in-second-parent.js @@ -0,0 +1,44 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + + + one + + + + + + two + + + + three + + + + four + + + + + + five + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['a', 'e', 'f', 'g', 'h', 'i', 'j', 'k'] diff --git a/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-spanning-first-text.js b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-spanning-first-text.js new file mode 100644 index 000000000..bf300c59f --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/nested-blocks-selection-spanning-first-text.js @@ -0,0 +1,31 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + + + + one + + + + + + two + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['a', 'b', 'c', 'd'] diff --git a/packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-beginning-of-text.js b/packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-beginning-of-text.js new file mode 100644 index 000000000..8c2cf67c7 --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-beginning-of-text.js @@ -0,0 +1,28 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + one + + + + + two + + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['c', 'd'] diff --git a/packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-end-of-text.js b/packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-end-of-text.js new file mode 100644 index 000000000..95cd00d9a --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-end-of-text.js @@ -0,0 +1,31 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + one + + + + two + + + + + three + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['c', 'd'] diff --git a/packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-middle-of-text.js b/packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-middle-of-text.js new file mode 100644 index 000000000..82834efe4 --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/single-block-cursor-middle-of-text.js @@ -0,0 +1,27 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + + one + + + + two + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['a', 'b'] diff --git a/packages/slate/test/models/node/get-nodes-at-range/single-block-with-inline.js b/packages/slate/test/models/node/get-nodes-at-range/single-block-with-inline.js new file mode 100644 index 000000000..c9cd8dd0c --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/single-block-with-inline.js @@ -0,0 +1,27 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + one + + + two + + + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['a', 'c', 'd'] diff --git a/packages/slate/test/models/node/get-nodes-at-range/single-void-block.js b/packages/slate/test/models/node/get-nodes-at-range/single-void-block.js new file mode 100644 index 000000000..a3c88b465 --- /dev/null +++ b/packages/slate/test/models/node/get-nodes-at-range/single-void-block.js @@ -0,0 +1,26 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + + + + + + + + +) + +export default function({ document, selection }) { + return document + .getNodesAtRange(selection) + .map(n => n.key) + .toArray() +} + +export const output = ['a', 'b']