mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-04-21 13:51:59 +02:00
add transforms, document, and normalizing
This commit is contained in:
parent
9fb8e674fd
commit
3bc080d967
examples
lib
components
models
plugins
serializers
@ -7,4 +7,6 @@ html {
|
||||
main {
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
max-width: 40em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
@ -7,4 +7,6 @@ html {
|
||||
main {
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
max-width: 40em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import { Plaintext } from '../..'
|
||||
* State.
|
||||
*/
|
||||
|
||||
const state = 'A string of plain text.'
|
||||
const state = 'This is editable plain text, just like a <textarea>!'
|
||||
|
||||
/**
|
||||
* App.
|
||||
|
@ -53,13 +53,13 @@ class Content extends React.Component {
|
||||
|
||||
onSelect(e) {
|
||||
let { state } = this.props
|
||||
let { selection } = state
|
||||
let { document, selection } = state
|
||||
const native = window.getSelection()
|
||||
|
||||
// No selection is active, so unset `isFocused`.
|
||||
if (!native.rangeCount && selection.isFocused) {
|
||||
selection = selection.set('isFocused', false)
|
||||
state = state.set('selection', selection)
|
||||
selection = selection.merge({ isFocused: false })
|
||||
state = state.merge({ selection })
|
||||
this.onChange(state)
|
||||
return
|
||||
}
|
||||
@ -68,7 +68,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 = state.filterNodes((node) => {
|
||||
const edges = document.filterNodes((node) => {
|
||||
return node.key == anchor.key || node.key == focus.key
|
||||
})
|
||||
|
||||
@ -86,7 +86,7 @@ class Content extends React.Component {
|
||||
isFocused: true
|
||||
})
|
||||
|
||||
state = state.set('selection', selection)
|
||||
state = state.merge({ selection })
|
||||
this.onChange(state)
|
||||
}
|
||||
|
||||
@ -98,12 +98,22 @@ class Content extends React.Component {
|
||||
|
||||
onBeforeInput(e) {
|
||||
let { state } = this.props
|
||||
const { selection } = state
|
||||
const { data } = e
|
||||
if (!data) return
|
||||
|
||||
e.preventDefault()
|
||||
if (state.isExpanded) state = state.delete()
|
||||
state = state.insert(data)
|
||||
let transform = state.transform()
|
||||
|
||||
// If the selection is still expanded, delete anything inside it first.
|
||||
if (selection.isExpanded) {
|
||||
transform = transform.delete()
|
||||
}
|
||||
|
||||
state = transform
|
||||
.insert(data)
|
||||
.apply()
|
||||
|
||||
this.onChange(state)
|
||||
}
|
||||
|
||||
@ -115,10 +125,10 @@ class Content extends React.Component {
|
||||
|
||||
render() {
|
||||
const { state } = this.props
|
||||
const { nodes } = state
|
||||
const children = nodes
|
||||
.toArray()
|
||||
const { document } = state
|
||||
const children = document.nodes
|
||||
.map(node => this.renderNode(node))
|
||||
.toArray()
|
||||
|
||||
const style = {
|
||||
outline: 'none', // prevent the default outline styles
|
||||
@ -165,8 +175,8 @@ class Content extends React.Component {
|
||||
|
||||
const Component = renderNode(node)
|
||||
const children = node.nodes
|
||||
.toArray()
|
||||
.map(node => this.renderNode(node))
|
||||
.toArray()
|
||||
|
||||
return (
|
||||
<Component
|
||||
|
382
lib/models/document.js
Normal file
382
lib/models/document.js
Normal file
@ -0,0 +1,382 @@
|
||||
|
||||
import Character from './character'
|
||||
import Node from './node'
|
||||
import Selection from './selection'
|
||||
import Text from './text'
|
||||
import { OrderedMap, Record } from 'immutable'
|
||||
|
||||
/**
|
||||
* Defaults.
|
||||
*/
|
||||
|
||||
const DEFAULT_PROPERTIES = {
|
||||
nodes: new OrderedMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Node-like methods, that should be mixed into the `Document` prototype.
|
||||
*/
|
||||
|
||||
const NODE_LIKE_METHODS = [
|
||||
'filterNodes',
|
||||
'findNode',
|
||||
'getNextNode',
|
||||
'getNode',
|
||||
'getParentNode',
|
||||
'getPreviousNode',
|
||||
'hasNode',
|
||||
'pushNode',
|
||||
'removeNode',
|
||||
'updateNode'
|
||||
]
|
||||
|
||||
/**
|
||||
* Document.
|
||||
*/
|
||||
|
||||
class Document extends Record(DEFAULT_PROPERTIES) {
|
||||
|
||||
/**
|
||||
* Create a new `Document` with `properties`.
|
||||
*
|
||||
* @param {Objetc} properties
|
||||
* @return {Document} document
|
||||
*/
|
||||
|
||||
static create(properties = {}) {
|
||||
return new Document(properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Node-like getters.
|
||||
*/
|
||||
|
||||
get length() {
|
||||
return this.text.length
|
||||
}
|
||||
|
||||
get text() {
|
||||
return this.nodes
|
||||
.map(node => node.text)
|
||||
.join('')
|
||||
}
|
||||
|
||||
get type() {
|
||||
return 'document'
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete everything in a `range`.
|
||||
*
|
||||
* @param {Selection} range
|
||||
* @return {Document} document
|
||||
*/
|
||||
|
||||
deleteAtRange(range) {
|
||||
let document = this
|
||||
|
||||
// If the range is collapsed, there's nothing to do.
|
||||
if (range.isCollapsed) return document
|
||||
|
||||
const { startKey, startOffset, endKey, endOffset } = range
|
||||
let startNode = document.getNode(startKey)
|
||||
|
||||
// If the start and end nodes are the same, remove the matching characters.
|
||||
if (startKey == endKey) {
|
||||
let { characters } = startNode
|
||||
|
||||
characters = characters.filterNot((char, i) => {
|
||||
return startOffset <= i && i < endOffset
|
||||
})
|
||||
|
||||
startNode = startNode.merge({ characters })
|
||||
document = document.updateNode(startNode)
|
||||
return document
|
||||
}
|
||||
|
||||
// Otherwise, remove the text from the first and last nodes...
|
||||
const startRange = Selection.create({
|
||||
anchorKey: startKey,
|
||||
anchorOffset: startOffset,
|
||||
focusKey: startKey,
|
||||
focusOffset: startNode.length
|
||||
})
|
||||
|
||||
const endRange = Selection.create({
|
||||
anchorKey: endKey,
|
||||
anchorOffset: 0,
|
||||
focusKey: endKey,
|
||||
focusOffset: endOffset
|
||||
})
|
||||
|
||||
document = document.deleteAtRange(startRange)
|
||||
document = document.deleteAtRange(endRange)
|
||||
|
||||
// Then remove any nodes in between the top-most start and end nodes...
|
||||
let startParent = document.getParentNode(startKey)
|
||||
let endParent = document.getParentNode(endKey)
|
||||
|
||||
const startGrandestParent = document.nodes.find((node) => {
|
||||
return node == startParent || node.hasNode(startParent)
|
||||
})
|
||||
|
||||
const endGrandestParent = document.nodes.find((node) => {
|
||||
return node == endParent || node.hasNode(endParent)
|
||||
})
|
||||
|
||||
const nodes = document.nodes
|
||||
.takeUntil(node => node == startGrandestParent)
|
||||
.set(startGrandestParent.key, startGrandestParent)
|
||||
.concat(document.nodes.skipUntil(node => node == endGrandestParent))
|
||||
|
||||
document = document.merge({ nodes })
|
||||
|
||||
// Then add the end parent's nodes to the start parent node.
|
||||
const newNodes = startParent.nodes.concat(endParent.nodes)
|
||||
startParent = startParent.merge({ nodes: newNodes })
|
||||
document = document.updateNode(startParent)
|
||||
|
||||
// Then remove the end parent.
|
||||
let endGrandparent = document.getParentNode(endParent)
|
||||
if (endGrandparent == document) {
|
||||
document = document.removeNode(endParent)
|
||||
} else {
|
||||
endGrandparent = endGrandparent.removeNode(endParent)
|
||||
document = document.updateNode(endGrandparent)
|
||||
}
|
||||
|
||||
// Normalize the document.
|
||||
return document.normalize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete backward `n` characters at a `range`.
|
||||
*
|
||||
* @param {Selection} range
|
||||
* @param {Number} n (optional)
|
||||
* @return {Document} document
|
||||
*/
|
||||
|
||||
deleteBackwardAtRange(range, n = 1) {
|
||||
let document = this
|
||||
|
||||
// When collapsed at the end of the document, there's nothing to do.
|
||||
if (range.isCollapsed && range.isAtEndOf(document)) return document
|
||||
|
||||
// When the range is still expanded, just do a regular delete.
|
||||
if (range.isExpanded) return document.deleteAtRange(range)
|
||||
|
||||
// When at start of a text node, merge forwards into the next text node.
|
||||
const { startKey } = range
|
||||
const startNode = document.getNode(startKey)
|
||||
|
||||
if (range.isAtStartOf(startNode)) {
|
||||
const parent = document.getParentNode(startNode)
|
||||
const previous = document.getPreviousNode(parent).nodes.first()
|
||||
range = range.extendBackwardToEndOf(previous)
|
||||
document = document.deleteAtRange(range)
|
||||
return document
|
||||
}
|
||||
|
||||
// Otherwise, remove `n` characters behind of the cursor.
|
||||
range = range.extendBackward(n)
|
||||
document = document.deleteAtRange(range)
|
||||
|
||||
// Normalize the document.
|
||||
return document.normalize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete forward `n` characters at a `range`.
|
||||
*
|
||||
* @param {Selection} range
|
||||
* @param {Number} n (optional)
|
||||
* @return {Document} document
|
||||
*/
|
||||
|
||||
deleteForwardAtRange(range, n = 1) {
|
||||
let document = this
|
||||
|
||||
// When collapsed at the end of the document, there's nothing to do.
|
||||
if (range.isCollapsed && range.isAtEndOf(document)) return document
|
||||
|
||||
// When the range is still expanded, just do a regular delete.
|
||||
if (range.isExpanded) return document.deleteAtRange(range)
|
||||
|
||||
// When at end of a text node, merge forwards into the next text node.
|
||||
const { startKey } = range
|
||||
const startNode = document.getNode(startKey)
|
||||
|
||||
if (range.isAtEndOf(startNode)) {
|
||||
const parent = document.getParentNode(startNode)
|
||||
const next = document.getNextNode(parent).nodes.first()
|
||||
range = range.extendForwardToStartOf(next)
|
||||
document = document.deleteAtRange(range)
|
||||
return document
|
||||
}
|
||||
|
||||
// Otherwise, remove `n` characters ahead of the cursor.
|
||||
range = range.extendForward(n)
|
||||
document = document.deleteAtRange(range)
|
||||
|
||||
// Normalize the document.
|
||||
return document.normalize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert `data` at a `range`.
|
||||
*
|
||||
* @param {Selection} range
|
||||
* @param {String or Node or OrderedMap} data
|
||||
* @return {Document} document
|
||||
*/
|
||||
|
||||
insertAtRange(range, data) {
|
||||
let document = this
|
||||
|
||||
// When still expanded, remove the current range first.
|
||||
if (range.isExpanded) {
|
||||
document = document.deleteAtRange(range)
|
||||
range = range.moveToStart()
|
||||
}
|
||||
|
||||
// When the data is a string of characters...
|
||||
if (typeof data == 'string') {
|
||||
let { startNode, startOffset } = document
|
||||
let { characters } = startNode
|
||||
|
||||
// Create a list of the new characters, with the right marks.
|
||||
const marks = characters.has(startOffset)
|
||||
? characters.get(startOffset).marks
|
||||
: null
|
||||
|
||||
const newCharacters = data.split('').reduce((list, char) => {
|
||||
const obj = { text: char }
|
||||
if (marks) obj.marks = marks
|
||||
return list.push(Character.create(obj))
|
||||
}, Character.createList())
|
||||
|
||||
// Splice in the new characters.
|
||||
const resumeOffset = startOffset + data.length - 1
|
||||
characters = characters.slice(0, startOffset)
|
||||
.concat(newCharacters)
|
||||
.concat(characters.slice(resumeOffset, Infinity))
|
||||
|
||||
// Update the existing text node.
|
||||
startNode = startNode.merge({ characters })
|
||||
document = document.updateNode(startNode)
|
||||
return document
|
||||
}
|
||||
|
||||
// Normalize the document.
|
||||
return document.normalize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the document, joining any two adjacent text nodes.
|
||||
*
|
||||
* @return {Document} document
|
||||
*/
|
||||
|
||||
normalize() {
|
||||
let document = this
|
||||
let first = document.findNode((node) => {
|
||||
if (node.type != 'text') return
|
||||
const parent = document.getParentNode(node)
|
||||
const next = parent.getNextNode(node)
|
||||
return next && next.type == 'text'
|
||||
})
|
||||
|
||||
// If no text node was followed by another, do nothing.
|
||||
if (!first) return document
|
||||
|
||||
// Otherwise, add the text of the second node to the first...
|
||||
let parent = document.getParentNode(first)
|
||||
const second = parent.getNextNode(first)
|
||||
const characters = first.characters.concat(second.characters)
|
||||
first = first.merge({ characters })
|
||||
parent = parent.updateNode(first)
|
||||
|
||||
// Then remove the second node.
|
||||
parent = parent.removeNode(second)
|
||||
document = document.updateNode(parent)
|
||||
|
||||
// Finally, recurse by normalizing again.
|
||||
return document.normalize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the nodes at a `range`.
|
||||
*
|
||||
* @param {Selection} range
|
||||
* @return {Document} document
|
||||
*/
|
||||
|
||||
splitAtRange(range) {
|
||||
let document = this
|
||||
|
||||
// If the range is expanded, remove it first.
|
||||
if (range.isExpanded) {
|
||||
document = document.deleteAtRange(range)
|
||||
range = range.moveToStart()
|
||||
}
|
||||
|
||||
const { startKey, startOffset } = range
|
||||
const startNode = document.getNode(startKey)
|
||||
|
||||
// Split the text node's characters.
|
||||
const { characters, length } = startNode
|
||||
const firstCharacters = characters.take(startOffset)
|
||||
const secondCharacters = characters.takeLast(length - startOffset)
|
||||
|
||||
// Create a new first node with only the first set of characters.
|
||||
const parent = document.getParentNode(startNode)
|
||||
const firstText = startNode.set('characters', firstCharacters)
|
||||
const firstNode = parent.updateNode(firstText)
|
||||
|
||||
// Create a brand new second node with the second set of characters.
|
||||
let secondText = Text.create({})
|
||||
let secondNode = Node.create({
|
||||
type: firstNode.type,
|
||||
data: firstNode.data
|
||||
})
|
||||
|
||||
secondText = secondText.set('characters', secondCharacters)
|
||||
secondNode = secondNode.pushNode(secondText)
|
||||
|
||||
// Replace the old parent node in the grandparent with the two new ones.
|
||||
let grandparent = document.getParentNode(parent)
|
||||
const befores = grandparent.nodes.takeUntil(node => node.key == parent.key)
|
||||
const afters = grandparent.nodes.skipUntil(node => node.key == parent.key).rest()
|
||||
const nodes = befores
|
||||
.set(firstNode.key, firstNode)
|
||||
.set(secondNode.key, secondNode)
|
||||
.concat(afters)
|
||||
|
||||
// If the document is the grandparent, just merge, otherwise deep merge.
|
||||
if (grandparent == document) {
|
||||
document = document.merge({ nodes })
|
||||
} else {
|
||||
grandparent = grandparent.merge({ nodes })
|
||||
document = document.updateNode(grandparent)
|
||||
}
|
||||
|
||||
// Normalize the document.
|
||||
return document.normalize()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix in node-like methods.
|
||||
*/
|
||||
|
||||
NODE_LIKE_METHODS.forEach((method) => {
|
||||
Document.prototype[method] = Node.prototype[method]
|
||||
})
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*/
|
||||
|
||||
export default Document
|
@ -1,63 +1,45 @@
|
||||
|
||||
import Character from './character'
|
||||
import Node from './node'
|
||||
import Document from './document'
|
||||
import Selection from './selection'
|
||||
import Text from './text'
|
||||
import toCamel from 'to-camel-case'
|
||||
import { OrderedMap, Record, Stack } from 'immutable'
|
||||
import Transform from './transform'
|
||||
import { Record, Stack } from 'immutable'
|
||||
|
||||
/**
|
||||
* Record.
|
||||
* History.
|
||||
*/
|
||||
|
||||
const StateRecord = new Record({
|
||||
nodes: new OrderedMap(),
|
||||
selection: new Selection(),
|
||||
undoStack: new Stack(),
|
||||
redoStack: new Stack()
|
||||
const History = new Record({
|
||||
undos: new Stack(),
|
||||
redos: new Stack()
|
||||
})
|
||||
|
||||
/**
|
||||
* Node-like methods, that should be mixed into the `State` prototype.
|
||||
* Default properties.
|
||||
*/
|
||||
|
||||
const NODE_LIKE_METHODS = [
|
||||
'filterNodes',
|
||||
'findNode',
|
||||
'getNextNode',
|
||||
'getNode',
|
||||
'getParentNode',
|
||||
'getPreviousNode',
|
||||
'hasNode',
|
||||
'pushNode',
|
||||
'removeNode',
|
||||
'updateNode'
|
||||
]
|
||||
const DEFAULT_PROPERTIES = {
|
||||
document: new Document(),
|
||||
selection: new Selection(),
|
||||
history: new History()
|
||||
}
|
||||
|
||||
/**
|
||||
* Selection-like methods, that should be mixed into the `State` prototype.
|
||||
* Document-like methods, that should be mixed into the `State` prototype.
|
||||
*/
|
||||
|
||||
const SELECTION_LIKE_METHODS = [
|
||||
'moveTo',
|
||||
'moveToAnchor',
|
||||
'moveToEnd',
|
||||
'moveToFocus',
|
||||
'moveToStart',
|
||||
'moveToStartOf',
|
||||
'moveToEndOf',
|
||||
'moveToRangeOf',
|
||||
'moveForward',
|
||||
'moveBackward',
|
||||
'extendForward',
|
||||
'extendBackward'
|
||||
const DOCUMENT_LIKE_METHODS = [
|
||||
'deleteAtRange',
|
||||
'deleteBackwardAtRange',
|
||||
'deleteForwardAtRange',
|
||||
'insertAtRange',
|
||||
'splitAtRange'
|
||||
]
|
||||
|
||||
/**
|
||||
* State.
|
||||
*/
|
||||
|
||||
class State extends StateRecord {
|
||||
class State extends Record(DEFAULT_PROPERTIES) {
|
||||
|
||||
/**
|
||||
* Create a new `State` with `properties`.
|
||||
@ -71,131 +53,13 @@ class State extends StateRecord {
|
||||
}
|
||||
|
||||
/**
|
||||
* Node-like getters.
|
||||
*/
|
||||
|
||||
get length() {
|
||||
return this.text.length
|
||||
}
|
||||
|
||||
get text() {
|
||||
return this.nodes
|
||||
.map(node => node.text)
|
||||
.join('')
|
||||
}
|
||||
|
||||
get type() {
|
||||
return 'state'
|
||||
}
|
||||
|
||||
/**
|
||||
* Selection-like getters.
|
||||
*/
|
||||
|
||||
get isCollapsed() {
|
||||
return this.selection.isCollapsed
|
||||
}
|
||||
|
||||
get isExpanded() {
|
||||
return this.selection.isExpanded
|
||||
}
|
||||
|
||||
get isExtended() {
|
||||
return this.selection.isExtended
|
||||
}
|
||||
|
||||
get anchorKey() {
|
||||
return this.selection.anchorKey
|
||||
}
|
||||
|
||||
get anchorOffset() {
|
||||
return this.selection.anchorOffset
|
||||
}
|
||||
|
||||
get focusKey() {
|
||||
return this.selection.focusKey
|
||||
}
|
||||
|
||||
get focusOffset() {
|
||||
return this.selection.focusOffset
|
||||
}
|
||||
|
||||
get startKey() {
|
||||
return this.selection.startKey
|
||||
}
|
||||
|
||||
get startOffset() {
|
||||
return this.selection.startOffset
|
||||
}
|
||||
|
||||
get endKey() {
|
||||
return this.selection.endKey
|
||||
}
|
||||
|
||||
get endOffset() {
|
||||
return this.selection.endOffset
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current anchor node.
|
||||
* Return a new `Transform` with the current state as a starting point.
|
||||
*
|
||||
* @return {Node} node
|
||||
* @return {Transform} transform
|
||||
*/
|
||||
|
||||
get anchorNode() {
|
||||
return this.getNode(this.anchorKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current focus node.
|
||||
*
|
||||
* @return {Node} node
|
||||
*/
|
||||
|
||||
get focusNode() {
|
||||
return this.getNode(this.focusKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current start node.
|
||||
*
|
||||
* @return {Node} node
|
||||
*/
|
||||
|
||||
get startNode() {
|
||||
return this.getNode(this.startKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current end node.
|
||||
*
|
||||
* @return {Node} node
|
||||
*/
|
||||
|
||||
get endNode() {
|
||||
return this.getNode(this.endKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the selection at the start of `node`?
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {Boolean} isAtStart
|
||||
*/
|
||||
|
||||
isAtStartOf(node) {
|
||||
return this.selection.isAtStartOf(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the selection at the end of `node`?
|
||||
*
|
||||
* @param {Node} node
|
||||
* @return {Boolean} isAtEnd
|
||||
*/
|
||||
|
||||
isAtEndOf(node) {
|
||||
return this.selection.isAtEndOf(node)
|
||||
transform() {
|
||||
return new Transform({ state: this })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -206,88 +70,15 @@ class State extends StateRecord {
|
||||
|
||||
delete() {
|
||||
let state = this
|
||||
let { document, selection } = state
|
||||
|
||||
// When collapsed, there's nothing to do.
|
||||
if (state.isCollapsed) return state
|
||||
if (selection.isCollapsed) return state
|
||||
|
||||
// Otherwise, delete and update the selection.
|
||||
state = state.deleteAtRange(state.selection)
|
||||
state = state.moveToStart()
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete everything in a `range`.
|
||||
*
|
||||
* @param {Selection} range
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
deleteAtRange(range) {
|
||||
let state = this
|
||||
|
||||
// If the range is collapsed, there's nothing to do.
|
||||
if (range.isCollapsed) return state
|
||||
|
||||
const { startKey, startOffset, endKey, endOffset } = range
|
||||
let startNode = state.getNode(startKey)
|
||||
|
||||
// If the start and end nodes are the same, remove the matching characters.
|
||||
if (startKey == endKey) {
|
||||
let { characters } = startNode
|
||||
|
||||
characters = characters.filterNot((char, i) => {
|
||||
return startOffset <= i && i < endOffset
|
||||
})
|
||||
|
||||
startNode = startNode.merge({ characters })
|
||||
state = state.updateNode(startNode)
|
||||
return state
|
||||
}
|
||||
|
||||
// Otherwise, remove the text from the first and last nodes...
|
||||
const startRange = Selection.create({
|
||||
anchorKey: startKey,
|
||||
anchorOffset: startOffset,
|
||||
focusKey: startKey,
|
||||
focusOffset: startNode.length
|
||||
})
|
||||
|
||||
const endRange = Selection.create({
|
||||
anchorKey: endKey,
|
||||
anchorOffset: 0,
|
||||
focusKey: endKey,
|
||||
focusOffset: endOffset
|
||||
})
|
||||
|
||||
state = state.deleteAtRange(startRange)
|
||||
state = state.deleteAtRange(endRange)
|
||||
|
||||
// Then remove any nodes in between the top-most start and end nodes...
|
||||
let startParent = state.getParentNode(startKey)
|
||||
let endParent = state.getParentNode(endKey)
|
||||
|
||||
const startGrandestParent = state.nodes.find((node) => {
|
||||
return node == startParent || node.hasNode(startParent)
|
||||
})
|
||||
|
||||
const endGrandestParent = state.nodes.find((node) => {
|
||||
return node == endParent || node.hasNode(endParent)
|
||||
})
|
||||
|
||||
const nodes = state.nodes
|
||||
.takeUntil(node => node == startGrandestParent)
|
||||
.set(startGrandestParent.key, startGrandestParent)
|
||||
.concat(state.nodes.skipUntil(node => node == endGrandestParent))
|
||||
|
||||
state = state.merge({ nodes })
|
||||
|
||||
// Then bring the end text node into the start node.
|
||||
let endText = state.getNode(endKey)
|
||||
startParent = startParent.pushNode(endText)
|
||||
endParent = endParent.removeNode(endText)
|
||||
state = state.updateNode(startParent)
|
||||
state = state.updateNode(endParent)
|
||||
document = document.deleteAtRange(selection)
|
||||
selection = selection.moveToStart()
|
||||
state = state.merge({ document, selection })
|
||||
return state
|
||||
}
|
||||
|
||||
@ -300,63 +91,31 @@ class State extends StateRecord {
|
||||
|
||||
deleteBackward(n = 1) {
|
||||
let state = this
|
||||
let selection = state.selection
|
||||
let { document, selection } = state
|
||||
let after = selection
|
||||
|
||||
// Determine what the selection should be after deleting.
|
||||
const startNode = state.startNode
|
||||
const { startKey } = selection
|
||||
const startNode = document.getNode(startKey)
|
||||
|
||||
if (state.isExpanded) {
|
||||
selection = selection.moveToStart()
|
||||
if (selection.isExpanded) {
|
||||
after = selection.moveToStart()
|
||||
}
|
||||
|
||||
else if (state.isAtStartOf(startNode)) {
|
||||
const parent = state.getParentNode(startNode)
|
||||
const previous = state.getPreviousNode(parent).nodes.first()
|
||||
selection = selection.moveToEndOf(previous)
|
||||
else if (selection.isAtStartOf(startNode)) {
|
||||
const parent = document.getParentNode(startNode)
|
||||
const previous = document.getPreviousNode(parent).nodes.first()
|
||||
after = selection.moveToEndOf(previous)
|
||||
}
|
||||
|
||||
else if (!state.isAtEndOf(state)) {
|
||||
selection = selection.moveBackward(n)
|
||||
else if (!selection.isAtEndOf(document)) {
|
||||
after = selection.moveBackward(n)
|
||||
}
|
||||
|
||||
// Delete backward and then update the selection.
|
||||
state = state.deleteBackwardAtRange(state.selection)
|
||||
state = state.merge({ selection })
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete backward `n` characters at a `range`.
|
||||
*
|
||||
* @param {Selection} range
|
||||
* @param {Number} n (optional)
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
deleteBackwardAtRange(range, n = 1) {
|
||||
let state = this
|
||||
|
||||
// When collapsed at the end of the document, there's nothing to do.
|
||||
if (range.isCollapsed && range.isAtEndOf(state)) return state
|
||||
|
||||
// When the range is still expanded, just do a regular delete.
|
||||
if (range.isExpanded) return state.deleteAtRange(range)
|
||||
|
||||
// When at start of a text node, merge forwards into the next text node.
|
||||
const { startKey } = range
|
||||
const startNode = state.getNode(startKey)
|
||||
|
||||
if (range.isAtStartOf(startNode)) {
|
||||
const parent = state.getParentNode(startNode)
|
||||
const previous = state.getPreviousNode(parent).nodes.first()
|
||||
range = range.extendBackwardToEndOf(previous)
|
||||
state = state.deleteAtRange(range)
|
||||
return state
|
||||
}
|
||||
|
||||
// Otherwise, remove `n` characters behind of the cursor.
|
||||
range = range.extendBackward(n)
|
||||
state = state.deleteAtRange(range)
|
||||
document = document.deleteBackwardAtRange(selection)
|
||||
selection = after
|
||||
state = state.merge({ document, selection })
|
||||
return state
|
||||
}
|
||||
|
||||
@ -369,51 +128,18 @@ class State extends StateRecord {
|
||||
|
||||
deleteForward(n = 1) {
|
||||
let state = this
|
||||
let selection = state.selection
|
||||
let { document, selection } = state
|
||||
let after = selection
|
||||
|
||||
// Determine what the selection should be after deleting.
|
||||
if (state.isExpanded) {
|
||||
selection = selection.moveToStart()
|
||||
if (selection.isExpanded) {
|
||||
after = selection.moveToStart()
|
||||
}
|
||||
|
||||
// Delete forward and then update the selection.
|
||||
state = state.deleteForwardAtRange(state.selection)
|
||||
state = state.merge({ selection })
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete forward `n` characters at a `range`.
|
||||
*
|
||||
* @param {Selection} range
|
||||
* @param {Number} n (optional)
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
deleteForwardAtRange(range, n = 1) {
|
||||
let state = this
|
||||
|
||||
// When collapsed at the end of the document, there's nothing to do.
|
||||
if (range.isCollapsed && range.isAtEndOf(state)) return state
|
||||
|
||||
// When the range is still expanded, just do a regular delete.
|
||||
if (range.isExpanded) return state.deleteAtRange(range)
|
||||
|
||||
// When at end of a text node, merge forwards into the next text node.
|
||||
const { startKey } = range
|
||||
const startNode = state.getNode(startKey)
|
||||
|
||||
if (range.isAtEndOf(startNode)) {
|
||||
const parent = state.getParentNode(startNode)
|
||||
const next = state.getNextNode(parent).nodes.first()
|
||||
range = range.extendForwardToStartOf(next)
|
||||
state = state.deleteAtRange(range)
|
||||
return state
|
||||
}
|
||||
|
||||
// Otherwise, remove `n` characters ahead of the cursor.
|
||||
range = range.extendForward(n)
|
||||
state = state.deleteAtRange(range)
|
||||
document = document.deleteForwardAtRange(selection)
|
||||
selection = after
|
||||
state = state.merge({ document, selection })
|
||||
return state
|
||||
}
|
||||
|
||||
@ -426,246 +152,58 @@ class State extends StateRecord {
|
||||
|
||||
insert(data) {
|
||||
let state = this
|
||||
state = state.insertAtRange(state.selection, data)
|
||||
let { document, selection } = state
|
||||
let after
|
||||
|
||||
// When the data is a string of characters...
|
||||
// Determine what the selection should be after inserting.
|
||||
if (typeof data == 'string') {
|
||||
state = state.moveForward(data.length)
|
||||
after = selection.moveForward(data.length)
|
||||
}
|
||||
|
||||
// Insert the data and update the selection.
|
||||
document = document.insertAtRange(selection, data)
|
||||
selection = after
|
||||
state = state.merge({ document, selection })
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert `data` at a `range`.
|
||||
*
|
||||
* @param {Selection} range
|
||||
* @param {String or Node or OrderedMap} data
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
insertAtRange(range, data) {
|
||||
let state = this
|
||||
|
||||
// When still expanded, remove the current range first.
|
||||
if (range.isExpanded) {
|
||||
state = state.deleteAtRange(range)
|
||||
range = range.moveToStart()
|
||||
}
|
||||
|
||||
// When the data is a string of characters...
|
||||
if (typeof data == 'string') {
|
||||
let { startNode, startOffset } = state
|
||||
let { characters } = startNode
|
||||
|
||||
// Create a list of the new characters, with the right marks.
|
||||
const marks = characters.has(startOffset)
|
||||
? characters.get(startOffset).marks
|
||||
: null
|
||||
|
||||
const newCharacters = data.split('').reduce((list, char) => {
|
||||
const obj = { text: char }
|
||||
if (marks) obj.marks = marks
|
||||
return list.push(Character.create(obj))
|
||||
}, Character.createList())
|
||||
|
||||
// Splice in the new characters.
|
||||
const resumeOffset = startOffset + data.length - 1
|
||||
characters = characters.slice(0, startOffset)
|
||||
.concat(newCharacters)
|
||||
.concat(characters.slice(resumeOffset, Infinity))
|
||||
|
||||
// Update the existing text node.
|
||||
startNode = startNode.merge({ characters })
|
||||
state = state.updateNode(startNode)
|
||||
return state
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize all nodes, ensuring that no two text nodes are adjacent.
|
||||
*
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
normalize() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Split at a `selection`.
|
||||
* Split at a the current cursor position.
|
||||
*
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
split() {
|
||||
let state = this
|
||||
state = state.splitAtRange(state.selection)
|
||||
let { document, selection } = state
|
||||
let after
|
||||
|
||||
const parent = state.getParentNode(state.startNode)
|
||||
const next = state.getNextNode(parent)
|
||||
// Split the document.
|
||||
document = document.splitAtRange(selection)
|
||||
|
||||
// Determine what the selection should be after splitting.
|
||||
const { startKey } = selection
|
||||
const startNode = document.getNode(startKey)
|
||||
const parent = document.getParentNode(startNode)
|
||||
const next = document.getNextNode(parent)
|
||||
const text = next.nodes.first()
|
||||
state = state.moveToStartOf(text)
|
||||
|
||||
// const next = state.getNextTextNode(state.startNode)
|
||||
// state = state.moveToStartOf(next)
|
||||
selection = selection.moveToStartOf(text)
|
||||
|
||||
state = state.merge({ document, selection })
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the nodes at a `range`.
|
||||
*
|
||||
* @param {Selection} range
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
splitAtRange(range) {
|
||||
let state = this
|
||||
|
||||
// If the range is expanded, remove it first.
|
||||
if (range.isExpanded) {
|
||||
state = state.deleteAtRange(range)
|
||||
range = range.moveToStart()
|
||||
}
|
||||
|
||||
const { startKey, startOffset } = range
|
||||
const startNode = state.getNode(startKey)
|
||||
|
||||
// Split the text node's characters.
|
||||
const { characters, length } = startNode
|
||||
const firstCharacters = characters.take(startOffset)
|
||||
const secondCharacters = characters.takeLast(length - startOffset)
|
||||
|
||||
// Create a new first node with only the first set of characters.
|
||||
const parent = state.getParentNode(startNode)
|
||||
const firstText = startNode.set('characters', firstCharacters)
|
||||
const firstNode = parent.updateNode(firstText)
|
||||
|
||||
// Create a brand new second node with the second set of characters.
|
||||
let secondText = Text.create({})
|
||||
let secondNode = Node.create({
|
||||
type: firstNode.type,
|
||||
data: firstNode.data
|
||||
})
|
||||
|
||||
secondText = secondText.set('characters', secondCharacters)
|
||||
secondNode = secondNode.pushNode(secondText)
|
||||
|
||||
// Replace the old parent node in the grandparent with the two new ones.
|
||||
let grandparent = state.getParentNode(parent)
|
||||
const befores = grandparent.nodes.takeUntil(node => node.key == parent.key)
|
||||
const afters = grandparent.nodes.skipUntil(node => node.key == parent.key).rest()
|
||||
const nodes = befores
|
||||
.set(firstNode.key, firstNode)
|
||||
.set(secondNode.key, secondNode)
|
||||
.concat(afters)
|
||||
|
||||
// If the state is the grandparent, just merge, otherwise deep merge.
|
||||
if (grandparent == state) {
|
||||
state = state.merge({ nodes })
|
||||
} else {
|
||||
grandparent = grandparent.merge({ nodes })
|
||||
state = state.updateNode(grandparent)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current state into the history.
|
||||
*
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
save() {
|
||||
let state = this
|
||||
let { undoStack, redoStack } = state
|
||||
|
||||
undoStack = undoStack.unshift(state)
|
||||
redoStack = redoStack.clear()
|
||||
state = state.merge({
|
||||
undoStack,
|
||||
redoStack
|
||||
})
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo.
|
||||
*
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
undo() {
|
||||
let state = this
|
||||
let { undoStack, redoStack } = state
|
||||
|
||||
// If there's no previous state, do nothing.
|
||||
let previous = undoStack.peek()
|
||||
if (!previous) return state
|
||||
|
||||
// Remove the previous state from the undo stack.
|
||||
undoStack = undoStack.shift()
|
||||
|
||||
// Move the current state into the redo stack.
|
||||
redoStack = redoStack.unshift(state)
|
||||
|
||||
// Return the previous state, with the new history.
|
||||
return previous.merge({
|
||||
undoStack,
|
||||
redoStack
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo.
|
||||
*
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
redo() {
|
||||
let state = this
|
||||
let { undoStack, redoStack } = state
|
||||
|
||||
// If there's no next state, do nothing.
|
||||
let next = redoStack.peek()
|
||||
if (!next) return state
|
||||
|
||||
// Remove the next state from the redo stack.
|
||||
redoStack = redoStack.shift()
|
||||
|
||||
// Move the current state into the undo stack.
|
||||
undoStack = undoStack.unshift(state)
|
||||
|
||||
// Return the next state, with the new history.
|
||||
return next.merge({
|
||||
undoStack,
|
||||
redoStack
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix in node-like methods.
|
||||
*/
|
||||
|
||||
NODE_LIKE_METHODS.forEach((method) => {
|
||||
State.prototype[method] = Node.prototype[method]
|
||||
})
|
||||
|
||||
/**
|
||||
* Mix in selection-like methods.
|
||||
*/
|
||||
|
||||
SELECTION_LIKE_METHODS.forEach((method) => {
|
||||
DOCUMENT_LIKE_METHODS.forEach((method) => {
|
||||
State.prototype[method] = function (...args) {
|
||||
let selection = this.selection[method](...args)
|
||||
return this.merge({ selection })
|
||||
let { document } = this
|
||||
document = document[method](...args)
|
||||
return this.merge({ document })
|
||||
}
|
||||
})
|
||||
|
||||
|
175
lib/models/transform.js
Normal file
175
lib/models/transform.js
Normal file
@ -0,0 +1,175 @@
|
||||
|
||||
import { List, Record } from 'immutable'
|
||||
|
||||
/**
|
||||
* Snapshot, with a state-like shape.
|
||||
*/
|
||||
|
||||
const Snapshot = Record({
|
||||
document: null,
|
||||
selection: null
|
||||
})
|
||||
|
||||
/**
|
||||
* Step.
|
||||
*/
|
||||
|
||||
const Step = Record({
|
||||
type: null,
|
||||
args: null
|
||||
})
|
||||
|
||||
/**
|
||||
* Defaults.
|
||||
*/
|
||||
|
||||
const DEFAULT_PROPERTIES = {
|
||||
state: null,
|
||||
steps: new List()
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform types.
|
||||
*/
|
||||
|
||||
const TRANSFORM_TYPES = [
|
||||
'delete',
|
||||
'deleteAtRange',
|
||||
'deleteBackward',
|
||||
'deleteBackwardAtRange',
|
||||
'deleteForward',
|
||||
'deleteForwardAtRange',
|
||||
'insert',
|
||||
'insertAtRange',
|
||||
'split',
|
||||
'splitAtRange'
|
||||
]
|
||||
|
||||
/**
|
||||
* Transform.
|
||||
*/
|
||||
|
||||
class Transform extends Record(DEFAULT_PROPERTIES) {
|
||||
|
||||
/**
|
||||
* Create a history-ready snapshot of the current state.
|
||||
*
|
||||
* @return {Snapshot} snapshot
|
||||
*/
|
||||
|
||||
snapshot() {
|
||||
let { state } = this
|
||||
let { document, selection } = state
|
||||
return new Snapshot({ document, selection })
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the transform and return the new state.
|
||||
*
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
apply() {
|
||||
let transform = this
|
||||
let { state, steps } = transform
|
||||
let { history } = state
|
||||
let { undos, redos } = history
|
||||
|
||||
// Save the current state into the history before transforming.
|
||||
let snapshot = transform.snapshot()
|
||||
undos = undos.unshift(snapshot)
|
||||
redos = redos.clear()
|
||||
history = history.merge({ undos, redos })
|
||||
state = state.merge({ history })
|
||||
|
||||
// Apply each of the steps in the transform, arriving at a new state.
|
||||
state = steps.reduce((state, step) => {
|
||||
const { type, args } = step
|
||||
return state[type](...args)
|
||||
}, state)
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Undo to the previous state in the history.
|
||||
*
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
undo() {
|
||||
let transform = this
|
||||
let { state } = transform
|
||||
let { history } = state
|
||||
let { undos, redos } = history
|
||||
|
||||
// If there's no previous snapshot, return the current state.
|
||||
let previous = undos.peek()
|
||||
if (!previous) return state
|
||||
|
||||
// Remove the previous snapshot from the undo stack.
|
||||
undos = undos.shift()
|
||||
|
||||
// Snapshot the current state, and move it into the redos stack.
|
||||
let snapshot = transform.snapshot()
|
||||
redos = redos.unshift(snapshot)
|
||||
|
||||
// Return the previous state, with the updated history.
|
||||
let { document, selection } = previous
|
||||
history = history.merge({ undos, redos })
|
||||
state = state.merge({ document, selection, history })
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo to the next state in the history.
|
||||
*
|
||||
* @return {State} state
|
||||
*/
|
||||
|
||||
redo() {
|
||||
let transform = this
|
||||
let { state } = transform
|
||||
let { history } = state
|
||||
let { undos, redos } = history
|
||||
|
||||
// If there's no next snapshot, return the current state.
|
||||
let next = redos.peek()
|
||||
if (!next) return state
|
||||
|
||||
// Remove the next history from the redo stack.
|
||||
redos = redos.shift()
|
||||
|
||||
// Snapshot the current state, and move it into the undos stack.
|
||||
let snapshot = transform.snapshot()
|
||||
undos = undos.unshift(snapshot)
|
||||
|
||||
// Return the next state, with the updated history.
|
||||
let { document, selection } = next
|
||||
history = history.merge({ undos, redos })
|
||||
state = state.merge({ document, selection, history })
|
||||
return state
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a step-creating method for each transform type.
|
||||
*/
|
||||
|
||||
TRANSFORM_TYPES.forEach((type) => {
|
||||
Transform.prototype[type] = function (...args) {
|
||||
let transform = this
|
||||
let { steps } = transform
|
||||
steps = steps.push(new Step({ type, args }))
|
||||
transform = transform.merge({ steps })
|
||||
return transform
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*/
|
||||
|
||||
export default Transform
|
@ -24,7 +24,10 @@ export default {
|
||||
switch (key) {
|
||||
case 'enter': {
|
||||
e.preventDefault()
|
||||
return state.split()
|
||||
return state
|
||||
.transform()
|
||||
.split()
|
||||
.apply()
|
||||
}
|
||||
|
||||
case 'backspace': {
|
||||
@ -32,8 +35,14 @@ export default {
|
||||
if (IS_WINDOWS && e.shiftKey) return
|
||||
e.preventDefault()
|
||||
return isWord(e)
|
||||
? state.save().backspaceWord()
|
||||
: state.save().deleteBackward()
|
||||
? state
|
||||
.transform()
|
||||
.backspaceWord()
|
||||
.apply()
|
||||
: state
|
||||
.transform()
|
||||
.deleteBackward()
|
||||
.apply()
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
@ -41,26 +50,34 @@ export default {
|
||||
if (IS_WINDOWS && e.shiftKey) return
|
||||
e.preventDefault()
|
||||
return isWord(e)
|
||||
? state.save().deleteWord()
|
||||
: state.save().deleteForward()
|
||||
? state
|
||||
.transform()
|
||||
.deleteWord()
|
||||
.apply()
|
||||
: state
|
||||
.transform()
|
||||
.deleteForward()
|
||||
.apply()
|
||||
}
|
||||
|
||||
case 'y': {
|
||||
if (!isCtrl(e) || !IS_WINDOWS) return
|
||||
e.preventDefault()
|
||||
return state.redo()
|
||||
return state
|
||||
.transform()
|
||||
.redo()
|
||||
}
|
||||
|
||||
case 'z': {
|
||||
if (!isCommand(e)) return
|
||||
e.preventDefault()
|
||||
return IS_MAC && e.shiftKey
|
||||
? state.redo()
|
||||
: state.undo()
|
||||
}
|
||||
|
||||
default: {
|
||||
console.log('Unhandled key down.')
|
||||
? state
|
||||
.transform()
|
||||
.redo()
|
||||
: state
|
||||
.transform()
|
||||
.undo()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -2,6 +2,7 @@
|
||||
import Character from '../models/character'
|
||||
import Node from '../models/node'
|
||||
import Text from '../models/text'
|
||||
import Document from '../models/document'
|
||||
import State from '../models/state'
|
||||
|
||||
/**
|
||||
@ -12,7 +13,7 @@ import State from '../models/state'
|
||||
*/
|
||||
|
||||
function serialize(state) {
|
||||
return state.nodes
|
||||
return state.document.nodes
|
||||
.map(node => node.text)
|
||||
.join('\n')
|
||||
}
|
||||
@ -39,7 +40,8 @@ function deserialize(string) {
|
||||
})
|
||||
|
||||
const nodes = Node.createMap([node])
|
||||
const state = State.create({ nodes })
|
||||
const document = Document.create({ nodes })
|
||||
const state = State.create({ document })
|
||||
return state
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
|
||||
import Character from '../models/character'
|
||||
import Document from '../models/document'
|
||||
import Mark from '../models/mark'
|
||||
import Node from '../models/node'
|
||||
import Text from '../models/text'
|
||||
@ -16,7 +17,7 @@ import { Map } from 'immutable'
|
||||
|
||||
function serialize(state) {
|
||||
return {
|
||||
nodes: state.nodes.toArray().map(node => serializeNode(node))
|
||||
nodes: serializeNode(state.document)
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +30,11 @@ function serialize(state) {
|
||||
|
||||
function serializeNode(node) {
|
||||
switch (node.type) {
|
||||
case 'document': {
|
||||
return {
|
||||
nodes: node.nodes.toArray().map(node => serializeNode(node))
|
||||
}
|
||||
}
|
||||
case 'text': {
|
||||
return {
|
||||
type: 'text',
|
||||
@ -99,7 +105,9 @@ function serializeMark(mark) {
|
||||
|
||||
function deserialize(object) {
|
||||
return State.create({
|
||||
nodes: Node.createMap(object.nodes.map(deserializeNode))
|
||||
document: Document.create({
|
||||
nodes: Node.createMap(object.nodes.map(deserializeNode))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user