mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-22 06:53:25 +02:00
Merge branch 'master' into schema-normalize
This commit is contained in:
@@ -51,9 +51,9 @@ An array of rules to initialize the `Html` serializer with, defining your schema
|
|||||||
Deserialize an HTML `string` into a [`State`](../models/state.md). How the string is deserialized will be determined by the rules that the `Html` serializer was constructed with.
|
Deserialize an HTML `string` into a [`State`](../models/state.md). How the string is deserialized will be determined by the rules that the `Html` serializer was constructed with.
|
||||||
|
|
||||||
### `Html.serialize`
|
### `Html.serialize`
|
||||||
`Html.serialize(state: State) => String`
|
`Html.serialize(state: State, [options: Object]) => String || Array`
|
||||||
|
|
||||||
Serialize a `state` into an HTML string. How the string is serialized will be determined by the rules that the `Html` serializer was constructed with.
|
Serialize a `state` into an HTML string. How the string is serialized will be determined by the rules that the `Html` serializer was constructed with. If you pass `render: false` as an option, the return value will instead be an iterable list of the top-level React elements, to be rendered as children in your own React component.
|
||||||
|
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "slate",
|
"name": "slate",
|
||||||
"description": "A completely customizable framework for building rich text editors.",
|
"description": "A completely customizable framework for building rich text editors.",
|
||||||
"version": "0.14.15",
|
"version": "0.14.16",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "git://github.com/ianstormtaylor/slate.git",
|
"repository": "git://github.com/ianstormtaylor/slate.git",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
|
@@ -128,7 +128,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()
|
||||||
|
@@ -101,23 +101,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
|
||||||
@@ -132,7 +162,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
|
||||||
@@ -282,14 +312,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)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -406,16 +435,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
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -649,10 +669,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)
|
||||||
@@ -743,10 +763,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
|
||||||
@@ -755,7 +778,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}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -764,17 +787,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
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
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -819,10 +875,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)
|
||||||
@@ -876,6 +932,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`.
|
||||||
*
|
*
|
||||||
@@ -1070,10 +1154,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)
|
||||||
|
|
||||||
@@ -1183,8 +1270,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
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1210,6 +1311,8 @@ memoize(Node, [
|
|||||||
'filterDescendants',
|
'filterDescendants',
|
||||||
'filterDescendantsDeep',
|
'filterDescendantsDeep',
|
||||||
'findDescendant',
|
'findDescendant',
|
||||||
|
'findDescendantDeep',
|
||||||
|
'getAncestors',
|
||||||
'getBlocks',
|
'getBlocks',
|
||||||
'getBlocksAtRange',
|
'getBlocksAtRange',
|
||||||
'getCharactersAtRange',
|
'getCharactersAtRange',
|
||||||
@@ -1228,6 +1331,7 @@ memoize(Node, [
|
|||||||
'getDepth',
|
'getDepth',
|
||||||
'getDescendant',
|
'getDescendant',
|
||||||
'getDescendantDecorators',
|
'getDescendantDecorators',
|
||||||
|
'getFirstText',
|
||||||
'getFragmentAtRange',
|
'getFragmentAtRange',
|
||||||
'getFurthest',
|
'getFurthest',
|
||||||
'getFurthestBlock',
|
'getFurthestBlock',
|
||||||
@@ -1235,6 +1339,7 @@ memoize(Node, [
|
|||||||
'getHighestChild',
|
'getHighestChild',
|
||||||
'getHighestOnlyChildParent',
|
'getHighestOnlyChildParent',
|
||||||
'getInlinesAtRange',
|
'getInlinesAtRange',
|
||||||
|
'getLastText',
|
||||||
'getMarksAtRange',
|
'getMarksAtRange',
|
||||||
'getNextBlock',
|
'getNextBlock',
|
||||||
'getNextSibling',
|
'getNextSibling',
|
||||||
|
@@ -149,7 +149,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +161,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +203,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +216,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +261,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +275,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -45,7 +45,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,6 +12,8 @@ It doesn't hardcode any information about the schema itself (like which tag mean
|
|||||||
|
|
||||||
It handles all of the heavy lifting of actually parsing the HTML, and iterating over the elements, and all you have to supply it is a `serialize()` and `deserialize()` function for each type of [`Node`](../models#node) or [`Mark`](../models/#mark) you want it to handle.
|
It handles all of the heavy lifting of actually parsing the HTML, and iterating over the elements, and all you have to supply it is a `serialize()` and `deserialize()` function for each type of [`Node`](../models#node) or [`Mark`](../models/#mark) you want it to handle.
|
||||||
|
|
||||||
|
If called with `{render: false}` as the optional second argument, the serializer will return an iterable list of the top-level React elements generated, instead of automatically rendering these to a markup string.
|
||||||
|
|
||||||
|
|
||||||
#### Raw
|
#### Raw
|
||||||
|
|
||||||
@@ -20,4 +22,3 @@ The `Raw` serializer is the simplest serializer, which translates a [`State`](..
|
|||||||
It doesn't just use Immutable.js's [`.toJSON()`](https://facebook.github.io/immutable-js/docs/#/List/toJS) method. Instead, it performs a little bit of "minifying" logic to reduce unnecessary information from being in the raw output.
|
It doesn't just use Immutable.js's [`.toJSON()`](https://facebook.github.io/immutable-js/docs/#/List/toJS) method. Instead, it performs a little bit of "minifying" logic to reduce unnecessary information from being in the raw output.
|
||||||
|
|
||||||
It also transforms [`Text`](../models#text) nodes's content from being organized by [`Characters`](../models#character) into the concept of "ranges", which have a unique set of [`Marks`](../models#mark).
|
It also transforms [`Text`](../models#text) nodes's content from being organized by [`Characters`](../models#character) into the concept of "ranges", which have a unique set of [`Marks`](../models#mark).
|
||||||
|
|
||||||
|
@@ -225,12 +225,16 @@ class Html {
|
|||||||
* Serialize a `state` object into an HTML string.
|
* Serialize a `state` object into an HTML string.
|
||||||
*
|
*
|
||||||
* @param {State} state
|
* @param {State} state
|
||||||
* @return {String} html
|
* @param {Object} options
|
||||||
|
* @property {Boolean} render
|
||||||
|
* @return {String|Array} html
|
||||||
*/
|
*/
|
||||||
|
|
||||||
serialize = (state) => {
|
serialize = (state, options = {}) => {
|
||||||
const { document } = state
|
const { document } = state
|
||||||
const elements = document.nodes.map(this.serializeNode)
|
const elements = document.nodes.map(this.serializeNode)
|
||||||
|
if (options.render === false) return elements
|
||||||
|
|
||||||
const html = ReactDOMServer.renderToStaticMarkup(<body>{elements}</body>)
|
const html = ReactDOMServer.renderToStaticMarkup(<body>{elements}</body>)
|
||||||
const inner = html.slice(6, -7)
|
const inner = html.slice(6, -7)
|
||||||
return inner
|
return inner
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,6 +113,7 @@ export function deleteBackward(transform, n = 1) {
|
|||||||
export function deleteForward(transform, n = 1) {
|
export function deleteForward(transform, n = 1) {
|
||||||
const { state } = transform
|
const { state } = transform
|
||||||
const { selection } = state
|
const { selection } = state
|
||||||
|
|
||||||
return transform
|
return transform
|
||||||
.deleteForwardAtRange(selection, n)
|
.deleteForwardAtRange(selection, n)
|
||||||
.collapseToEnd()
|
.collapseToEnd()
|
||||||
@@ -156,9 +157,10 @@ 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))
|
||||||
|
|
||||||
transform.unsetSelection()
|
transform.unsetSelection()
|
||||||
transform.insertFragmentAtRange(selection, fragment)
|
transform.insertFragmentAtRange(selection, fragment)
|
||||||
@@ -167,17 +169,13 @@ export function insertFragment(transform, fragment) {
|
|||||||
|
|
||||||
const keys = beforeTexts.map(text => text.key)
|
const keys = beforeTexts.map(text => text.key)
|
||||||
const news = document.getTexts().filter(n => !keys.includes(n.key))
|
const news = document.getTexts().filter(n => !keys.includes(n.key))
|
||||||
const text = news.size ? news.takeLast(2).first() : null
|
const text = appending ? news.last() : news.takeLast(2).first()
|
||||||
let after
|
let after
|
||||||
|
|
||||||
if (text && lastInline) {
|
if (text && lastInline) {
|
||||||
after = selection.collapseToEndOf(text)
|
after = selection.collapseToEndOf(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (text && lastInline) {
|
|
||||||
after = selection.collapseToStart()
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (text) {
|
else if (text) {
|
||||||
after = selection
|
after = selection
|
||||||
.collapseToStartOf(text)
|
.collapseToStartOf(text)
|
||||||
@@ -495,7 +493,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -65,6 +65,8 @@ export function deleteAtRange(transform, range, options = {}) {
|
|||||||
|
|
||||||
let { state } = transform
|
let { state } = transform
|
||||||
let { document } = state
|
let { document } = state
|
||||||
|
|
||||||
|
// split the nodes at range, within the common ancestor
|
||||||
let ancestor = document.getCommonAncestor(startKey, endKey)
|
let ancestor = document.getCommonAncestor(startKey, endKey)
|
||||||
let startChild = ancestor.getHighestChild(startKey)
|
let startChild = ancestor.getHighestChild(startKey)
|
||||||
let endChild = ancestor.getHighestChild(endKey)
|
let endChild = ancestor.getHighestChild(endKey)
|
||||||
@@ -76,18 +78,16 @@ export function deleteAtRange(transform, range, options = {}) {
|
|||||||
|
|
||||||
state = transform.state
|
state = transform.state
|
||||||
document = state.document
|
document = state.document
|
||||||
ancestor = document.getCommonAncestor(startKey, endKey)
|
|
||||||
const startBlock = document.getClosestBlock(startKey)
|
const startBlock = document.getClosestBlock(startKey)
|
||||||
const endBlock = document.getClosestBlock(document.getNextText(endKey))
|
const endBlock = document.getClosestBlock(document.getNextText(endKey))
|
||||||
|
|
||||||
|
// remove all of the nodes between range
|
||||||
|
ancestor = document.getCommonAncestor(startKey, endKey)
|
||||||
startChild = ancestor.getHighestChild(startKey)
|
startChild = ancestor.getHighestChild(startKey)
|
||||||
endChild = ancestor.getHighestChild(endKey)
|
endChild = ancestor.getHighestChild(endKey)
|
||||||
|
|
||||||
const startIndex = ancestor.nodes.indexOf(startChild)
|
const startIndex = ancestor.nodes.indexOf(startChild)
|
||||||
const endIndex = ancestor.nodes.indexOf(endChild)
|
const endIndex = ancestor.nodes.indexOf(endChild)
|
||||||
const middles = ancestor.nodes.slice(
|
const middles = ancestor.nodes.slice(startIndex + 1, endIndex + 1)
|
||||||
startIndex + 1,
|
|
||||||
endIndex + 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if (middles.size) {
|
if (middles.size) {
|
||||||
// remove first nodes directly so the document is not normalized
|
// remove first nodes directly so the document is not normalized
|
||||||
@@ -110,6 +110,9 @@ export function deleteAtRange(transform, range, options = {}) {
|
|||||||
if (normalize) {
|
if (normalize) {
|
||||||
transform.normalizeNodeByKey(ancestor.key)
|
transform.normalizeNodeByKey(ancestor.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transform.normalizeDocument()
|
||||||
|
|
||||||
return transform
|
return transform
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
|
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
|
import type from 'type-of'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import readMetadata from 'read-metadata'
|
import readMetadata from 'read-metadata'
|
||||||
import strip from '../helpers/strip-dynamic'
|
import strip from '../helpers/strip-dynamic'
|
||||||
import { Html, Json, Plain, Raw } from '../..'
|
import { Html, Json, Plain, Raw } from '../..'
|
||||||
import { equal, strictEqual } from '../helpers/assert-json'
|
import { equal, strictEqual } from '../helpers/assert-json'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
|
import React from 'react'
|
||||||
|
import { Iterable } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests.
|
* Tests.
|
||||||
@@ -46,6 +49,14 @@ describe('serializers', () => {
|
|||||||
strictEqual(serialized, expected.trim())
|
strictEqual(serialized, expected.trim())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('optionally returns an iterable list of React elements', () => {
|
||||||
|
const html = new Html(require('./fixtures/html/serialize/block-nested').default)
|
||||||
|
const input = require('./fixtures/html/serialize/block-nested/input.js').default
|
||||||
|
const serialized = html.serialize(input, { render: false })
|
||||||
|
assert(Iterable.isIterable(serialized), 'did not return an interable list')
|
||||||
|
assert(React.isValidElement(serialized.first()), 'did not return valid React elements')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: list-item
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: fragment
|
||||||
|
- kind: block
|
||||||
|
type: list-item
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: second fragment
|
||||||
|
- kind: block
|
||||||
|
type: list-item
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: third fragment
|
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
import assert from 'assert'
|
||||||
|
import path from 'path'
|
||||||
|
import readMetadata from 'read-metadata'
|
||||||
|
import { Raw } from '../../../../../..'
|
||||||
|
|
||||||
|
export default function (state) {
|
||||||
|
const file = path.resolve(__dirname, 'fragment.yaml')
|
||||||
|
const raw = readMetadata.sync(file)
|
||||||
|
const fragment = Raw.deserialize(raw, { terse: true }).document
|
||||||
|
|
||||||
|
const { document, selection } = state
|
||||||
|
const texts = document.getTexts()
|
||||||
|
const first = texts.first()
|
||||||
|
const range = selection.merge({
|
||||||
|
anchorKey: first.key,
|
||||||
|
anchorOffset: first.length,
|
||||||
|
focusKey: first.key,
|
||||||
|
focusOffset: first.length
|
||||||
|
})
|
||||||
|
|
||||||
|
const next = state
|
||||||
|
.transform()
|
||||||
|
.moveTo(range)
|
||||||
|
.insertFragment(fragment)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
const last = next.document.getTexts().last()
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
next.selection.toJS(),
|
||||||
|
range.merge({
|
||||||
|
anchorKey: last.key,
|
||||||
|
anchorOffset: last.length,
|
||||||
|
focusKey: last.key,
|
||||||
|
focusOffset: last.length
|
||||||
|
}).toJS()
|
||||||
|
)
|
||||||
|
|
||||||
|
return next
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: paragraph
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: word
|
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: paragraph
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: wordfragment
|
||||||
|
- kind: block
|
||||||
|
type: list-item
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: second fragment
|
||||||
|
- kind: block
|
||||||
|
type: list-item
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: third fragment
|
Reference in New Issue
Block a user