mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-17 20:51:20 +02:00
fixing lots of marks logic
This commit is contained in:
@@ -21,7 +21,7 @@ p {
|
|||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
margin: 0 -10px;
|
margin: 0 -10px;
|
||||||
padding: 1px 0 9px 7px;
|
padding: 1px 0 9px 8px;
|
||||||
border-bottom: 2px solid #eee;
|
border-bottom: 2px solid #eee;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
|
|
||||||
import Editor from '../..'
|
import Editor, { Mark, Raw } from '../..'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import { Raw } from '../..'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State.
|
* State.
|
||||||
@@ -72,13 +71,7 @@ class App extends React.Component {
|
|||||||
isMarkActive(type) {
|
isMarkActive(type) {
|
||||||
const { state } = this.state
|
const { state } = this.state
|
||||||
const { document, selection } = state
|
const { document, selection } = state
|
||||||
const { startKey, startOffset } = selection
|
const marks = document.getMarksAtRange(selection)
|
||||||
const startNode = document.getNode(startKey)
|
|
||||||
if (!startNode) return false
|
|
||||||
|
|
||||||
const { characters } = startNode
|
|
||||||
const character = characters.get(startOffset)
|
|
||||||
const { marks } = character
|
|
||||||
return marks.some(mark => mark.type == type)
|
return marks.some(mark => mark.type == type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +81,14 @@ class App extends React.Component {
|
|||||||
let { state } = this.state
|
let { state } = this.state
|
||||||
const { marks } = state
|
const { marks } = state
|
||||||
const isActive = this.isMarkActive(type)
|
const isActive = this.isMarkActive(type)
|
||||||
|
const mark = Mark.create({ type })
|
||||||
|
|
||||||
state = state
|
state = state
|
||||||
.transform()
|
.transform()
|
||||||
[isActive ? 'unmark' : 'mark']()
|
[isActive ? 'unmark' : 'mark'](mark)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
this.onChange(state)
|
this.setState({ state })
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@@ -13,6 +13,7 @@ export default Editor
|
|||||||
export { default as Character } from './models/character'
|
export { default as Character } from './models/character'
|
||||||
export { default as Element } from './models/element'
|
export { default as Element } from './models/element'
|
||||||
export { default as Document } from './models/document'
|
export { default as Document } from './models/document'
|
||||||
|
export { default as Mark } from './models/mark'
|
||||||
export { default as Selection } from './models/selection'
|
export { default as Selection } from './models/selection'
|
||||||
export { default as State } from './models/state'
|
export { default as State } from './models/state'
|
||||||
export { default as Text } from './models/text'
|
export { default as Text } from './models/text'
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
import { List, Record } from 'immutable'
|
import { List, Record, Set } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record.
|
* Record.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CharacterRecord = new Record({
|
const CharacterRecord = new Record({
|
||||||
marks: new List(),
|
marks: new Set(),
|
||||||
text: ''
|
text: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { List, Map, Record } from 'immutable'
|
import { Map, Record, Set } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record.
|
* Record.
|
||||||
@@ -29,14 +29,14 @@ class Mark extends MarkRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a marks list from an array of marks.
|
* Create a marks set from an array of marks.
|
||||||
*
|
*
|
||||||
* @param {Array} array
|
* @param {Array} array
|
||||||
* @return {List} marks
|
* @return {Set} marks
|
||||||
*/
|
*/
|
||||||
|
|
||||||
static createList(array = []) {
|
static createSet(array = []) {
|
||||||
return new List(array)
|
return new Set(array)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import Character from './character'
|
|||||||
import Element from './element'
|
import Element from './element'
|
||||||
import Selection from './selection'
|
import Selection from './selection'
|
||||||
import Text from './text'
|
import Text from './text'
|
||||||
import { OrderedMap } from 'immutable'
|
import { List, OrderedMap, Set } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Node.
|
* Node.
|
||||||
@@ -14,6 +14,16 @@ import { OrderedMap } from 'immutable'
|
|||||||
|
|
||||||
const Node = {
|
const Node = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that the node has a child by `key`.
|
||||||
|
*
|
||||||
|
* @param {String or Node} key
|
||||||
|
*/
|
||||||
|
|
||||||
|
assertHasNode(key) {
|
||||||
|
if (!this.hasNode(key)) throw new Error('Could not find that child node.')
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete everything in a `range`.
|
* Delete everything in a `range`.
|
||||||
*
|
*
|
||||||
@@ -29,8 +39,8 @@ const Node = {
|
|||||||
|
|
||||||
// Make sure the children exist.
|
// Make sure the children exist.
|
||||||
const { startKey, startOffset, endKey, endOffset } = range
|
const { startKey, startOffset, endKey, endOffset } = range
|
||||||
if (!node.hasNode(startKey)) throw new Error('Could not find that start node.')
|
node.assertHasNode(startKey)
|
||||||
if (!node.hasNode(endKey)) throw new Error('Could not find that end node.')
|
node.assertHasNode(endKey)
|
||||||
|
|
||||||
let startNode = node.getNode(startKey)
|
let startNode = node.getNode(startKey)
|
||||||
|
|
||||||
@@ -212,6 +222,66 @@ const Node = {
|
|||||||
return deep
|
return deep
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of the characters in a `range`.
|
||||||
|
*
|
||||||
|
* @param {Selection} range
|
||||||
|
* @return {List} characters
|
||||||
|
*/
|
||||||
|
|
||||||
|
getCharactersAtRange(range) {
|
||||||
|
const texts = this.getTextNodesAtRange(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
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a set of the marks in a `range`.
|
||||||
|
*
|
||||||
|
* @param {Selection} range
|
||||||
|
* @return {Set} marks
|
||||||
|
*/
|
||||||
|
|
||||||
|
getMarksAtRange(range) {
|
||||||
|
const { startKey, startOffset, endKey } = range
|
||||||
|
|
||||||
|
// If the selection isn't set, return nothing.
|
||||||
|
if (startKey == null || endKey == null) return new Set()
|
||||||
|
|
||||||
|
// If the range is collapsed, and at the start of the node, check the
|
||||||
|
// previous text node.
|
||||||
|
if (range.isCollapsed && startOffset == 0) {
|
||||||
|
const previous = this.getPreviousTextNode(startKey)
|
||||||
|
if (!previous) return new Set()
|
||||||
|
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.getNode(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
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a child node by `key`.
|
* Get a child node by `key`.
|
||||||
*
|
*
|
||||||
@@ -220,9 +290,41 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getNode(key) {
|
getNode(key) {
|
||||||
|
key = normalizeKey(key)
|
||||||
return this.findNode(node => node.key == key) || null
|
return this.findNode(node => node.key == key) || null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the child text node at an `offset`.
|
||||||
|
*
|
||||||
|
* @param {String} offset
|
||||||
|
* @return {Node or Null}
|
||||||
|
*/
|
||||||
|
|
||||||
|
getNodeOffset(key) {
|
||||||
|
this.assertHasNode(key)
|
||||||
|
const match = this.getNode(key)
|
||||||
|
|
||||||
|
// Get all of the nodes that come before the matching child.
|
||||||
|
const child = this.nodes.find((node) => {
|
||||||
|
if (node == match) return true
|
||||||
|
return node.type == 'text'
|
||||||
|
? false
|
||||||
|
: node.hasNode(match)
|
||||||
|
})
|
||||||
|
|
||||||
|
const befores = this.nodes.takeUntil(node => node.key == child.key)
|
||||||
|
|
||||||
|
// Calculate the offset of the nodes before the matching child.
|
||||||
|
const offset = befores.map(child => child.length)
|
||||||
|
|
||||||
|
// If the child's parent is this node, return the offset of all of the nodes
|
||||||
|
// before it, otherwise recurse.
|
||||||
|
return this.nodes.has(match.key)
|
||||||
|
? offset
|
||||||
|
: offset + child.getNodeOffset(key)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the child node after the one by `key`.
|
* Get the child node after the one by `key`.
|
||||||
*
|
*
|
||||||
@@ -231,9 +333,7 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getNextNode(key) {
|
getNextNode(key) {
|
||||||
if (typeof key != 'string') {
|
key = normalizeKey(key)
|
||||||
key = key.key
|
|
||||||
}
|
|
||||||
|
|
||||||
const shallow = this.nodes
|
const shallow = this.nodes
|
||||||
.skipUntil(node => node.key == key)
|
.skipUntil(node => node.key == key)
|
||||||
@@ -256,9 +356,7 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getPreviousNode(key) {
|
getPreviousNode(key) {
|
||||||
if (typeof key != 'string') {
|
key = normalizeKey(key)
|
||||||
key = key.key
|
|
||||||
}
|
|
||||||
|
|
||||||
const matches = this.nodes.get(key)
|
const matches = this.nodes.get(key)
|
||||||
|
|
||||||
@@ -274,6 +372,30 @@ const Node = {
|
|||||||
.first()
|
.first()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the previous text node by `key`.
|
||||||
|
*
|
||||||
|
* @param {String or Node} key
|
||||||
|
* @return {Node or Null}
|
||||||
|
*/
|
||||||
|
|
||||||
|
getPreviousTextNode(key) {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
|
||||||
|
// Create a new selection starting at the first text node.
|
||||||
|
const first = this.findNode(node => node.type == 'text')
|
||||||
|
const range = Selection.create({
|
||||||
|
anchorKey: first.key,
|
||||||
|
anchorOffset: 0,
|
||||||
|
focusKey: key,
|
||||||
|
focusOffset: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const texts = this.getTextNodesAtRange()
|
||||||
|
const previous = texts.get(text.size - 2)
|
||||||
|
return previous
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the parent of a child node by `key`.
|
* Get the parent of a child node by `key`.
|
||||||
*
|
*
|
||||||
@@ -282,9 +404,7 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getParentNode(key) {
|
getParentNode(key) {
|
||||||
if (typeof key != 'string') {
|
key = normalizeKey(key)
|
||||||
key = key.key
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.nodes.get(key)) return this
|
if (this.nodes.get(key)) return this
|
||||||
let node = null
|
let node = null
|
||||||
@@ -299,13 +419,13 @@ const Node = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the child text node at `offset`.
|
* Get the child text node at an `offset`.
|
||||||
*
|
*
|
||||||
* @param {String} offset
|
* @param {String} offset
|
||||||
* @return {Node or Null}
|
* @return {Node or Null}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getNodeAtOffset(offset) {
|
getTextNodeAtOffset(offset) {
|
||||||
let match = null
|
let match = null
|
||||||
let i
|
let i
|
||||||
|
|
||||||
@@ -318,6 +438,81 @@ const Node = {
|
|||||||
return match
|
return match
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the child text nodes after an `offset`.
|
||||||
|
*
|
||||||
|
* @param {String} offset
|
||||||
|
* @return {OrderedMap} matches
|
||||||
|
*/
|
||||||
|
|
||||||
|
getTextNodesAfterOffset(offset) {
|
||||||
|
let matches = new OrderedMap()
|
||||||
|
let i
|
||||||
|
|
||||||
|
this.nodes.forEach((child) => {
|
||||||
|
if (child.length <= offset + i) return
|
||||||
|
|
||||||
|
matches = child.type == 'text'
|
||||||
|
? matches.set(child.key, child)
|
||||||
|
: matches.concat(child.getTextNodesAfterOffset(offset - i))
|
||||||
|
|
||||||
|
i += child.length
|
||||||
|
})
|
||||||
|
|
||||||
|
return matches
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the child text nodes before an `offset`.
|
||||||
|
*
|
||||||
|
* @param {String} offset
|
||||||
|
* @return {OrderedMap} matches
|
||||||
|
*/
|
||||||
|
|
||||||
|
getTextNodesBeforeOffset(offset) {
|
||||||
|
let matches = new OrderedMap()
|
||||||
|
let i
|
||||||
|
|
||||||
|
this.nodes.forEach((child) => {
|
||||||
|
if (child.length > offset + i) return
|
||||||
|
|
||||||
|
matches = child.type == 'text'
|
||||||
|
? matches.set(child.key, child)
|
||||||
|
: matches.concat(child.getTextNodesBeforeOffset(offset - i))
|
||||||
|
|
||||||
|
i += child.length
|
||||||
|
})
|
||||||
|
|
||||||
|
return matches
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all of the text nodes in a `range`.
|
||||||
|
*
|
||||||
|
* @param {Selection} range
|
||||||
|
* @return {OrderedMap} nodes
|
||||||
|
*/
|
||||||
|
|
||||||
|
getTextNodesAtRange(range) {
|
||||||
|
const { startKey, endKey } = range
|
||||||
|
if (startKey == null || endKey == null) return new OrderedMap()
|
||||||
|
|
||||||
|
this.assertHasNode(startKey)
|
||||||
|
this.assertHasNode(endKey)
|
||||||
|
|
||||||
|
// Convert the start and end nodes to offsets.
|
||||||
|
const startNode = this.getNode(startKey)
|
||||||
|
const endNode = this.getNode(endKey)
|
||||||
|
const startOffset = this.getNodeOffset(startNode)
|
||||||
|
const endOffset = this.getNodeOffset(endNode)
|
||||||
|
|
||||||
|
// Return the text nodes after the start offset and before the end offset.
|
||||||
|
const afterStart = this.getTextNodesAfterOffset(startOffset)
|
||||||
|
const beforeEnd = this.getTextNodesBeforeOffset(endOffset)
|
||||||
|
const between = afterStart.filter(node => beforeEnd.includes(node))
|
||||||
|
return between
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively check if a child node exists by `key`.
|
* Recursively check if a child node exists by `key`.
|
||||||
*
|
*
|
||||||
@@ -326,9 +521,7 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
hasNode(key) {
|
hasNode(key) {
|
||||||
if (typeof key != 'string') {
|
key = normalizeKey(key)
|
||||||
key = key.key
|
|
||||||
}
|
|
||||||
|
|
||||||
const shallow = this.nodes.has(key)
|
const shallow = this.nodes.has(key)
|
||||||
if (shallow) return true
|
if (shallow) return true
|
||||||
@@ -346,7 +539,7 @@ const Node = {
|
|||||||
*
|
*
|
||||||
* @param {Selection} range
|
* @param {Selection} range
|
||||||
* @param {String} text
|
* @param {String} text
|
||||||
* @return {Document} node
|
* @return {Node} node
|
||||||
*/
|
*/
|
||||||
|
|
||||||
insertTextAtRange(range, text) {
|
insertTextAtRange(range, text) {
|
||||||
@@ -362,9 +555,11 @@ const Node = {
|
|||||||
let startNode = node.getNode(startKey)
|
let startNode = node.getNode(startKey)
|
||||||
let { characters } = startNode
|
let { characters } = startNode
|
||||||
|
|
||||||
// Create a list of the new characters, with the right marks.
|
// Create a list of the new characters, with the marks from the previous
|
||||||
const marks = characters.has(startOffset)
|
// character if one exists.
|
||||||
? characters.get(startOffset).marks
|
const prevOffset = startOffset - 1
|
||||||
|
const marks = characters.has(prevOffset)
|
||||||
|
? characters.get(prevOffset).marks
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const newCharacters = text.split('').reduce((list, char) => {
|
const newCharacters = text.split('').reduce((list, char) => {
|
||||||
@@ -387,6 +582,44 @@ const Node = {
|
|||||||
return node.normalize()
|
return node.normalize()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new `mark` to the characters at `range`.
|
||||||
|
*
|
||||||
|
* @param {Selection} range
|
||||||
|
* @param {Mark} mark
|
||||||
|
* @return {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
markAtRange(range, mark) {
|
||||||
|
let node = this
|
||||||
|
|
||||||
|
// When the range is collapsed, do nothing.
|
||||||
|
if (range.isCollapsed) return node
|
||||||
|
|
||||||
|
// Otherwise, find each of the text nodes within the range.
|
||||||
|
const { startKey, startOffset, endKey, endOffset } = range
|
||||||
|
let texts = node.getTextNodesAtRange(range)
|
||||||
|
|
||||||
|
// Apply the mark to each of the text nodes's matching characters.
|
||||||
|
texts = texts.map((text) => {
|
||||||
|
let characters = text.characters.map((char, i) => {
|
||||||
|
if (!isInRange(i, text, range)) return char
|
||||||
|
let { marks } = char
|
||||||
|
marks = marks.add(mark)
|
||||||
|
return char.merge({ marks })
|
||||||
|
})
|
||||||
|
|
||||||
|
return text.merge({ characters })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update each of the text nodes.
|
||||||
|
texts.forEach((text) => {
|
||||||
|
node = node.updateNode(text)
|
||||||
|
})
|
||||||
|
|
||||||
|
return node
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize the node, joining any two adjacent text child nodes.
|
* Normalize the node, joining any two adjacent text child nodes.
|
||||||
*
|
*
|
||||||
@@ -430,14 +663,14 @@ const Node = {
|
|||||||
* Push a new `node` onto the map of nodes.
|
* Push a new `node` onto the map of nodes.
|
||||||
*
|
*
|
||||||
* @param {String or Node} key
|
* @param {String or Node} key
|
||||||
* @param {Node} node
|
* @param {Node} node (optional)
|
||||||
* @return {Node} node
|
* @return {Node} node
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pushNode(key, node) {
|
pushNode(key, node) {
|
||||||
if (typeof key != 'string') {
|
if (arguments.length == 1) {
|
||||||
node = key
|
node = key
|
||||||
key = node.key
|
key = normalizeKey(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
let nodes = this.nodes.set(key, node)
|
let nodes = this.nodes.set(key, node)
|
||||||
@@ -452,9 +685,7 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
removeNode(key) {
|
removeNode(key) {
|
||||||
if (typeof key != 'string') {
|
key = normalizeKey(key)
|
||||||
key = key.key
|
|
||||||
}
|
|
||||||
|
|
||||||
let nodes = this.nodes.remove(key)
|
let nodes = this.nodes.remove(key)
|
||||||
return this.merge({ nodes })
|
return this.merge({ nodes })
|
||||||
@@ -520,18 +751,55 @@ const Node = {
|
|||||||
return node.normalize()
|
return node.normalize()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an existing `mark` to the characters at `range`.
|
||||||
|
*
|
||||||
|
* @param {Selection} range
|
||||||
|
* @param {Mark} mark
|
||||||
|
* @return {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
unmarkAtRange(range, mark) {
|
||||||
|
let node = this
|
||||||
|
|
||||||
|
// When the range is collapsed, do nothing.
|
||||||
|
if (range.isCollapsed) return node
|
||||||
|
|
||||||
|
// Otherwise, find each of the text nodes within the range.
|
||||||
|
let texts = node.getTextNodesAtRange(range)
|
||||||
|
|
||||||
|
// Apply the mark to each of the text nodes's matching characters.
|
||||||
|
texts = texts.map((text) => {
|
||||||
|
let characters = text.characters.map((char, i) => {
|
||||||
|
if (!isInRange(i, text, range)) return char
|
||||||
|
let { marks } = char
|
||||||
|
marks = marks.remove(mark)
|
||||||
|
return char.merge({ marks })
|
||||||
|
})
|
||||||
|
|
||||||
|
return text.merge({ characters })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update each of the text nodes.
|
||||||
|
texts.forEach((text) => {
|
||||||
|
node = node.updateNode(text)
|
||||||
|
})
|
||||||
|
|
||||||
|
return node
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new value for a child node by `key`.
|
* Set a new value for a child node by `key`.
|
||||||
*
|
*
|
||||||
* @param {String or Node} key
|
* @param {String or Node} key
|
||||||
* @param {Node} node
|
* @param {Node} node (optional)
|
||||||
* @return {Node} node
|
* @return {Node} node
|
||||||
*/
|
*/
|
||||||
|
|
||||||
updateNode(key, node) {
|
updateNode(key, node) {
|
||||||
if (typeof key != 'string') {
|
if (arguments.length == 1) {
|
||||||
node = key
|
node = key
|
||||||
key = node.key
|
key = normalizeKey(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.nodes.get(key)) {
|
if (this.nodes.get(key)) {
|
||||||
@@ -548,6 +816,42 @@ const Node = {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a `key`, from a key string or a node.
|
||||||
|
*
|
||||||
|
* @param {String or Node} key
|
||||||
|
* @return {String} key
|
||||||
|
*/
|
||||||
|
|
||||||
|
function normalizeKey(key) {
|
||||||
|
if (typeof key == 'string') return key
|
||||||
|
return key.key
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an `index` of a `text` node is in a `range`.
|
||||||
|
*
|
||||||
|
* @param {Number} index
|
||||||
|
* @param {Text} text
|
||||||
|
* @param {Selection} range
|
||||||
|
* @return {Set} characters
|
||||||
|
*/
|
||||||
|
|
||||||
|
function isInRange(index, text, range) {
|
||||||
|
const { startKey, startOffset, endKey, endOffset } = range
|
||||||
|
let matcher
|
||||||
|
|
||||||
|
if (text.key == startKey && text.key == endKey) {
|
||||||
|
return startOffset <= index && index < endOffset
|
||||||
|
} else if (text.key == startKey) {
|
||||||
|
return startOffset <= index
|
||||||
|
} else if (text.key == endKey) {
|
||||||
|
return index < endOffset
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export.
|
* Export.
|
||||||
*/
|
*/
|
||||||
|
@@ -17,7 +17,7 @@ const History = new Record({
|
|||||||
* Default properties.
|
* Default properties.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DEFAULT_PROPERTIES = {
|
const DEFAULTS = {
|
||||||
document: new Document(),
|
document: new Document(),
|
||||||
selection: new Selection(),
|
selection: new Selection(),
|
||||||
history: new History(),
|
history: new History(),
|
||||||
@@ -25,10 +25,10 @@ const DEFAULT_PROPERTIES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Document-like methods, that should be mixed into the `State` prototype.
|
* Node-like methods that should be mixed into the `State` prototype.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DOCUMENT_LIKE_METHODS = [
|
const NODE_LIKE_METHODS = [
|
||||||
'deleteAtRange',
|
'deleteAtRange',
|
||||||
'deleteBackwardAtRange',
|
'deleteBackwardAtRange',
|
||||||
'deleteForwardAtRange',
|
'deleteForwardAtRange',
|
||||||
@@ -40,12 +40,12 @@ const DOCUMENT_LIKE_METHODS = [
|
|||||||
* State.
|
* State.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class State extends Record(DEFAULT_PROPERTIES) {
|
class State extends Record(DEFAULTS) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new `State` with `properties`.
|
* Create a new `State` with `properties`.
|
||||||
*
|
*
|
||||||
* @param {Objetc} properties
|
* @param {Object} properties
|
||||||
* @return {State} state
|
* @return {State} state
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -53,6 +53,39 @@ class State extends Record(DEFAULT_PROPERTIES) {
|
|||||||
return new State(properties)
|
return new State(properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the characters in the current selection.
|
||||||
|
*
|
||||||
|
* @return {List} characters
|
||||||
|
*/
|
||||||
|
|
||||||
|
get characters() {
|
||||||
|
const { document, selection } = this
|
||||||
|
return document.getCharactersAtRange(selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the marks of the current selection.
|
||||||
|
*
|
||||||
|
* @return {Set} marks
|
||||||
|
*/
|
||||||
|
|
||||||
|
get marks() {
|
||||||
|
const { document, selection } = this
|
||||||
|
return document.getMarksAtRange(selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the text nodes in the current selection.
|
||||||
|
*
|
||||||
|
* @return {OrderedMap} nodes
|
||||||
|
*/
|
||||||
|
|
||||||
|
get textNodes() {
|
||||||
|
const { document, selection } = this
|
||||||
|
return document.getTextNodesAtRange(selection)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a new `Transform` with the current state as a starting point.
|
* Return a new `Transform` with the current state as a starting point.
|
||||||
*
|
*
|
||||||
@@ -60,11 +93,12 @@ class State extends Record(DEFAULT_PROPERTIES) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
transform() {
|
transform() {
|
||||||
return new Transform({ state: this })
|
const state = this
|
||||||
|
return new Transform({ state })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a single character.
|
* Delete at the current selection.
|
||||||
*
|
*
|
||||||
* @return {State} state
|
* @return {State} state
|
||||||
*/
|
*/
|
||||||
@@ -145,7 +179,7 @@ class State extends Record(DEFAULT_PROPERTIES) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a `text` string at the current cursor position.
|
* Insert a `text` string at the current selection.
|
||||||
*
|
*
|
||||||
* @param {String} text
|
* @param {String} text
|
||||||
* @return {State} state
|
* @return {State} state
|
||||||
@@ -163,7 +197,22 @@ class State extends Record(DEFAULT_PROPERTIES) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split at a the current cursor position.
|
* Add a `mark` to the characters in the current selection.
|
||||||
|
*
|
||||||
|
* @param {Mark} mark
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
mark(mark) {
|
||||||
|
let state = this
|
||||||
|
let { document, selection } = state
|
||||||
|
document = document.markAtRange(selection, mark)
|
||||||
|
state = state.merge({ document })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split the node at the current selection.
|
||||||
*
|
*
|
||||||
* @return {State} state
|
* @return {State} state
|
||||||
*/
|
*/
|
||||||
@@ -188,13 +237,28 @@ class State extends Record(DEFAULT_PROPERTIES) {
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a `mark` to the characters in the current selection.
|
||||||
|
*
|
||||||
|
* @param {Mark} mark
|
||||||
|
* @return {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
unmark(mark) {
|
||||||
|
let state = this
|
||||||
|
let { document, selection } = state
|
||||||
|
document = document.unmarkAtRange(selection, mark)
|
||||||
|
state = state.merge({ document })
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mix in node-like methods.
|
* Mix in node-like methods.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
DOCUMENT_LIKE_METHODS.forEach((method) => {
|
NODE_LIKE_METHODS.forEach((method) => {
|
||||||
State.prototype[method] = function (...args) {
|
State.prototype[method] = function (...args) {
|
||||||
let { document } = this
|
let { document } = this
|
||||||
document = document[method](...args)
|
document = document[method](...args)
|
||||||
|
@@ -44,8 +44,12 @@ const TRANSFORM_TYPES = [
|
|||||||
'deleteForwardAtRange',
|
'deleteForwardAtRange',
|
||||||
'insertText',
|
'insertText',
|
||||||
'insertTextAtRange',
|
'insertTextAtRange',
|
||||||
|
'mark',
|
||||||
|
'markAtRange',
|
||||||
'split',
|
'split',
|
||||||
'splitAtRange'
|
'splitAtRange',
|
||||||
|
'unmark',
|
||||||
|
'unmarkAtRange'
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -137,7 +137,7 @@ function deserializeRanges(array) {
|
|||||||
.map(char => {
|
.map(char => {
|
||||||
return Character.create({
|
return Character.create({
|
||||||
text: char,
|
text: char,
|
||||||
marks: Mark.createList(marks.map(deserializeMark))
|
marks: Mark.createSet(marks.map(deserializeMark))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user