mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-30 18:39:51 +02:00
Node methods optimizations (#364)
* Node.getParent exits earlier * Add Node.getAncestors method * Remove numerous getParent in Node.getClosest * Remove use of assertDescendant in getPath Still throws when not finding the descendant though * Remove assertDescendant from Node.updateDescendant * Remove assertDescendant from Node.removeDescendant * Fix Node.findDescendant, which always returned first level descendants * Add Node.findDescendantDeep * Memoize Node.getAncestors * Implement and use Node.get{First|Last}Text * Add jsdom devDepencency Required as peer dependency by mocha-jsdom
This commit is contained in:
committed by
Ian Storm Taylor
parent
dca60c42ce
commit
8851855b5b
@@ -47,6 +47,7 @@
|
|||||||
"http-server": "^0.9.0",
|
"http-server": "^0.9.0",
|
||||||
"is-image": "^1.0.1",
|
"is-image": "^1.0.1",
|
||||||
"is-url": "^1.2.2",
|
"is-url": "^1.2.2",
|
||||||
|
"jsdom": "9.8.0",
|
||||||
"jsdom": "9.6.0",
|
"jsdom": "9.6.0",
|
||||||
"jsdom-global": "2.1.0",
|
"jsdom-global": "2.1.0",
|
||||||
"microtime": "2.1.1",
|
"microtime": "2.1.1",
|
||||||
|
@@ -130,7 +130,7 @@ class Void extends React.Component {
|
|||||||
|
|
||||||
renderLeaf = () => {
|
renderLeaf = () => {
|
||||||
const { node, schema, state } = this.props
|
const { node, schema, state } = this.props
|
||||||
const child = node.getTexts().first()
|
const child = node.getFirstText()
|
||||||
const ranges = child.getRanges()
|
const ranges = child.getRanges()
|
||||||
const text = ''
|
const text = ''
|
||||||
const marks = Mark.createSet()
|
const marks = Mark.createSet()
|
||||||
|
@@ -105,23 +105,53 @@ const Node = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively find all ancestor nodes by `iterator`.
|
* Recursively find all descendant nodes by `iterator`. Breadth first.
|
||||||
*
|
*
|
||||||
* @param {Function} iterator
|
* @param {Function} iterator
|
||||||
* @return {Node} node
|
* @return {Node or Null} node
|
||||||
*/
|
*/
|
||||||
|
|
||||||
findDescendant(iterator) {
|
findDescendant(iterator) {
|
||||||
return (
|
const found = this.nodes.find(iterator)
|
||||||
this.nodes.find(iterator) ||
|
if (found) return found
|
||||||
this.nodes
|
|
||||||
.map(node => node.kind == 'text' ? null : node.findDescendant(iterator))
|
let descendantFound = null
|
||||||
.find(exists => exists)
|
this.nodes.find(node => {
|
||||||
)
|
if (node.kind != 'text') {
|
||||||
|
descendantFound = node.findDescendant(iterator)
|
||||||
|
return descendantFound
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return descendantFound
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively filter all ancestor nodes with `iterator`.
|
* Recursively find all descendant nodes by `iterator`. Depth first.
|
||||||
|
*
|
||||||
|
* @param {Function} iterator
|
||||||
|
* @return {Node or Null} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
findDescendantDeep(iterator) {
|
||||||
|
let descendantFound = null
|
||||||
|
|
||||||
|
const found = this.nodes.find(node => {
|
||||||
|
if (node.kind != 'text') {
|
||||||
|
descendantFound = node.findDescendantDeep(iterator)
|
||||||
|
return descendantFound || iterator(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
return iterator(node) ? node : null
|
||||||
|
})
|
||||||
|
|
||||||
|
return descendantFound || found
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively filter all descendant nodes with `iterator`.
|
||||||
*
|
*
|
||||||
* @param {Function} iterator
|
* @param {Function} iterator
|
||||||
* @return {List} nodes
|
* @return {List} nodes
|
||||||
@@ -136,7 +166,7 @@ const Node = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively filter all ancestor nodes with `iterator`, depth-first.
|
* Recursively filter all descendant nodes with `iterator`, depth-first.
|
||||||
*
|
*
|
||||||
* @param {Function} iterator
|
* @param {Function} iterator
|
||||||
* @return {List} nodes
|
* @return {List} nodes
|
||||||
@@ -286,14 +316,13 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getClosest(key, iterator) {
|
getClosest(key, iterator) {
|
||||||
let node = this.assertDescendant(key)
|
let ancestors = this.getAncestors(key)
|
||||||
|
if (!ancestors) {
|
||||||
while (node = this.getParent(node)) {
|
throw new Error(`Could not find a descendant node with key "${key}".`)
|
||||||
if (node == this) return null
|
|
||||||
if (iterator(node)) return node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
// Exclude this node itself
|
||||||
|
return ancestors.rest().findLast(iterator)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -410,16 +439,7 @@ const Node = {
|
|||||||
getDescendant(key) {
|
getDescendant(key) {
|
||||||
key = Normalize.key(key)
|
key = Normalize.key(key)
|
||||||
|
|
||||||
let child = this.getChild(key)
|
return this.findDescendantDeep(node => node.key == key)
|
||||||
if (child) return child
|
|
||||||
|
|
||||||
this.nodes.find((node) => {
|
|
||||||
if (node.kind == 'text') return false
|
|
||||||
child = node.getDescendant(key)
|
|
||||||
return child
|
|
||||||
})
|
|
||||||
|
|
||||||
return child
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -654,10 +674,10 @@ const Node = {
|
|||||||
let last
|
let last
|
||||||
|
|
||||||
if (child.kind == 'block') {
|
if (child.kind == 'block') {
|
||||||
last = child.getTexts().last()
|
last = child.getLastText()
|
||||||
} else {
|
} else {
|
||||||
const block = this.getClosestBlock(key)
|
const block = this.getClosestBlock(key)
|
||||||
last = block.getTexts().last()
|
last = block.getLastText()
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = this.getNextText(last)
|
const next = this.getNextText(last)
|
||||||
@@ -748,10 +768,13 @@ const Node = {
|
|||||||
|
|
||||||
let node = null
|
let node = null
|
||||||
|
|
||||||
this.nodes.forEach((child) => {
|
this.nodes.find((child) => {
|
||||||
if (child.kind == 'text') return
|
if (child.kind == 'text') {
|
||||||
const match = child.getParent(key)
|
return false
|
||||||
if (match) node = match
|
} else {
|
||||||
|
node = child.getParent(key)
|
||||||
|
return node
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return node
|
return node
|
||||||
@@ -760,7 +783,7 @@ const Node = {
|
|||||||
/**
|
/**
|
||||||
* Get the path of a descendant node by `key`.
|
* Get the path of a descendant node by `key`.
|
||||||
*
|
*
|
||||||
* @param {String || Node} node
|
* @param {String || Node} key
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -769,17 +792,50 @@ const Node = {
|
|||||||
|
|
||||||
if (key == this.key) return []
|
if (key == this.key) return []
|
||||||
|
|
||||||
let child = this.assertDescendant(key)
|
|
||||||
let path = []
|
let path = []
|
||||||
|
let childKey = key
|
||||||
let parent
|
let parent
|
||||||
|
|
||||||
while (parent = this.getParent(child)) {
|
// Efficient with getParent memoization
|
||||||
const index = parent.nodes.indexOf(child)
|
while (parent = this.getParent(childKey)) {
|
||||||
|
const index = parent.nodes.findIndex(n => n.key === childKey)
|
||||||
path.unshift(index)
|
path.unshift(index)
|
||||||
child = parent
|
childKey = parent.key
|
||||||
}
|
}
|
||||||
|
|
||||||
return path
|
if (childKey === key) {
|
||||||
|
// Did not loop once, meaning we could not find the child
|
||||||
|
throw new Error(`Could not find a descendant node with key "${key}".`)
|
||||||
|
} else {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path of ancestors of a descendant node by `key`.
|
||||||
|
*
|
||||||
|
* @param {String || Node} node
|
||||||
|
* @return {List<Node> or Null}
|
||||||
|
*/
|
||||||
|
|
||||||
|
getAncestors(key) {
|
||||||
|
key = Normalize.key(key)
|
||||||
|
|
||||||
|
if (key == this.key) return List()
|
||||||
|
if (this.hasChild(key)) return List([this])
|
||||||
|
|
||||||
|
let ancestors
|
||||||
|
this.nodes.find((node) => {
|
||||||
|
if (node.kind == 'text') return false
|
||||||
|
ancestors = node.getAncestors(key)
|
||||||
|
return ancestors
|
||||||
|
})
|
||||||
|
|
||||||
|
if (ancestors) {
|
||||||
|
return ancestors.unshift(this)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -824,10 +880,10 @@ const Node = {
|
|||||||
let first
|
let first
|
||||||
|
|
||||||
if (child.kind == 'block') {
|
if (child.kind == 'block') {
|
||||||
first = child.getTexts().first()
|
first = child.getFirstText()
|
||||||
} else {
|
} else {
|
||||||
const block = this.getClosestBlock(key)
|
const block = this.getClosestBlock(key)
|
||||||
first = block.getTexts().first()
|
first = block.getFirstText()
|
||||||
}
|
}
|
||||||
|
|
||||||
const previous = this.getPreviousText(first)
|
const previous = this.getPreviousText(first)
|
||||||
@@ -881,6 +937,34 @@ const Node = {
|
|||||||
}, Block.createList())
|
}, Block.createList())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first child text node.
|
||||||
|
*
|
||||||
|
* @return {Node || Null} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
getFirstText() {
|
||||||
|
return this.findDescendantDeep(node => node.kind == 'text')
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last child text node.
|
||||||
|
*
|
||||||
|
* @return {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
getLastText() {
|
||||||
|
let descendantFound = null
|
||||||
|
|
||||||
|
const found = this.nodes.findLast((node) => {
|
||||||
|
if (node.kind == 'text') return true
|
||||||
|
descendantFound = node.getLastText()
|
||||||
|
return descendantFound
|
||||||
|
})
|
||||||
|
|
||||||
|
return descendantFound || found
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all of the text nodes in a `range`.
|
* Get all of the text nodes in a `range`.
|
||||||
*
|
*
|
||||||
@@ -1163,10 +1247,13 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
removeDescendant(key) {
|
removeDescendant(key) {
|
||||||
|
key = Normalize.key(key)
|
||||||
|
|
||||||
let node = this
|
let node = this
|
||||||
const desc = node.assertDescendant(key)
|
let parent = node.getParent(key)
|
||||||
let parent = node.getParent(desc)
|
if (!parent) throw new Error(`Could not find a descendant node with key "${key}".`)
|
||||||
const index = parent.nodes.indexOf(desc)
|
|
||||||
|
const index = parent.nodes.findIndex(n => n.key === key)
|
||||||
const isParent = node == parent
|
const isParent = node == parent
|
||||||
const nodes = parent.nodes.splice(index, 1)
|
const nodes = parent.nodes.splice(index, 1)
|
||||||
|
|
||||||
@@ -1277,8 +1364,22 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
updateDescendant(node) {
|
updateDescendant(node) {
|
||||||
this.assertDescendant(node)
|
let found = false
|
||||||
return this.mapDescendants(d => d.key == node.key ? node : d)
|
|
||||||
|
const result = this.mapDescendants(d => {
|
||||||
|
if (d.key == node.key) {
|
||||||
|
found = true
|
||||||
|
return node
|
||||||
|
} else {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
throw new Error(`Could not update descendant node with key "${node.key}".`)
|
||||||
|
} else {
|
||||||
|
return result
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1304,6 +1405,8 @@ memoize(Node, [
|
|||||||
'filterDescendants',
|
'filterDescendants',
|
||||||
'filterDescendantsDeep',
|
'filterDescendantsDeep',
|
||||||
'findDescendant',
|
'findDescendant',
|
||||||
|
'findDescendantDeep',
|
||||||
|
'getAncestors',
|
||||||
'getBlocks',
|
'getBlocks',
|
||||||
'getBlocksAtRange',
|
'getBlocksAtRange',
|
||||||
'getCharactersAtRange',
|
'getCharactersAtRange',
|
||||||
@@ -1322,6 +1425,7 @@ memoize(Node, [
|
|||||||
'getDepth',
|
'getDepth',
|
||||||
'getDescendant',
|
'getDescendant',
|
||||||
'getDescendantDecorators',
|
'getDescendantDecorators',
|
||||||
|
'getFirstText',
|
||||||
'getFragmentAtRange',
|
'getFragmentAtRange',
|
||||||
'getFurthest',
|
'getFurthest',
|
||||||
'getFurthestBlock',
|
'getFurthestBlock',
|
||||||
@@ -1329,6 +1433,7 @@ memoize(Node, [
|
|||||||
'getHighestChild',
|
'getHighestChild',
|
||||||
'getHighestOnlyChildParent',
|
'getHighestOnlyChildParent',
|
||||||
'getInlinesAtRange',
|
'getInlinesAtRange',
|
||||||
|
'getLastText',
|
||||||
'getMarksAtRange',
|
'getMarksAtRange',
|
||||||
'getNextBlock',
|
'getNextBlock',
|
||||||
'getNextSibling',
|
'getNextSibling',
|
||||||
|
@@ -148,7 +148,7 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
|
|
||||||
hasAnchorAtStartOf(node) {
|
hasAnchorAtStartOf(node) {
|
||||||
if (this.anchorOffset != 0) return false
|
if (this.anchorOffset != 0) return false
|
||||||
const first = node.kind == 'text' ? node : node.getTexts().first()
|
const first = node.kind == 'text' ? node : node.getFirstText()
|
||||||
return this.anchorKey == first.key
|
return this.anchorKey == first.key
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
hasAnchorAtEndOf(node) {
|
hasAnchorAtEndOf(node) {
|
||||||
const last = node.kind == 'text' ? node : node.getTexts().last()
|
const last = node.kind == 'text' ? node : node.getLastText()
|
||||||
return this.anchorKey == last.key && this.anchorOffset == last.length
|
return this.anchorKey == last.key && this.anchorOffset == last.length
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
hasFocusAtEndOf(node) {
|
hasFocusAtEndOf(node) {
|
||||||
const last = node.kind == 'text' ? node : node.getTexts().last()
|
const last = node.kind == 'text' ? node : node.getLastText()
|
||||||
return this.focusKey == last.key && this.focusOffset == last.length
|
return this.focusKey == last.key && this.focusOffset == last.length
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
|
|
||||||
hasFocusAtStartOf(node) {
|
hasFocusAtStartOf(node) {
|
||||||
if (this.focusOffset != 0) return false
|
if (this.focusOffset != 0) return false
|
||||||
const first = node.kind == 'text' ? node : node.getTexts().first()
|
const first = node.kind == 'text' ? node : node.getFirstText()
|
||||||
return this.focusKey == first.key
|
return this.focusKey == first.key
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
const { isExpanded, startKey, startOffset } = this
|
const { isExpanded, startKey, startOffset } = this
|
||||||
if (isExpanded) return false
|
if (isExpanded) return false
|
||||||
if (startOffset != 0) return false
|
if (startOffset != 0) return false
|
||||||
const first = node.kind == 'text' ? node : node.getTexts().first()
|
const first = node.kind == 'text' ? node : node.getFirstText()
|
||||||
return startKey == first.key
|
return startKey == first.key
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ class Selection extends new Record(DEFAULTS) {
|
|||||||
isAtEndOf(node) {
|
isAtEndOf(node) {
|
||||||
const { endKey, endOffset, isExpanded } = this
|
const { endKey, endOffset, isExpanded } = this
|
||||||
if (isExpanded) return false
|
if (isExpanded) return false
|
||||||
const last = node.kind == 'text' ? node : node.getTexts().last()
|
const last = node.kind == 'text' ? node : node.getLastText()
|
||||||
return endKey == last.key && endOffset == last.length
|
return endKey == last.key && endOffset == last.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -47,7 +47,7 @@ class State extends new Record(DEFAULTS) {
|
|||||||
let selection = Selection.create(properties.selection)
|
let selection = Selection.create(properties.selection)
|
||||||
|
|
||||||
if (selection.isUnset) {
|
if (selection.isUnset) {
|
||||||
const text = document.getTexts().first()
|
const text = document.getFirstText()
|
||||||
selection = selection.collapseToStartOf(text)
|
selection = selection.collapseToStartOf(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -70,7 +70,7 @@ export function _delete(transform) {
|
|||||||
after = selection.collapseToEndOf(previous)
|
after = selection.collapseToEndOf(previous)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const last = previous.getTexts().last()
|
const last = previous.getLastText()
|
||||||
after = selection.collapseToEndOf(last)
|
after = selection.collapseToEndOf(last)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ export function deleteBackward(transform, n = 1) {
|
|||||||
after = selection.collapseToEndOf(previous)
|
after = selection.collapseToEndOf(previous)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const last = previous.getTexts().last()
|
const last = previous.getLastText()
|
||||||
after = selection.collapseToEndOf(last)
|
after = selection.collapseToEndOf(last)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -206,7 +206,7 @@ export function deleteForward(transform, n = 1) {
|
|||||||
after = selection.collapseToEndOf(previous)
|
after = selection.collapseToEndOf(previous)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const last = previous.getTexts().last()
|
const last = previous.getLastText()
|
||||||
after = selection.collapseToEndOf(last)
|
after = selection.collapseToEndOf(last)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@ export function insertFragment(transform, fragment) {
|
|||||||
|
|
||||||
if (!fragment.length) return transform
|
if (!fragment.length) return transform
|
||||||
|
|
||||||
const lastText = fragment.getTexts().last()
|
const lastText = fragment.getLastText()
|
||||||
const lastInline = fragment.getClosestInline(lastText)
|
const lastInline = fragment.getClosestInline(lastText)
|
||||||
const beforeTexts = document.getTexts()
|
const beforeTexts = document.getTexts()
|
||||||
const appending = selection.hasEdgeAtEndOf(document.getDescendant(selection.endKey))
|
const appending = selection.hasEdgeAtEndOf(document.getDescendant(selection.endKey))
|
||||||
@@ -575,7 +575,7 @@ export function wrapInline(transform, properties) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
else if (selection.startOffset == 0) {
|
else if (selection.startOffset == 0) {
|
||||||
const text = previous ? document.getNextText(previous) : document.getTexts().first()
|
const text = previous ? document.getNextText(previous) : document.getFirstText()
|
||||||
after = selection.moveToRangeOf(text)
|
after = selection.moveToRangeOf(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user