1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-16 20:24:01 +02:00

fixing lots of marks logic

This commit is contained in:
Ian Storm Taylor
2016-06-20 12:57:31 -07:00
parent c0802c57e0
commit 6bcac30e64
9 changed files with 428 additions and 61 deletions

View File

@@ -21,7 +21,7 @@ p {
.menu {
margin: 0 -10px;
padding: 1px 0 9px 7px;
padding: 1px 0 9px 8px;
border-bottom: 2px solid #eee;
margin-bottom: 10px;
}

View File

@@ -1,8 +1,7 @@
import Editor from '../..'
import Editor, { Mark, Raw } from '../..'
import React from 'react'
import ReactDOM from 'react-dom'
import { Raw } from '../..'
/**
* State.
@@ -72,13 +71,7 @@ class App extends React.Component {
isMarkActive(type) {
const { state } = this.state
const { document, selection } = state
const { startKey, startOffset } = selection
const startNode = document.getNode(startKey)
if (!startNode) return false
const { characters } = startNode
const character = characters.get(startOffset)
const { marks } = character
const marks = document.getMarksAtRange(selection)
return marks.some(mark => mark.type == type)
}
@@ -88,13 +81,14 @@ class App extends React.Component {
let { state } = this.state
const { marks } = state
const isActive = this.isMarkActive(type)
const mark = Mark.create({ type })
state = state
.transform()
[isActive ? 'unmark' : 'mark']()
[isActive ? 'unmark' : 'mark'](mark)
.apply()
this.onChange(state)
this.setState({ state })
}
render() {

View File

@@ -13,6 +13,7 @@ export default Editor
export { default as Character } from './models/character'
export { default as Element } from './models/element'
export { default as Document } from './models/document'
export { default as Mark } from './models/mark'
export { default as Selection } from './models/selection'
export { default as State } from './models/state'
export { default as Text } from './models/text'

View File

@@ -1,12 +1,12 @@
import { List, Record } from 'immutable'
import { List, Record, Set } from 'immutable'
/**
* Record.
*/
const CharacterRecord = new Record({
marks: new List(),
marks: new Set(),
text: ''
})

View File

@@ -1,5 +1,5 @@
import { List, Map, Record } from 'immutable'
import { Map, Record, Set } from 'immutable'
/**
* 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
* @return {List} marks
* @return {Set} marks
*/
static createList(array = []) {
return new List(array)
static createSet(array = []) {
return new Set(array)
}
}

View File

@@ -3,7 +3,7 @@ import Character from './character'
import Element from './element'
import Selection from './selection'
import Text from './text'
import { OrderedMap } from 'immutable'
import { List, OrderedMap, Set } from 'immutable'
/**
* Node.
@@ -14,6 +14,16 @@ import { OrderedMap } from 'immutable'
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`.
*
@@ -29,8 +39,8 @@ const Node = {
// Make sure the children exist.
const { startKey, startOffset, endKey, endOffset } = range
if (!node.hasNode(startKey)) throw new Error('Could not find that start node.')
if (!node.hasNode(endKey)) throw new Error('Could not find that end node.')
node.assertHasNode(startKey)
node.assertHasNode(endKey)
let startNode = node.getNode(startKey)
@@ -212,6 +222,66 @@ const Node = {
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`.
*
@@ -220,9 +290,41 @@ const Node = {
*/
getNode(key) {
key = normalizeKey(key)
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`.
*
@@ -231,9 +333,7 @@ const Node = {
*/
getNextNode(key) {
if (typeof key != 'string') {
key = key.key
}
key = normalizeKey(key)
const shallow = this.nodes
.skipUntil(node => node.key == key)
@@ -256,9 +356,7 @@ const Node = {
*/
getPreviousNode(key) {
if (typeof key != 'string') {
key = key.key
}
key = normalizeKey(key)
const matches = this.nodes.get(key)
@@ -274,6 +372,30 @@ const Node = {
.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`.
*
@@ -282,9 +404,7 @@ const Node = {
*/
getParentNode(key) {
if (typeof key != 'string') {
key = key.key
}
key = normalizeKey(key)
if (this.nodes.get(key)) return this
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
* @return {Node or Null}
*/
getNodeAtOffset(offset) {
getTextNodeAtOffset(offset) {
let match = null
let i
@@ -318,6 +438,81 @@ const Node = {
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`.
*
@@ -326,9 +521,7 @@ const Node = {
*/
hasNode(key) {
if (typeof key != 'string') {
key = key.key
}
key = normalizeKey(key)
const shallow = this.nodes.has(key)
if (shallow) return true
@@ -346,7 +539,7 @@ const Node = {
*
* @param {Selection} range
* @param {String} text
* @return {Document} node
* @return {Node} node
*/
insertTextAtRange(range, text) {
@@ -362,9 +555,11 @@ const Node = {
let startNode = node.getNode(startKey)
let { characters } = startNode
// Create a list of the new characters, with the right marks.
const marks = characters.has(startOffset)
? characters.get(startOffset).marks
// Create a list of the new characters, with the marks from the previous
// character if one exists.
const prevOffset = startOffset - 1
const marks = characters.has(prevOffset)
? characters.get(prevOffset).marks
: null
const newCharacters = text.split('').reduce((list, char) => {
@@ -387,6 +582,44 @@ const Node = {
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.
*
@@ -430,14 +663,14 @@ const Node = {
* Push a new `node` onto the map of nodes.
*
* @param {String or Node} key
* @param {Node} node
* @param {Node} node (optional)
* @return {Node} node
*/
pushNode(key, node) {
if (typeof key != 'string') {
if (arguments.length == 1) {
node = key
key = node.key
key = normalizeKey(key)
}
let nodes = this.nodes.set(key, node)
@@ -452,9 +685,7 @@ const Node = {
*/
removeNode(key) {
if (typeof key != 'string') {
key = key.key
}
key = normalizeKey(key)
let nodes = this.nodes.remove(key)
return this.merge({ nodes })
@@ -520,18 +751,55 @@ const Node = {
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`.
*
* @param {String or Node} key
* @param {Node} node
* @param {Node} node (optional)
* @return {Node} node
*/
updateNode(key, node) {
if (typeof key != 'string') {
if (arguments.length == 1) {
node = key
key = node.key
key = normalizeKey(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.
*/

View File

@@ -17,7 +17,7 @@ const History = new Record({
* Default properties.
*/
const DEFAULT_PROPERTIES = {
const DEFAULTS = {
document: new Document(),
selection: new Selection(),
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',
'deleteBackwardAtRange',
'deleteForwardAtRange',
@@ -40,12 +40,12 @@ const DOCUMENT_LIKE_METHODS = [
* State.
*/
class State extends Record(DEFAULT_PROPERTIES) {
class State extends Record(DEFAULTS) {
/**
* Create a new `State` with `properties`.
*
* @param {Objetc} properties
* @param {Object} properties
* @return {State} state
*/
@@ -53,6 +53,39 @@ class State extends Record(DEFAULT_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.
*
@@ -60,11 +93,12 @@ class State extends Record(DEFAULT_PROPERTIES) {
*/
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
*/
@@ -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
* @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
*/
@@ -188,13 +237,28 @@ class State extends Record(DEFAULT_PROPERTIES) {
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.
*/
DOCUMENT_LIKE_METHODS.forEach((method) => {
NODE_LIKE_METHODS.forEach((method) => {
State.prototype[method] = function (...args) {
let { document } = this
document = document[method](...args)

View File

@@ -44,8 +44,12 @@ const TRANSFORM_TYPES = [
'deleteForwardAtRange',
'insertText',
'insertTextAtRange',
'mark',
'markAtRange',
'split',
'splitAtRange'
'splitAtRange',
'unmark',
'unmarkAtRange'
]
/**

View File

@@ -137,7 +137,7 @@ function deserializeRanges(array) {
.map(char => {
return Character.create({
text: char,
marks: Mark.createList(marks.map(deserializeMark))
marks: Mark.createSet(marks.map(deserializeMark))
})
})