From 583b560ec3f3b79882da5475af49ca969778b907 Mon Sep 17 00:00:00 2001 From: Soreine Date: Wed, 9 Nov 2016 12:16:13 +0100 Subject: [PATCH] Add Node.splitNodeAfter, to allow for exact undo of `joinNode` --- src/models/node.js | 36 +++++++++- src/transforms/apply-operation.js | 115 +++++++++++++++++------------- src/transforms/operations.js | 25 +++++-- 3 files changed, 118 insertions(+), 58 deletions(-) diff --git a/src/models/node.js b/src/models/node.js index 517316813..e528b196e 100644 --- a/src/models/node.js +++ b/src/models/node.js @@ -1259,7 +1259,7 @@ const Node = { /** * Split a node by `path` at `offset`. * - * @param {String} path + * @param {Array} path * @param {Number} offset * @return {Node} */ @@ -1312,6 +1312,40 @@ const Node = { return base }, + /** + * Split a node by `path` after 'count' children. + * Does not work on Text nodes. Use `Node.splitNode` to split text nodes as well. + * + * @param {Array} path + * @param {Number} count + * @return {Node} + */ + + splitNodeAfter(path, count) { + let base = this + let node = base.assertPath(path) + if (node.kind === 'text') throw new Error('Cannot split text node at index. Use Node.splitNode at offset instead') + const { nodes } = node + + let parent = base.getParent(node.key) + const isParent = base == parent + + const oneNodes = nodes.take(count) + const twoNodes = nodes.skip(count) + + const one = node.merge({ nodes: oneNodes }) + const two = node.merge({ nodes: twoNodes, key: uid() }) + + + const nodeIndex = parent.nodes.indexOf(node) + parent = parent.removeNode(nodeIndex) + parent = parent.insertNode(nodeIndex, two) + parent = parent.insertNode(nodeIndex, one) + + base = isParent ? parent : base.updateDescendant(parent) + return base + }, + /** * Split the block nodes at a `range`, to optional `height`. * diff --git a/src/transforms/apply-operation.js b/src/transforms/apply-operation.js index 564ee7898..f288dc8bc 100644 --- a/src/transforms/apply-operation.js +++ b/src/transforms/apply-operation.js @@ -392,63 +392,76 @@ function setSelection(state, operation) { * * @param {State} state * @param {Object} operation + * @param {Array} operation.path The path of the node to split + * @param {Number} operation.offset (optional) Split using a relative offset + * @param {Number} operation.count (optional) Split after `count` + * children. Cannot be used in combination with offset. * @return {State} */ function splitNode(state, operation) { - const { path, offset } = operation + const { path, offset, count } = operation const { document } = state - // Update document - const newDocument = document.splitNode(path, offset) - - // Update selection - let { selection } = state - const { anchorKey, anchorOffset, focusKey, focusOffset } = selection - - const node = document.assertPath(path) - // The text node that was split - const splittedText = node.kind == 'text' - ? node - : node.getTextAtOffset(offset) - const textOffset = node.kind == 'text' - ? offset - : offset - node.getOffset(splittedText.key) - - // Should we update the selection ? - const shouldUpdateAnchor = splittedText.key == anchorKey && textOffset <= anchorOffset - const shouldUpdateFocus = splittedText.key == focusKey && textOffset <= focusOffset - if (shouldUpdateFocus || shouldUpdateAnchor) { - // The node next to `node`, resulting from the split - const secondNode = newDocument.getNextSibling(node.key) - let secondText, newOffset - - if (shouldUpdateAnchor) { - newOffset = anchorOffset - textOffset - secondText = secondNode.kind == 'text' - ? secondNode - : secondNode.getTextAtOffset(newOffset) - selection = selection.merge({ - anchorKey: secondText.key, - anchorOffset: newOffset - }) - } - - if (shouldUpdateFocus) { - newOffset = focusOffset - textOffset - secondText = secondNode.kind == 'text' - ? secondNode - : secondNode.getTextAtOffset(newOffset) - selection = selection.merge({ - focusKey: secondText.key, - focusOffset: newOffset - }) - } + if (offset === undefined) { + return state.merge({ + document: document.splitNodeAfter(path, count) + // No need to update selection + }) } - state = state.merge({ - document: newDocument, - selection - }) - return state + else { + // Update document + let newDocument = document.splitNode(path, offset) + + // Update selection + let { selection } = state + const { anchorKey, anchorOffset, focusKey, focusOffset } = selection + + const node = document.assertPath(path) + // The text node that was split + const splittedText = node.kind == 'text' + ? node + : node.getTextAtOffset(offset) + const textOffset = node.kind == 'text' + ? offset + : offset - node.getOffset(splittedText.key) + + // Should we update the selection ? + const shouldUpdateAnchor = splittedText.key == anchorKey && textOffset <= anchorOffset + const shouldUpdateFocus = splittedText.key == focusKey && textOffset <= focusOffset + if (shouldUpdateFocus || shouldUpdateAnchor) { + // The node next to `node`, resulting from the split + const secondNode = newDocument.getNextSibling(node.key) + let secondText, newOffset + + if (shouldUpdateAnchor) { + newOffset = anchorOffset - textOffset + secondText = secondNode.kind == 'text' + ? secondNode + : secondNode.getTextAtOffset(newOffset) + selection = selection.merge({ + anchorKey: secondText.key, + anchorOffset: newOffset + }) + } + + if (shouldUpdateFocus) { + newOffset = focusOffset - textOffset + secondText = secondNode.kind == 'text' + ? secondNode + : secondNode.getTextAtOffset(newOffset) + selection = selection.merge({ + focusKey: secondText.key, + focusOffset: newOffset + }) + } + } + + state = state.merge({ + document: newDocument, + selection + }) + return state + } } diff --git a/src/transforms/operations.js b/src/transforms/operations.js index 4766f3341..52aa83a17 100644 --- a/src/transforms/operations.js +++ b/src/transforms/operations.js @@ -106,13 +106,26 @@ export function joinNodeOperation(transform, path, withPath) { const { state } = transform const { document } = state const node = document.assertPath(withPath) - const offset = node.length - const inverse = [{ - type: 'split_node', - path: withPath, - offset, - }] + let inverse + if (node.kind === 'text') { + const offset = node.length + + inverse = [{ + type: 'split_node', + path: withPath, + offset, + }] + } else { + // The number of children after which we split + const count = node.nodes.count() + + inverse = [{ + type: 'split_node', + path: withPath, + count, + }] + } const operation = { type: 'join_node',