mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-19 05:31:56 +02:00
refactor core schema to remove last for void nodes
This commit is contained in:
@@ -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',
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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 })
|
||||
|
@@ -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`.
|
||||
*
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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()
|
||||
)
|
||||
|
Reference in New Issue
Block a user