1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-11 09:43:58 +02:00
This commit is contained in:
Ian Storm Taylor
2016-06-23 12:34:47 -07:00
parent 64e837230d
commit c639db25c4
5 changed files with 241 additions and 239 deletions

View File

@@ -24,28 +24,51 @@ clean:
# Build the source.
dist: $(shell find ./lib)
@ $(babel) --out-dir ./dist ./lib
@ $(babel) \
--out-dir \
./dist \
./lib
@ touch ./dist
# Build the auto-markdown example.
example-auto-markdown:
@ $(browserify) --debug --transform babelify --outfile ./examples/auto-markdown/build.js ./examples/auto-markdown/index.js
@ $(browserify) \
--debug \
--transform babelify \
--outfile ./examples/auto-markdown/build.js \
./examples/auto-markdown/index.js
# Build the links example.
example-links:
@ $(browserify) --debug --transform babelify --outfile ./examples/links/build.js ./examples/links/index.js
@ $(browserify) \
--debug \
--transform babelify \
--outfile ./examples/links/build.js \
./examples/links/index.js
# Build the plain-text example.
example-plain-text:
@ $(browserify) --debug --transform babelify --outfile ./examples/plain-text/build.js ./examples/plain-text/index.js
@ $(browserify) \
--debug \
--transform babelify \
--outfile ./examples/plain-text/build.js \
./examples/plain-text/index.js
# Build the rich-text example.
example-rich-text:
@ $(browserify) --debug --transform babelify --outfile ./examples/rich-text/build.js ./examples/rich-text/index.js
@ $(browserify) \
--debug \
--transform babelify \
--outfile ./examples/rich-text/build.js \
./examples/rich-text/index.js
# Build the table example.
example-table:
@ $(browserify) --debug --transform babelify --outfile ./examples/table/build.js ./examples/table/index.js
@ $(browserify) \
--debug \
--transform babelify \
--outfile ./examples/table/build.js \
./examples/table/index.js
# Install the dependencies.
install:
@@ -57,14 +80,20 @@ lint:
# Build the test source.
test/browser/support/build.js: $(shell find ./lib) ./test/browser.js
@ $(browserify) --debug --transform babelify --outfile ./test/support/build.js ./test/browser.js
@ $(browserify) \
--debug \
--transform babelify \
--outfile ./test/support/build.js ./test/browser.js
# Run the tests.
test: test-browser test-server
# Run the browser-side tests.
test-browser: ./test/support/build.js
@ $(mocha-phantomjs) --reporter spec --timeout 5000 ./test/support/browser.html
@ $(mocha-phantomjs) \
--reporter spec \
--timeout 5000 \
./test/support/browser.html
# Run the server-side tests.
test-server:

View File

@@ -83,7 +83,7 @@ class Content extends React.Component {
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
const anchor = OffsetKey.findPoint(anchorNode, anchorOffset)
const focus = OffsetKey.findPoint(focusNode, focusOffset)
const edges = document.filterDeep((node) => {
const edges = document.filterDescendants((node) => {
return node.key == anchor.key || node.key == focus.key
})

View File

@@ -22,8 +22,24 @@ const Node = {
* @param {String or Node} key
*/
assertHasDeep(key) {
if (!this.hasDeep(key)) throw new Error('Could not find that child node.')
assertHasChild(key) {
key = normalizeKey(key)
if (!this.hasChild(key)) {
throw new Error(`Could not find a child node with key "${key}".`)
}
},
/**
* Assert that the node has a descendant by `key`.
*
* @param {String or Node} key
*/
assertHasDescendant(key) {
key = normalizeKey(key)
if (!this.hasDescendant(key)) {
throw new Error(`Could not find a descendant node with key "${key}".`)
}
},
/**
@@ -42,10 +58,10 @@ const Node = {
// Make sure the children exist.
const { startKey, startOffset, endKey, endOffset } = range
node.assertHasDeep(startKey)
node.assertHasDeep(endKey)
node.assertHasDescendant(startKey)
node.assertHasDescendant(endKey)
let startNode = node.getDeep(startKey)
let startNode = node.getDescendant(startKey)
// If the start and end nodes are the same, remove the matching characters.
if (startKey == endKey) {
@@ -81,11 +97,11 @@ const Node = {
let endParent = node.getParent(endKey)
const startGrandestParent = node.nodes.find((child) => {
return child == startParent || child.hasDeep(startParent)
return child == startParent || child.hasDescendant(startParent)
})
const endGrandestParent = node.nodes.find((child) => {
return child == endParent || child.hasDeep(endParent)
return child == endParent || child.hasDescendant(endParent)
})
const nodes = node.nodes
@@ -103,9 +119,9 @@ const Node = {
// Then remove the end parent.
let endGrandparent = node.getParent(endParent)
if (endGrandparent == node) {
node = node.removeDeep(endParent)
node = node.removeDescendant(endParent)
} else {
endGrandparent = endGrandparent.removeDeep(endParent)
endGrandparent = endGrandparent.removeDescendant(endParent)
node = node.updateDeep(endGrandparent)
}
@@ -133,7 +149,7 @@ const Node = {
// When at start of a text node, merge forwards into the next text node.
const { startKey } = range
const startNode = node.getDeep(startKey)
const startNode = node.getDescendant(startKey)
if (range.isAtStartOf(startNode)) {
const previous = node.getPreviousText(startNode)
@@ -170,7 +186,7 @@ const Node = {
// When at end of a text node, merge forwards into the next text node.
const { startKey } = range
const startNode = node.getDeep(startKey)
const startNode = node.getDescendant(startKey)
if (range.isAtEndOf(startNode)) {
const next = node.getNextText(startNode)
@@ -188,33 +204,32 @@ const Node = {
},
/**
* Recursively find nodes nodes by `iterator`.
* Recursively find all ancestor nodes by `iterator`.
*
* @param {Function} iterator
* @return {Node} node
*/
findDeep(iterator) {
const shallow = this.nodes.find(iterator)
if (shallow != null) return shallow
return this.nodes
.map(node => node.kind == 'text' ? null : node.findDeep(iterator))
.filter(node => node)
.first()
findDescendant(iterator) {
return (
this.nodes.find(iterator) ||
this.nodes
.map(node => node.kind == 'text' ? null : node.findDescendant(iterator))
.find(exists => exists)
)
},
/**
* Recursively filter nodes nodes with `iterator`.
* Recursively filter all ancestor nodes with `iterator`.
*
* @param {Function} iterator
* @return {OrderedMap} matches
* @return {List} nodes
*/
filterDeep(iterator) {
filterDescendants(iterator) {
return this.nodes.reduce((matches, child, i, nodes) => {
if (iterator(child, i, nodes)) matches = matches.push(child)
if (child.kind != 'text') matches = matches.concat(child.filterDeep(iterator))
if (child.kind != 'text') matches = matches.concat(child.filterDescendants(iterator))
return matches
}, Block.createList())
},
@@ -223,14 +238,14 @@ const Node = {
* Get the closest block nodes for each text node in a `range`.
*
* @param {Selection} range
* @return {OrderedMap} nodes
* @return {List} nodes
*/
getBlocksAtRange(range) {
range = range.normalize(this)
const texts = this.getTextsAtRange(range)
const blocks = texts.map(text => this.getClosestBlock(text))
return blocks
return this
.getTextsAtRange(range)
.map(text => this.getClosestBlock(text))
},
/**
@@ -242,16 +257,12 @@ const Node = {
getCharactersAtRange(range) {
range = range.normalize(this)
const texts = this.getTextsAtRange(range)
let list = new List()
texts.forEach((text) => {
let { characters } = text
characters = characters.filter((char, i) => isInRange(i, text, range))
list = list.concat(characters)
})
return list
return this
.getTextsAtRange(range)
.reduce((characters, text) => {
const chars = text.characters.filter((char, i) => isInRange(i, text, range))
return characters.concat(chars)
}, Character.createList())
},
/**
@@ -259,14 +270,11 @@ const Node = {
*
* @param {String or Node} key
* @param {Function} iterator
* @return {Node or Null} parent
* @return {Node or Null} node
*/
getClosest(key, iterator) {
key = normalizeKey(key)
this.assertHasDeep(key)
let node = this.getDeep(key)
let node = this.getDescendant(key)
while (node = this.getParent(node)) {
if (node == this) return null
@@ -280,66 +288,46 @@ const Node = {
* Get the closest block parent of a `node`.
*
* @param {String or Node} key
* @return {Node or Null} parent
* @return {Node or Null} node
*/
getClosestBlock(key) {
key = normalizeKey(key)
this.assertHasDeep(key)
const match = this.getClosest(key, parent => parent.kind == 'block')
return match
return this.getClosest(key, parent => parent.kind == 'block')
},
/**
* Get the closest inline parent of a `node`.
*
* @param {String or Node} key
* @return {Node or Null} parent
* @return {Node or Null} node
*/
getClosestInline(key) {
key = normalizeKey(key)
this.assertHasDeep(key)
const match = this.getClosest(key, parent => parent.kind == 'inline')
return match
},
/**
* Get the furthest inline parent of a node by `key`.
*
* @param {String or Node} key
* @return {Node or Null} parent
*/
getFurthestInline(key) {
key = normalizeKey(key)
this.assertHasDeep(key)
let child = this.getDeep(key)
let furthest = null
let next
while (next = this.getClosestInline(child)) {
furthest = next
child = next
}
return furthest
return this.getClosest(key, parent => parent.kind == 'inline')
},
/**
* Get a child node by `key`.
*
* @param {String} key
* @return {Node or Null}
* @return {Node or Null} node
*/
getDeep(key) {
getChild(key) {
key = normalizeKey(key)
const match = this.findDeep(node => node.key == key)
return match || null
return this.nodes.find(node => node.key == key)
},
/**
* Get a descendant node by `key`.
*
* @param {String} key
* @return {Node or Null} node
*/
getDescendant(key) {
key = normalizeKey(key)
return this.findDescendant(node => node.key == key)
},
/**
@@ -352,7 +340,7 @@ const Node = {
getDepth(key, startAt = 1) {
key = normalizeKey(key)
this.assertHasDeep(key)
this.assertHasDescendant(key)
const shallow = this.nodes.find(node => node.key == key)
if (shallow) return startAt
@@ -360,7 +348,7 @@ const Node = {
const child = this.nodes.find(node => {
return node.kind == 'text'
? null
: node.hasDeep(key)
: node.hasDescendant(key)
})
return child
@@ -369,41 +357,58 @@ const Node = {
},
/**
* Get the first text child node.
* Get the furthest block parent of a node by `key`.
*
* @return {Text or Null} text
* @param {String or Node} key
* @return {Node or Null} node
*/
getFirstText() {
return this.getTextNodes().first() || null
getFurthestBlock(key) {
let node = this.getDescendant(key)
let furthest = null
while (node = this.getClosestBlock(node)) {
furthest = node
}
return furthest
},
/**
* Get the furthest inline parent of a node by `key`.
*
* @param {String or Node} key
* @return {Node or Null} node
*/
getFurthestInline(key) {
let node = this.getDescendant(key)
let furthest = null
while (node = this.getClosestInline(node)) {
furthest = node
}
return furthest
},
/**
* Get the closest inline nodes for each text node in a `range`.
*
* @param {Selection} range
* @return {OrderedMap} nodes
* @return {List} nodes
*/
getInlinesAtRange(range) {
range = range.normalize(this)
const node = this
const texts = node.getTextsAtRange(range)
const inlines = texts
.map(text => node.getClosest(text, p => p.kind == 'inline'))
.filter(inline => inline)
return inlines
},
// If the range isn't set, return an empty list.
if (range.isUnset) return Inline.createList()
/**
* Get the last text child node.
*
* @return {Text or Null} text
*/
getLastText() {
return this.getTextNodes().last() || null
return this
.getTextsAtRange(range)
.map(text => this.getClosestInline(text))
.filter(exists => exists)
},
/**
@@ -415,65 +420,54 @@ const Node = {
getMarksAtRange(range) {
range = range.normalize(this)
const { startKey, startOffset, endKey } = range
const { startKey, startOffset } = range
const marks = Mark.createSet()
// If the selection isn't set, return nothing.
if (startKey == null || endKey == null) return new Set()
// If the range isn't set, return an empty set.
if (range.isUnset) return marks
// If the range is collapsed, and at the start of the node, check the
// previous text node.
// If the range is collapsed at the start of the node, check the previous.
if (range.isCollapsed && startOffset == 0) {
const previous = this.getPreviousText(startKey)
if (!previous) return new Set()
if (!previous) return marks
const char = text.characters.get(previous.length - 1)
return char.marks
}
// If the range is collapsed, check the character before the start.
if (range.isCollapsed) {
const text = this.getDeep(startKey)
const text = this.getDescendant(startKey)
const char = text.characters.get(range.startOffset - 1)
return char.marks
}
// Otherwise, get a set of the marks for each character in the range.
const characters = this.getCharactersAtRange(range)
let set = new Set()
characters.forEach((char) => {
set = set.union(char.marks)
})
return set
this
.getCharactersAtRange(range)
.reduce((marks, char) => {
return marks.union(char.marks)
}, marks)
},
/**
* Get the child node after the one by `key`.
* Get the node after a descendant by `key`.
*
* @param {String or Node} key
* @return {Node or Null}
* @return {Node or Null} node
*/
getNextSibling(key) {
key = normalizeKey(key)
this.assertHasDeep(key)
const shallow = this.nodes.find(node => node.key == key)
if (shallow) {
return this.nodes
.skipUntil(node => node.key == key)
.rest()
.first()
}
return this.nodes
.map(node => node.kind == 'text' ? null : node.getNextSibling(key))
.filter(node => node)
.first()
const node = this.getDescendant(key)
if (!node) return null
return this
.getParent(node)
.nodes
.skipUntil(child => child == node)
.get(1)
},
/**
* Get the text node after a text node by `key`.
* Get the text node after a descendant text node by `key`.
*
* @param {String or Node} key
* @return {Node or Null} node
@@ -481,16 +475,13 @@ const Node = {
getNextText(key) {
key = normalizeKey(key)
this.assertHasDeep(key)
return this.getTextNodes()
.skipUntil(text => text.key == key)
.take(2)
.last()
.get(1)
},
/**
* Get the offset for a child text node by `key`.
* Get the offset for a descendant text node by `key`.
*
* @param {String or Node} key
* @return {Number} offset
@@ -498,16 +489,16 @@ const Node = {
getOffset(key) {
key = normalizeKey(key)
this.assertHasDeep(key)
this.assertHasDescendant(key)
const match = this.getDeep(key)
const match = this.getDescendant(key)
// Find the shallow matching child.
const child = this.nodes.find((node) => {
if (node == match) return true
return node.kind == 'text'
? false
: node.hasDeep(match)
: node.hasDescendant(match)
})
// Get all of the nodes that come before the matching child.
@@ -529,17 +520,15 @@ const Node = {
* Get the parent of a child node by `key`.
*
* @param {String or Node} key
* @return {Node or Null}
* @return {Node or Null} node
*/
getParent(key) {
key = normalizeKey(key)
// this.assertHasDeep(key)
const shallow = this.nodes.find(node => node.key == key)
if (shallow) return this
if (this.hasChild(key)) return this
let node = null
this.nodes.forEach((child) => {
if (child.kind == 'text') return
const match = child.getParent(key)
@@ -550,31 +539,24 @@ const Node = {
},
/**
* Get the child node before the one by `key`.
* Get the node before a descendant node by `key`.
*
* @param {String or Node} key
* @return {Node or Null}
* @return {Node or Null} node
*/
getPreviousSibling(key) {
key = normalizeKey(key)
this.assertHasDeep(key)
const shallow = this.nodes.find(node => node.key == key)
if (shallow) {
return this.nodes
.takeUntil(node => node.key == key)
const node = this.getDescendant(key)
if (!node) return null
return this
.getParent(node)
.nodes
.takeUntil(child => child == node)
.last()
}
return this.nodes
.map(node => node.kind == 'text' ? null : node.getPreviousSibling(key))
.filter(node => node)
.first()
},
/**
* Get the text node before a text node by `key`.
* Get the text node before a descendant text node by `key`.
*
* @param {String or Node} key
* @return {Node or Null} node
@@ -582,35 +564,32 @@ const Node = {
getPreviousText(key) {
key = normalizeKey(key)
this.assertHasDeep(key)
return this.getTextNodes()
.takeUntil(text => text.key == key)
.last()
},
/**
* Get the child text node at an `offset`.
* Get the descendent text node at an `offset`.
*
* @param {String} offset
* @return {Node or Null}
* @return {Node or Null} node
*/
getTextAtOffset(offset) {
let length = 0
let texts = this.getTextNodes()
let match = texts.find((node) => {
length += node.length
return this
.getTextNodes()
.find((text) => {
length += text.length
return length >= offset
})
return match
},
/**
* Recursively get all of the child text nodes in order of appearance.
*
* @return {OrderedMap} nodes
* @return {List} nodes
*/
getTextNodes() {
@@ -625,43 +604,49 @@ const Node = {
* Get all of the text nodes in a `range`.
*
* @param {Selection} range
* @return {OrderedMap} nodes
* @return {List} nodes
*/
getTextsAtRange(range) {
range = range.normalize(this)
// If the selection is unset, return an empty list.
if (range.isUnset) return Block.createList()
const { startKey, endKey } = range
// If the selection is unset, return an empty map.
if (range.isUnset) return new OrderedMap()
// Assert that the nodes exist before searching.
this.assertHasDeep(startKey)
this.assertHasDeep(endKey)
// Return the text nodes after the start offset and before the end offset.
const texts = this.getTextNodes()
const endNode = this.getDeep(endKey)
const afterStart = texts.skipUntil(node => node.key == startKey)
const upToEnd = afterStart.takeUntil(node => node.key == endKey)
const matches = upToEnd.push(endNode)
return matches
const startText = this.getDescendant(startKey)
const endText = this.getDescendant(endKey)
const start = texts.indexOf(startText)
const end = texts.indexOf(endText)
return texts.slice(start, end + 1)
},
/**
* Check if a child node exists by `key`.
*
* @param {String or Node} key
* @return {Boolean} exists
*/
hasChild(key) {
key = normalizeKey(key)
return !! this.nodes.find(node => node.key == key)
},
/**
* Recursively check if a child node exists by `key`.
*
* @param {String or Node} key
* @return {Boolean} true
* @return {Boolean} exists
*/
hasDeep(key) {
hasDescendant(key) {
key = normalizeKey(key)
return !! this.nodes.find((node) => {
return node.kind == 'text'
? node.key == key
: node.key == key || node.hasDeep(key)
: node.key == key || node.hasDescendant(key)
})
},
@@ -684,7 +669,7 @@ const Node = {
}
let { startKey, startOffset } = range
let startNode = node.getDeep(startKey)
let startNode = node.getDescendant(startKey)
let { characters } = startNode
// Create a list of the new characters, with the marks from the previous
@@ -764,7 +749,7 @@ const Node = {
let node = this
// See if there are any adjacent text nodes.
let firstAdjacent = node.findDeep((child) => {
let firstAdjacent = node.findDescendant((child) => {
if (child.kind != 'text') return
const parent = node.getParent(child)
const next = parent.getNextSibling(child)
@@ -782,7 +767,7 @@ const Node = {
parent = parent.updateDeep(firstAdjacent)
// Then remove the second node.
parent = parent.removeDeep(second)
parent = parent.removeDescendant(second)
// If the parent isn't this node, it needs to be updated.
if (parent != node) {
@@ -795,18 +780,6 @@ const Node = {
return node.normalize()
},
/**
* Push a new `node` onto the map of nodes.
*
* @param {Node} node
* @return {Node} node
*/
pushNode(node) {
const nodes = this.nodes.push(node)
return this.merge({ nodes })
},
/**
* Remove a `node` from the children node map.
*
@@ -814,9 +787,9 @@ const Node = {
* @return {Node} node
*/
removeDeep(key) {
removeDescendant(key) {
key = normalizeKey(key)
this.assertHasDeep(key)
this.assertHasDescendant(key)
const nodes = this.nodes.filterNot(node => node.key == key)
return this.merge({ nodes })
},
@@ -913,14 +886,14 @@ const Node = {
// Find the highest inline elements that were split.
const { startKey } = range
const firstText = node.getDeep(startKey)
const firstText = node.getDescendant(startKey)
const firstChild = node.getFurthestInline(firstText) || firstText
const secondText = node.getNextText(startKey)
const secondChild = node.getFurthestInline(secondText) || secondText
// Remove the second inline child from the first block.
let firstBlock = node.getBlocksAtRange(range).first()
firstBlock = firstBlock.removeDeep(secondChild)
firstBlock = firstBlock.removeDescendant(secondChild)
// Create a new block with the second inline child in it.
const secondBlock = Block.create({
@@ -961,7 +934,7 @@ const Node = {
// First split the text nodes.
node = node.splitTextAtRange(range)
let firstChild = node.getDeep(range.startKey)
let firstChild = node.getDescendant(range.startKey)
let secondChild = node.getNextText(firstChild)
let parent
@@ -1010,7 +983,7 @@ const Node = {
// Split the text node's characters.
const { startKey, startOffset } = range
const text = node.getDeep(startKey)
const text = node.getDescendant(startKey)
const { characters } = text
const firstChars = characters.take(startOffset)
const secondChars = characters.skip(startOffset)
@@ -1082,7 +1055,7 @@ const Node = {
*/
updateDeep(node) {
// this.assertHasDeep(key)
// this.assertHasDescendant(key)
const shallow = this.nodes.find(child => child.key == node.key)
if (shallow) {
@@ -1235,7 +1208,7 @@ const Node = {
// Determine the new end of the range, and split there.
const { startKey, startOffset, endKey, endOffset } = range
const firstNode = node.getDeep(startKey)
const firstNode = node.getDescendant(startKey)
const nextNode = node.getNextText(startKey)
const end = startKey != endKey
? range.moveToEnd()
@@ -1249,7 +1222,7 @@ const Node = {
node = node.splitInlineAtRange(end)
// Calculate the new range to wrap around.
const endNode = node.getDeep(end.anchorKey)
const endNode = node.getDescendant(end.anchorKey)
range = Selection.create({
anchorKey: nextNode.key,
anchorOffset: 0,

View File

@@ -114,7 +114,7 @@ class Selection extends Record(DEFAULTS) {
isAtStartOf(node) {
const { startKey, startOffset } = this
const first = node.kind == 'text' ? node : node.getFirstText()
const first = node.kind == 'text' ? node : node.getTextNodes().first()
return startKey == first.key && startOffset == 0
}
@@ -127,7 +127,7 @@ class Selection extends Record(DEFAULTS) {
isAtEndOf(node) {
const { endKey, endOffset } = this
const last = node.kind == 'text' ? node : node.getLastText()
const last = node.kind == 'text' ? node : node.getTextNodes().last()
return endKey == last.key && endOffset == last.length
}
@@ -147,10 +147,10 @@ class Selection extends Record(DEFAULTS) {
if (anchorKey == null || focusKey == null) return selection
// Asset that the anchor and focus nodes exist in the node tree.
node.assertHasDeep(anchorKey)
node.assertHasDeep(focusKey)
let anchorNode = node.getDeep(anchorKey)
let focusNode = node.getDeep(focusKey)
node.assertHasDescendant(anchorKey)
node.assertHasDescendant(focusKey)
let anchorNode = node.getDescendant(anchorKey)
let focusNode = node.getDescendant(focusKey)
// If the anchor node isn't a text node, match it to one.
if (anchorNode.kind != 'text') {

View File

@@ -280,7 +280,7 @@ class State extends Record(DEFAULTS) {
// Determine what the selection should be after deleting.
const { startKey } = selection
const startNode = document.getDeep(startKey)
const startNode = document.getDescendant(startKey)
if (selection.isExpanded) {
after = selection.moveToStart()
@@ -410,7 +410,7 @@ class State extends Record(DEFAULTS) {
// Determine what the selection should be after splitting.
const { startKey } = selection
const startNode = document.getDeep(startKey)
const startNode = document.getDescendant(startKey)
const parent = document.getParent(startNode)
const next = document.getNext(parent)
const text = next.nodes.first()