diff --git a/src/models/node.js b/src/models/node.js index 10070fbe6..00dd79b45 100644 --- a/src/models/node.js +++ b/src/models/node.js @@ -1105,6 +1105,17 @@ const Node = { return !!this.getDescendant(key) }, + /** + * Recursively check if a node exists by `key`. + * + * @param {String} key + * @return {Boolean} + */ + + hasNode(key) { + return !!this.getNode(key) + }, + /** * Check if a node has a void parent by `key`. * @@ -1480,6 +1491,7 @@ memoize(Node, [ 'getNextBlock', 'getNextSibling', 'getNextText', + 'getNode', 'getOffset', 'getOffsetAtRange', 'getParent', diff --git a/src/models/selection.js b/src/models/selection.js index 7fd42e167..dc3ec046a 100644 --- a/src/models/selection.js +++ b/src/models/selection.js @@ -105,6 +105,16 @@ class Selection extends new Record(DEFAULTS) { return this.isBackward == null ? null : !this.isBackward } + /** + * Check whether the selection's keys are set. + * + * @return {Boolean} + */ + + get isSet() { + return this.anchorKey != null && this.focusKey != null + } + /** * Check whether the selection's keys are not set. * @@ -112,7 +122,7 @@ class Selection extends new Record(DEFAULTS) { */ get isUnset() { - return this.anchorKey == null || this.focusKey == null + return !this.isSet } /** diff --git a/src/models/state.js b/src/models/state.js index 480d695e2..183c8a63e 100644 --- a/src/models/state.js +++ b/src/models/state.js @@ -57,6 +57,7 @@ class State extends new Record(DEFAULTS) { } const state = new State({ document, selection }) + return state.transform({ normalized: false }) .normalize(SCHEMA) .apply({ save: false }) diff --git a/src/models/text.js b/src/models/text.js index 0b82f8433..111ba4efd 100644 --- a/src/models/text.js +++ b/src/models/text.js @@ -190,6 +190,19 @@ class Text extends new Record(DEFAULTS) { return char.marks } + /** + * Get a node by `key`, to parallel other nodes. + * + * @param {String} key + * @return {Node|Null} + */ + + getNode(key) { + return this.key == key + ? this + : null + } + /** * Derive the ranges for a list of `characters`. * @@ -240,6 +253,17 @@ class Text extends new Record(DEFAULTS) { return ranges } + /** + * Check if the node has a node by `key`, to parallel other nodes. + * + * @param {String} key + * @return {Boolean} + */ + + hasNode(key) { + return !!this.getNode(key) + } + /** * Insert `text` at `index`. * diff --git a/src/schemas/core.js b/src/schemas/core.js index c4b653acf..9736fb405 100644 --- a/src/schemas/core.js +++ b/src/schemas/core.js @@ -3,6 +3,14 @@ import Schema from '../models/schema' import Text from '../models/text' import { List } from 'immutable' +/** + * Options object with normalize set to `false`. + * + * @type {Object} + */ + +const OPTS = { normalize: false } + /** * Only allow block nodes in documents. * @@ -19,7 +27,7 @@ const DOCUMENT_CHILDREN_RULE = { return invalids.size ? invalids : null }, normalize: (transform, document, invalids) => { - return invalids.reduce((t, n) => t.removeNodeByKey(n.key, { normalize: false }), transform) + return invalids.reduce((t, n) => t.removeNodeByKey(n.key, OPTS), transform) } } @@ -39,7 +47,7 @@ const BLOCK_CHILDREN_RULE = { return invalids.size ? invalids : null }, normalize: (transform, block, invalids) => { - return invalids.reduce((t, n) => t.removeNodeByKey(n.key, { normalize: false }), transform) + return invalids.reduce((t, n) => t.removeNodeByKey(n.key, OPTS), transform) } } @@ -59,7 +67,7 @@ const MIN_TEXT_RULE = { }, normalize: (transform, node) => { const text = Text.create() - return transform.insertNodeByKey(node.key, 0, text, { normalize: false }) + return transform.insertNodeByKey(node.key, 0, text, OPTS) } } @@ -79,7 +87,7 @@ const INLINE_CHILDREN_RULE = { return invalids.size ? invalids : null }, normalize: (transform, inline, invalids) => { - return invalids.reduce((t, n) => t.removeNodeByKey(n.key, { normalize: false }), transform) + return invalids.reduce((t, n) => t.removeNodeByKey(n.key, OPTS), transform) } } @@ -107,8 +115,8 @@ const INLINE_NO_EMPTY = { return block.nodes.reduce((tr, child, index) => { if (child.kind == 'inline' && child.text == '') { return transform - .removeNodeByKey(child.key, { normalize: false }) - .insertNodeByKey(block.key, index, Text.createFromString(''), { normalize: false }) + .removeNodeByKey(child.key, OPTS) + .insertNodeByKey(block.key, index, Text.createFromString(''), OPTS) } else { return tr } @@ -117,24 +125,35 @@ const INLINE_NO_EMPTY = { } /** - * Ensure that void nodes contain a single space of content. + * Ensure that void nodes contain a text node with a single space of text. * * @type {Object} */ const VOID_TEXT_RULE = { match: (object) => { - return (object.kind == 'inline' || object.kind == 'block') && object.isVoid + return ( + (object.kind == 'inline' || object.kind == 'block') && + (object.isVoid) + ) }, validate: (node) => { - return node.text !== ' ' || node.nodes.size !== 1 + return ( + node.text !== ' ' || + node.nodes.size !== 1 + ) }, normalize: (transform, node, result) => { - node.nodes.reduce((t, child) => { - return t.removeNodeByKey(child.key, { normalize: false }) - }, transform) + const text = Text.createFromString(' ') + const index = node.nodes.size - return transform.insertNodeByKey(node.key, 0, Text.createFromString(' '), { normalize: false }) + transform.insertNodeByKey(node.key, index, text, OPTS) + + node.nodes.forEach((child) => { + transform.removeNodeByKey(child.key, OPTS) + }) + + return transform } } @@ -175,11 +194,11 @@ const INLINE_VOID_TEXTS_AROUND_RULE = { return invalids.reduce((t, { index, next, prev }) => { if (prev) { - t = t.insertNodeByKey(block.key, shift + index, Text.create(), { normalize: false }) + t = t.insertNodeByKey(block.key, shift + index, Text.create(), OPTS) shift = shift + 1 } if (next) { - t = t.insertNodeByKey(block.key, shift + index + 1, Text.create(), { normalize: false }) + t = t.insertNodeByKey(block.key, shift + index + 1, Text.create(), OPTS) shift = shift + 1 } @@ -219,7 +238,7 @@ const NO_ADJACENT_TEXT_RULE = { .reverse() .reduce((t, pair) => { const [ first, second ] = pair - return t.joinNodeByKey(second.key, first.key, { normalize: false }) + return t.joinNodeByKey(second.key, first.key, OPTS) }, transform) } } @@ -273,7 +292,7 @@ const NO_EMPTY_TEXT_RULE = { }, normalize: (transform, node, invalids) => { return invalids.reduce((t, text) => { - return t.removeNodeByKey(text.key, { normalize: false }) + return t.removeNodeByKey(text.key, OPTS) }, transform) } } diff --git a/src/transforms/apply-operation.js b/src/transforms/apply-operation.js index b7c8b1fe6..d31ba66b2 100644 --- a/src/transforms/apply-operation.js +++ b/src/transforms/apply-operation.js @@ -231,54 +231,50 @@ function removeNode(state, operation) { const { path } = operation let { document, selection } = state const { startKey, endKey } = selection - - // Preserve previous document - const prevDocument = document - - // Update the document const node = document.assertPath(path) + + // If the selection is set, check to see if it needs to be updated. + if (selection.isSet) { + const hasStartNode = node.hasNode(startKey) + const hasEndNode = node.hasNode(endKey) + + // If one of the selection's nodes is being removed, we need to update it. + if (hasStartNode) { + const prev = document.getPreviousText(startKey) + const next = document.getNextText(startKey) + + if (prev) { + selection = selection.moveStartTo(prev.key, prev.length) + } else if (next) { + selection = selection.moveStartTo(next.key, 0) + } else { + selection = selection.unset() + } + } + + if (hasEndNode) { + const prev = document.getPreviousText(endKey) + const next = document.getNextText(endKey) + + if (prev) { + selection = selection.moveEndTo(prev.key, prev.length) + } else if (next) { + selection = selection.moveEndTo(next.key, 0) + } else { + selection = selection.unset() + } + } + + } + + // Remove the node from the document. let parent = document.getParent(node.key) const index = parent.nodes.indexOf(node) const isParent = document == parent parent = parent.removeNode(index) document = isParent ? parent : document.updateDescendant(parent) - function getRemoved(key) { - if (key === node.key) return node - if (node.kind == 'text') return null - return node.getDescendant(key) - } - - // Update the selection, if one of the anchor/focus has been removed - const startDesc = startKey ? getRemoved(startKey) : null - const endDesc = endKey ? getRemoved(endKey) : null - - if (startDesc) { - const prevText = prevDocument.getTexts() - .takeUntil(text => text.key == startKey) - .filter(text => !getRemoved(text.key)) - .last() - selection = !prevText - ? selection.unset() - : selection.moveStartTo(prevText.key, prevText.length) - } - if (endDesc) { - // The whole selection is inside the node, we collapse to the previous text node - if (startKey == endKey) { - selection = selection.collapseToStart() - } else { - const nextText = prevDocument.getTexts() - .skipUntil(text => text.key == startKey) - .slice(1) - .filter(text => !getRemoved(text.key)) - .first() - - selection = !nextText - ? selection.unset() - : selection.moveEndTo(nextText.key, 0) - } - } - + // Update the document and selection. state = state.merge({ document, selection }) return state } diff --git a/src/transforms/normalize.js b/src/transforms/normalize.js index 56e3ca69c..c3f193b1f 100644 --- a/src/transforms/normalize.js +++ b/src/transforms/normalize.js @@ -12,9 +12,9 @@ import warn from '../utils/warn' */ export function normalize(transform, schema) { + transform.normalizeDocument(schema) + transform.normalizeSelection(schema) return transform - .normalizeDocument(schema) - .normalizeSelection(schema) } /** @@ -28,7 +28,8 @@ export function normalize(transform, schema) { export function normalizeDocument(transform, schema) { const { state } = transform const { document } = state - return transform.normalizeNodeByKey(document.key, schema) + transform.normalizeNodeByKey(document.key, schema) + return transform } /** diff --git a/test/transforms/fixtures/at-current-range/set-inline/with-is-void/index.js b/test/transforms/fixtures/at-current-range/set-inline/with-is-void/index.js index 48d39e192..6393611dd 100644 --- a/test/transforms/fixtures/at-current-range/set-inline/with-is-void/index.js +++ b/test/transforms/fixtures/at-current-range/set-inline/with-is-void/index.js @@ -4,7 +4,7 @@ import assert from 'assert' export default function (state) { const { document, selection } = state const texts = document.getTexts() - let first = texts.first() + const first = texts.first() const range = selection.merge({ anchorKey: first.key, anchorOffset: 0, @@ -21,14 +21,14 @@ export default function (state) { }) .apply() - // Selection is reset, in theory it should me on the emoji - first = next.document.getTexts().first() + const updated = next.document.getTexts().get(1) + assert.deepEqual( next.selection.toJS(), range.merge({ - anchorKey: first.key, + anchorKey: updated.key, anchorOffset: 0, - focusKey: first.key, + focusKey: updated.key, focusOffset: 0 }).toJS() )