mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-20 06:01:24 +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)
|
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`.
|
* Check if a node has a void parent by `key`.
|
||||||
*
|
*
|
||||||
@@ -1480,6 +1491,7 @@ memoize(Node, [
|
|||||||
'getNextBlock',
|
'getNextBlock',
|
||||||
'getNextSibling',
|
'getNextSibling',
|
||||||
'getNextText',
|
'getNextText',
|
||||||
|
'getNode',
|
||||||
'getOffset',
|
'getOffset',
|
||||||
'getOffsetAtRange',
|
'getOffsetAtRange',
|
||||||
'getParent',
|
'getParent',
|
||||||
|
@@ -105,6 +105,16 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
return this.isBackward == null ? null : !this.isBackward
|
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.
|
* Check whether the selection's keys are not set.
|
||||||
*
|
*
|
||||||
@@ -112,7 +122,7 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
get isUnset() {
|
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 })
|
const state = new State({ document, selection })
|
||||||
|
|
||||||
return state.transform({ normalized: false })
|
return state.transform({ normalized: false })
|
||||||
.normalize(SCHEMA)
|
.normalize(SCHEMA)
|
||||||
.apply({ save: false })
|
.apply({ save: false })
|
||||||
|
@@ -190,6 +190,19 @@ class Text extends new Record(DEFAULTS) {
|
|||||||
return char.marks
|
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`.
|
* Derive the ranges for a list of `characters`.
|
||||||
*
|
*
|
||||||
@@ -240,6 +253,17 @@ class Text extends new Record(DEFAULTS) {
|
|||||||
return ranges
|
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`.
|
* Insert `text` at `index`.
|
||||||
*
|
*
|
||||||
|
@@ -3,6 +3,14 @@ import Schema from '../models/schema'
|
|||||||
import Text from '../models/text'
|
import Text from '../models/text'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options object with normalize set to `false`.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const OPTS = { normalize: false }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only allow block nodes in documents.
|
* Only allow block nodes in documents.
|
||||||
*
|
*
|
||||||
@@ -19,7 +27,7 @@ const DOCUMENT_CHILDREN_RULE = {
|
|||||||
return invalids.size ? invalids : null
|
return invalids.size ? invalids : null
|
||||||
},
|
},
|
||||||
normalize: (transform, document, invalids) => {
|
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
|
return invalids.size ? invalids : null
|
||||||
},
|
},
|
||||||
normalize: (transform, block, invalids) => {
|
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) => {
|
normalize: (transform, node) => {
|
||||||
const text = Text.create()
|
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
|
return invalids.size ? invalids : null
|
||||||
},
|
},
|
||||||
normalize: (transform, inline, invalids) => {
|
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) => {
|
return block.nodes.reduce((tr, child, index) => {
|
||||||
if (child.kind == 'inline' && child.text == '') {
|
if (child.kind == 'inline' && child.text == '') {
|
||||||
return transform
|
return transform
|
||||||
.removeNodeByKey(child.key, { normalize: false })
|
.removeNodeByKey(child.key, OPTS)
|
||||||
.insertNodeByKey(block.key, index, Text.createFromString(''), { normalize: false })
|
.insertNodeByKey(block.key, index, Text.createFromString(''), OPTS)
|
||||||
} else {
|
} else {
|
||||||
return tr
|
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}
|
* @type {Object}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const VOID_TEXT_RULE = {
|
const VOID_TEXT_RULE = {
|
||||||
match: (object) => {
|
match: (object) => {
|
||||||
return (object.kind == 'inline' || object.kind == 'block') && object.isVoid
|
return (
|
||||||
|
(object.kind == 'inline' || object.kind == 'block') &&
|
||||||
|
(object.isVoid)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
validate: (node) => {
|
validate: (node) => {
|
||||||
return node.text !== ' ' || node.nodes.size !== 1
|
return (
|
||||||
|
node.text !== ' ' ||
|
||||||
|
node.nodes.size !== 1
|
||||||
|
)
|
||||||
},
|
},
|
||||||
normalize: (transform, node, result) => {
|
normalize: (transform, node, result) => {
|
||||||
node.nodes.reduce((t, child) => {
|
const text = Text.createFromString(' ')
|
||||||
return t.removeNodeByKey(child.key, { normalize: false })
|
const index = node.nodes.size
|
||||||
}, transform)
|
|
||||||
|
|
||||||
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 }) => {
|
return invalids.reduce((t, { index, next, prev }) => {
|
||||||
if (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
|
shift = shift + 1
|
||||||
}
|
}
|
||||||
if (next) {
|
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
|
shift = shift + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +238,7 @@ const NO_ADJACENT_TEXT_RULE = {
|
|||||||
.reverse()
|
.reverse()
|
||||||
.reduce((t, pair) => {
|
.reduce((t, pair) => {
|
||||||
const [ first, second ] = pair
|
const [ first, second ] = pair
|
||||||
return t.joinNodeByKey(second.key, first.key, { normalize: false })
|
return t.joinNodeByKey(second.key, first.key, OPTS)
|
||||||
}, transform)
|
}, transform)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,7 +292,7 @@ const NO_EMPTY_TEXT_RULE = {
|
|||||||
},
|
},
|
||||||
normalize: (transform, node, invalids) => {
|
normalize: (transform, node, invalids) => {
|
||||||
return invalids.reduce((t, text) => {
|
return invalids.reduce((t, text) => {
|
||||||
return t.removeNodeByKey(text.key, { normalize: false })
|
return t.removeNodeByKey(text.key, OPTS)
|
||||||
}, transform)
|
}, transform)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -231,54 +231,50 @@ function removeNode(state, operation) {
|
|||||||
const { path } = operation
|
const { path } = operation
|
||||||
let { document, selection } = state
|
let { document, selection } = state
|
||||||
const { startKey, endKey } = selection
|
const { startKey, endKey } = selection
|
||||||
|
|
||||||
// Preserve previous document
|
|
||||||
const prevDocument = document
|
|
||||||
|
|
||||||
// Update the document
|
|
||||||
const node = document.assertPath(path)
|
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)
|
let parent = document.getParent(node.key)
|
||||||
const index = parent.nodes.indexOf(node)
|
const index = parent.nodes.indexOf(node)
|
||||||
const isParent = document == parent
|
const isParent = document == parent
|
||||||
parent = parent.removeNode(index)
|
parent = parent.removeNode(index)
|
||||||
document = isParent ? parent : document.updateDescendant(parent)
|
document = isParent ? parent : document.updateDescendant(parent)
|
||||||
|
|
||||||
function getRemoved(key) {
|
// Update the document and selection.
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.merge({ document, selection })
|
state = state.merge({ document, selection })
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
@@ -12,9 +12,9 @@ import warn from '../utils/warn'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export function normalize(transform, schema) {
|
export function normalize(transform, schema) {
|
||||||
|
transform.normalizeDocument(schema)
|
||||||
|
transform.normalizeSelection(schema)
|
||||||
return transform
|
return transform
|
||||||
.normalizeDocument(schema)
|
|
||||||
.normalizeSelection(schema)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +28,8 @@ export function normalize(transform, schema) {
|
|||||||
export function normalizeDocument(transform, schema) {
|
export function normalizeDocument(transform, schema) {
|
||||||
const { state } = transform
|
const { state } = transform
|
||||||
const { document } = state
|
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) {
|
export default function (state) {
|
||||||
const { document, selection } = state
|
const { document, selection } = state
|
||||||
const texts = document.getTexts()
|
const texts = document.getTexts()
|
||||||
let first = texts.first()
|
const first = texts.first()
|
||||||
const range = selection.merge({
|
const range = selection.merge({
|
||||||
anchorKey: first.key,
|
anchorKey: first.key,
|
||||||
anchorOffset: 0,
|
anchorOffset: 0,
|
||||||
@@ -21,14 +21,14 @@ export default function (state) {
|
|||||||
})
|
})
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
// Selection is reset, in theory it should me on the emoji
|
const updated = next.document.getTexts().get(1)
|
||||||
first = next.document.getTexts().first()
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
next.selection.toJS(),
|
next.selection.toJS(),
|
||||||
range.merge({
|
range.merge({
|
||||||
anchorKey: first.key,
|
anchorKey: updated.key,
|
||||||
anchorOffset: 0,
|
anchorOffset: 0,
|
||||||
focusKey: first.key,
|
focusKey: updated.key,
|
||||||
focusOffset: 0
|
focusOffset: 0
|
||||||
}).toJS()
|
}).toJS()
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user