mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-24 07:52:50 +02:00
Fix move_node dirtyPath and PathUtils.transform calculations (#2593)
* test and changes to test runner for move-node edge case * fix dirtyPaths calculation for move_node * prettier * remove commented out code, remove needless toArray call * remove test assertion * more lint fixes * possibly correct test for deleting all nested blocks * pretty test * fix PathUtils.transform edge case with move_node * tests for PathUtils.transform for some move_node cases * prettier * element moveNode also uses PathUtils.transform, fix sibling edge case * refactor for ease of comprehension * new edge case test, fix move_node invert * prettier * revert last move_node transform case for clarity
This commit is contained in:
@@ -524,29 +524,21 @@ function getDirtyPaths(operation) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'move_node': {
|
case 'move_node': {
|
||||||
let parentPath = PathUtils.lift(path)
|
|
||||||
let newParentPath = PathUtils.lift(newPath)
|
|
||||||
|
|
||||||
if (PathUtils.isEqual(path, newPath)) {
|
if (PathUtils.isEqual(path, newPath)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK: this clause only exists because the `move_path` logic isn't
|
const oldAncestors = PathUtils.getAncestors(path).reduce((arr, p) => {
|
||||||
// consistent when it deals with siblings.
|
arr.push(...PathUtils.transform(p, operation).toArray())
|
||||||
if (!PathUtils.isSibling(path, newPath)) {
|
return arr
|
||||||
if (newParentPath.size && PathUtils.isYounger(path, newPath)) {
|
}, [])
|
||||||
newParentPath = PathUtils.decrement(newParentPath, 1, path.size - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentPath.size && PathUtils.isYounger(newPath, path)) {
|
const newAncestors = PathUtils.getAncestors(newPath).reduce((arr, p) => {
|
||||||
parentPath = PathUtils.increment(parentPath, 1, newPath.size - 1)
|
arr.push(...PathUtils.transform(p, operation).toArray())
|
||||||
}
|
return arr
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const oldAncestors = PathUtils.getAncestors(parentPath).toArray()
|
return [...oldAncestors, ...newAncestors]
|
||||||
const newAncestors = PathUtils.getAncestors(newParentPath).toArray()
|
|
||||||
|
|
||||||
return [...oldAncestors, parentPath, ...newAncestors, newParentPath]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'remove_node': {
|
case 'remove_node': {
|
||||||
|
@@ -14,6 +14,7 @@ import Point from '../models/point'
|
|||||||
import Range from '../models/range'
|
import Range from '../models/range'
|
||||||
import Selection from '../models/selection'
|
import Selection from '../models/selection'
|
||||||
import Value from '../models/value'
|
import Value from '../models/value'
|
||||||
|
import Operation from '../models/operation'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The interface that `Document`, `Block` and `Inline` all implement, to make
|
* The interface that `Document`, `Block` and `Inline` all implement, to make
|
||||||
@@ -1763,13 +1764,15 @@ class ElementInterface {
|
|||||||
const newParentPath = PathUtils.lift(newPath)
|
const newParentPath = PathUtils.lift(newPath)
|
||||||
this.assertNode(newParentPath)
|
this.assertNode(newParentPath)
|
||||||
|
|
||||||
const position = PathUtils.compare(path, newPath)
|
// TODO: this is a bit hacky, re-creating the operation that led to this method being called
|
||||||
|
// Alternative 1: pass the operation through from apply -> value.moveNode
|
||||||
// If the old path ends above and before a node in the new path, then
|
// Alternative 2: add a third property to the operation called "transformedNewPath", pass that through
|
||||||
// removing it will alter the target, so we need to adjust the new path.
|
const op = Operation.create({
|
||||||
if (path.size < newPath.size && position === -1) {
|
type: 'move_node',
|
||||||
newPath = PathUtils.decrement(newPath, 1, path.size - 1)
|
path,
|
||||||
}
|
newPath,
|
||||||
|
})
|
||||||
|
newPath = PathUtils.transform(path, op).first()
|
||||||
|
|
||||||
let ret = this
|
let ret = this
|
||||||
ret = ret.removeNode(path)
|
ret = ret.removeNode(path)
|
||||||
|
@@ -42,23 +42,21 @@ function invertOperation(op) {
|
|||||||
return op
|
return op
|
||||||
}
|
}
|
||||||
|
|
||||||
let inversePath = newPath
|
// Get the true path that the moved node ended up at
|
||||||
let inverseNewPath = path
|
const inversePath = PathUtils.transform(path, op).first()
|
||||||
|
|
||||||
const position = PathUtils.compare(path, newPath)
|
// Get the true path we are trying to move back to
|
||||||
|
// We transform the right-sibling of the path
|
||||||
|
// This will end up at the operation.path most of the time
|
||||||
|
// But if the newPath is a left-sibling or left-ancestor-sibling, this will account for it
|
||||||
|
const transformedSibling = PathUtils.transform(
|
||||||
|
PathUtils.increment(path),
|
||||||
|
op
|
||||||
|
).first()
|
||||||
|
|
||||||
// If the node's old position was a left sibling of an ancestor of
|
const inverse = op
|
||||||
// its new position, we need to adjust part of the path by -1.
|
.set('path', inversePath)
|
||||||
// If the node's new position is an ancestor of the old position,
|
.set('newPath', transformedSibling)
|
||||||
// or a left sibling of an ancestor of its old position, we need
|
|
||||||
// to adjust part of the path by 1.
|
|
||||||
if (path.size < newPath.size && position === -1) {
|
|
||||||
inversePath = PathUtils.decrement(newPath, 1, path.size - 1)
|
|
||||||
} else if (path.size > newPath.size && position !== -1) {
|
|
||||||
inverseNewPath = PathUtils.increment(path, 1, newPath.size - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const inverse = op.set('path', inversePath).set('newPath', inverseNewPath)
|
|
||||||
return inverse
|
return inverse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -84,11 +84,11 @@ function decrement(path, n = 1, index = path.size - 1) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function getAncestors(path) {
|
function getAncestors(path) {
|
||||||
let ancestors = new List()
|
const ancestors = List().withMutations(list => {
|
||||||
|
for (let i = 0; i < path.size; i++) {
|
||||||
for (let i = 0; i < path.size; i++) {
|
list.push(path.slice(0, i))
|
||||||
ancestors = ancestors.push(path.slice(0, i))
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
return ancestors
|
return ancestors
|
||||||
}
|
}
|
||||||
@@ -340,35 +340,28 @@ function transform(path, operation) {
|
|||||||
|
|
||||||
if (type === 'move_node') {
|
if (type === 'move_node') {
|
||||||
const { newPath: np } = operation
|
const { newPath: np } = operation
|
||||||
const npIndex = np.size - 1
|
|
||||||
const npEqual = isEqual(np, path)
|
|
||||||
|
|
||||||
if (isEqual(p, np)) {
|
if (isEqual(p, np)) {
|
||||||
return List([path])
|
return List([path])
|
||||||
}
|
}
|
||||||
|
|
||||||
const npYounger = isYounger(np, path)
|
if (pAbove || pEqual) {
|
||||||
const npAbove = isAbove(np, path)
|
// We are comparing something that was moved
|
||||||
|
// The new path is unaffected unless the old path was the left-sibling of an ancestor
|
||||||
if (pAbove) {
|
if (isYounger(p, np) && p.size < np.size) {
|
||||||
if (isAfter(np, p)) {
|
|
||||||
path = decrement(np, 1, min(np, p) - 1).concat(path.slice(p.size))
|
path = decrement(np, 1, min(np, p) - 1).concat(path.slice(p.size))
|
||||||
} else {
|
} else {
|
||||||
path = np.concat(path.slice(p.size))
|
path = np.concat(path.slice(p.size))
|
||||||
}
|
}
|
||||||
} else if (pEqual) {
|
|
||||||
if (isAfter(np, p)) {
|
|
||||||
path = decrement(np, 1, min(np, p) - 1)
|
|
||||||
} else {
|
|
||||||
path = np
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
// This is equivalent logic to remove_node for path
|
||||||
if (pYounger) {
|
if (pYounger) {
|
||||||
path = decrement(path, 1, pIndex)
|
path = decrement(path, 1, pIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (npEqual || npYounger || npAbove) {
|
// This is the equivalent logic to insert_node for newPath
|
||||||
path = increment(path, 1, npIndex)
|
if (isYounger(np, path) || isEqual(np, path) || isAbove(np, path)) {
|
||||||
|
path = increment(path, 1, np.size - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,62 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
import { Block, Text } from 'slate'
|
||||||
|
|
||||||
|
function normalizeNode(node, editor, next) {
|
||||||
|
if (node.type === 'container' && node.nodes.first().type === 'container') {
|
||||||
|
return () =>
|
||||||
|
editor.insertNodeByKey(
|
||||||
|
node.key,
|
||||||
|
0,
|
||||||
|
Block.create({
|
||||||
|
type: 'paragraph',
|
||||||
|
nodes: [Text.create()],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const plugins = [{ normalizeNode }]
|
||||||
|
|
||||||
|
export default function(editor) {
|
||||||
|
editor.deleteBackward()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<block type="container" key="c1">
|
||||||
|
<paragraph key="p1">1 </paragraph>
|
||||||
|
<block type="container" key="c2">
|
||||||
|
<paragraph key="p2">
|
||||||
|
<cursor />1.1
|
||||||
|
</paragraph>
|
||||||
|
<block type="container" key="c3">
|
||||||
|
<paragraph key="p3">1.1.1</paragraph>
|
||||||
|
</block>
|
||||||
|
</block>
|
||||||
|
</block>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<block type="container" key="c1">
|
||||||
|
<paragraph key="p1">
|
||||||
|
1 <cursor />1.1
|
||||||
|
</paragraph>
|
||||||
|
<block type="container" key="c2">
|
||||||
|
<paragraph />
|
||||||
|
<block type="container" key="c3">
|
||||||
|
<paragraph key="p3">1.1.1</paragraph>
|
||||||
|
</block>
|
||||||
|
</block>
|
||||||
|
</block>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
@@ -0,0 +1,44 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../../helpers/h'
|
||||||
|
|
||||||
|
export default function(editor) {
|
||||||
|
editor.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<block type="container">
|
||||||
|
<paragraph key="original paragraph">
|
||||||
|
<anchor />one
|
||||||
|
</paragraph>
|
||||||
|
<block type="container">
|
||||||
|
<paragraph> two</paragraph>
|
||||||
|
<paragraph> three</paragraph>
|
||||||
|
<block type="container">
|
||||||
|
<paragraph> four</paragraph>
|
||||||
|
<block type="container">
|
||||||
|
<paragraph>
|
||||||
|
{' '}
|
||||||
|
five<focus />
|
||||||
|
</paragraph>
|
||||||
|
</block>
|
||||||
|
</block>
|
||||||
|
</block>
|
||||||
|
</block>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<block type="container">
|
||||||
|
<paragraph key="original paragraph">
|
||||||
|
<cursor />
|
||||||
|
</paragraph>
|
||||||
|
</block>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
47
packages/slate/test/history/undo/insert-fragment.js
Normal file
47
packages/slate/test/history/undo/insert-fragment.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import h from '../../helpers/h'
|
||||||
|
|
||||||
|
const fragment = (
|
||||||
|
<document>
|
||||||
|
<block type="d">
|
||||||
|
<paragraph>A</paragraph>
|
||||||
|
<block type="c">
|
||||||
|
<block type="d">
|
||||||
|
<paragraph>B</paragraph>
|
||||||
|
<paragraph>
|
||||||
|
<block type="d">
|
||||||
|
<paragraph>C</paragraph>
|
||||||
|
</block>
|
||||||
|
</paragraph>
|
||||||
|
</block>
|
||||||
|
<block type="d">
|
||||||
|
<paragraph>D</paragraph>
|
||||||
|
</block>
|
||||||
|
</block>
|
||||||
|
</block>
|
||||||
|
</document>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default function(editor) {
|
||||||
|
editor
|
||||||
|
.insertFragment(fragment)
|
||||||
|
.flush()
|
||||||
|
.undo()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const input = (
|
||||||
|
<value>
|
||||||
|
<document>
|
||||||
|
<block type="d">
|
||||||
|
<paragraph>
|
||||||
|
<text>
|
||||||
|
<cursor />
|
||||||
|
</text>
|
||||||
|
</paragraph>
|
||||||
|
</block>
|
||||||
|
</document>
|
||||||
|
</value>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const output = input
|
@@ -112,9 +112,11 @@ describe('slate', () => {
|
|||||||
// editor doesn't! It needs to live in the tests instead.
|
// editor doesn't! It needs to live in the tests instead.
|
||||||
|
|
||||||
fixtures(__dirname, 'commands', ({ module }) => {
|
fixtures(__dirname, 'commands', ({ module }) => {
|
||||||
const { input, output, options = {} } = module
|
const { input, output, options = {}, plugins: module_plugins } = module
|
||||||
const fn = module.default
|
const fn = module.default
|
||||||
const editor = new Editor({ plugins })
|
const editor = new Editor({
|
||||||
|
plugins: module_plugins ? plugins.concat(module_plugins) : plugins,
|
||||||
|
})
|
||||||
const opts = { preserveSelection: true, ...options }
|
const opts = { preserveSelection: true, ...options }
|
||||||
|
|
||||||
editor.setValue(input)
|
editor.setValue(input)
|
||||||
@@ -158,4 +160,9 @@ describe('slate', () => {
|
|||||||
|
|
||||||
assert.deepEqual(actual, expected)
|
assert.deepEqual(actual, expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fixtures(__dirname, 'utils/path-utils', ({ module }) => {
|
||||||
|
const fn = module.default
|
||||||
|
fn()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
import assert from 'assert'
|
||||||
|
import { PathUtils, Operation } from 'slate'
|
||||||
|
|
||||||
|
const assertEqualPaths = (p1, p2) =>
|
||||||
|
assert.deepEqual(p1.toArray(), p2.toArray())
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const path = PathUtils.create([0, 1, 0, 1])
|
||||||
|
const newPath = PathUtils.create([0, 1, 1, 0, 1])
|
||||||
|
const op = Operation.create({ path, newPath, type: 'move_node' })
|
||||||
|
|
||||||
|
const result_eq = PathUtils.transform(path, op).first()
|
||||||
|
assertEqualPaths(result_eq, newPath)
|
||||||
|
|
||||||
|
const before_old = PathUtils.create([0, 1, 0, 0])
|
||||||
|
const result_before_old = PathUtils.transform(before_old, op).first()
|
||||||
|
assertEqualPaths(result_before_old, before_old)
|
||||||
|
|
||||||
|
const after_old = PathUtils.create([0, 1, 0, 2])
|
||||||
|
const result_after_old = PathUtils.transform(after_old, op).first()
|
||||||
|
assertEqualPaths(result_after_old, PathUtils.decrement(after_old))
|
||||||
|
|
||||||
|
const before_new = PathUtils.create([0, 1, 1, 0, 0])
|
||||||
|
const result_before_new = PathUtils.transform(before_new, op).first()
|
||||||
|
assertEqualPaths(result_before_new, before_new)
|
||||||
|
|
||||||
|
const after_new = PathUtils.create([0, 1, 1, 0, 2])
|
||||||
|
const result_after_new = PathUtils.transform(after_new, op).first()
|
||||||
|
assertEqualPaths(result_after_new, PathUtils.increment(after_new))
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
import assert from 'assert'
|
||||||
|
import { PathUtils, Operation } from 'slate'
|
||||||
|
|
||||||
|
const assertEqualPaths = (p1, p2) =>
|
||||||
|
assert.deepEqual(p1.toArray(), p2.toArray())
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const path = PathUtils.create([0, 1, 1])
|
||||||
|
const newPath = PathUtils.create([0, 1, 2, 0, 1])
|
||||||
|
const op = Operation.create({ path, newPath, type: 'move_node' })
|
||||||
|
|
||||||
|
const result_eq = PathUtils.transform(path, op).first()
|
||||||
|
assertEqualPaths(result_eq, PathUtils.create([0, 1, 1, 0, 1]))
|
||||||
|
|
||||||
|
const before_old = PathUtils.create([0, 1, 0])
|
||||||
|
const result_before_old = PathUtils.transform(before_old, op).first()
|
||||||
|
assertEqualPaths(result_before_old, before_old)
|
||||||
|
|
||||||
|
const after_old = PathUtils.create([0, 1, 2])
|
||||||
|
const result_after_old = PathUtils.transform(after_old, op).first()
|
||||||
|
assertEqualPaths(result_after_old, PathUtils.decrement(after_old))
|
||||||
|
|
||||||
|
const before_new = PathUtils.create([0, 1, 2, 0, 0])
|
||||||
|
const result_before_new = PathUtils.transform(before_new, op).first()
|
||||||
|
assertEqualPaths(result_before_new, PathUtils.create([0, 1, 1, 0, 0]))
|
||||||
|
|
||||||
|
const after_new = PathUtils.create([0, 1, 2, 0, 2])
|
||||||
|
const result_after_new = PathUtils.transform(after_new, op).first()
|
||||||
|
assertEqualPaths(result_after_new, PathUtils.create([0, 1, 1, 0, 2]))
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
import assert from 'assert'
|
||||||
|
import { PathUtils, Operation } from 'slate'
|
||||||
|
|
||||||
|
const assertEqualPaths = (p1, p2) =>
|
||||||
|
assert.deepEqual(p1.toArray(), p2.toArray())
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const path = PathUtils.create([1])
|
||||||
|
const newPath = PathUtils.create([0])
|
||||||
|
const op = Operation.create({ path, newPath, type: 'move_node' })
|
||||||
|
|
||||||
|
const moved_node_result = PathUtils.transform(path, op).first()
|
||||||
|
assertEqualPaths(moved_node_result, newPath)
|
||||||
|
|
||||||
|
const sibling_result = PathUtils.transform(newPath, op).first()
|
||||||
|
assertEqualPaths(sibling_result, path)
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
import assert from 'assert'
|
||||||
|
import { PathUtils, Operation } from 'slate'
|
||||||
|
|
||||||
|
const assertEqualPaths = (p1, p2) =>
|
||||||
|
assert.deepEqual(p1.toArray(), p2.toArray())
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const path = PathUtils.create([0])
|
||||||
|
const newPath = PathUtils.create([1])
|
||||||
|
const op = Operation.create({ path, newPath, type: 'move_node' })
|
||||||
|
|
||||||
|
const moved_node_result = PathUtils.transform(path, op).first()
|
||||||
|
assertEqualPaths(moved_node_result, newPath)
|
||||||
|
|
||||||
|
const sibling_result = PathUtils.transform(newPath, op).first()
|
||||||
|
assertEqualPaths(sibling_result, path)
|
||||||
|
}
|
Reference in New Issue
Block a user