mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-31 10:51:44 +02:00
distinguish between block and inline nodes
This commit is contained in:
@@ -186,7 +186,8 @@ class App extends React.Component {
|
|||||||
onBackspace(e, state) {
|
onBackspace(e, state) {
|
||||||
if (state.isCurrentlyExpanded) return
|
if (state.isCurrentlyExpanded) return
|
||||||
if (state.currentStartOffset != 0) return
|
if (state.currentStartOffset != 0) return
|
||||||
const node = state.currentWrappingNodes.first()
|
const node = state.currentBlockNodes.first()
|
||||||
|
if (!node) debugger
|
||||||
if (node.type == 'paragraph') return
|
if (node.type == 'paragraph') return
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -207,7 +208,8 @@ class App extends React.Component {
|
|||||||
|
|
||||||
onEnter(e, state) {
|
onEnter(e, state) {
|
||||||
if (state.isCurrentlyExpanded) return
|
if (state.isCurrentlyExpanded) return
|
||||||
const node = state.currentWrappingNodes.first()
|
const node = state.currentBlockNodes.first()
|
||||||
|
if (!node) debugger
|
||||||
if (state.currentStartOffset == 0 && node.length == 0) return this.onBackspace(e, state)
|
if (state.currentStartOffset == 0 && node.length == 0) return this.onBackspace(e, state)
|
||||||
if (state.currentEndOffset != node.length) return
|
if (state.currentEndOffset != node.length) return
|
||||||
|
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
"kind": "block",
|
||||||
"type": "paragraph",
|
"type": "paragraph",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"kind": "text",
|
||||||
"ranges": [
|
"ranges": [
|
||||||
{
|
{
|
||||||
"text": "The editor gives you full control over the logic you can add. For example, it's fairly common to want to add markdown-like shortcuts to editors. So that, when you start a line with \"> \" you get a blockquote that looks like this:"
|
"text": "The editor gives you full control over the logic you can add. For example, it's fairly common to want to add markdown-like shortcuts to editors. So that, when you start a line with \"> \" you get a blockquote that looks like this:"
|
||||||
@@ -14,10 +15,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"kind": "block",
|
||||||
"type": "block-quote",
|
"type": "block-quote",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"kind": "text",
|
||||||
"ranges": [
|
"ranges": [
|
||||||
{
|
{
|
||||||
"text": "A wise quote."
|
"text": "A wise quote."
|
||||||
@@ -27,10 +29,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"kind": "block",
|
||||||
"type": "paragraph",
|
"type": "paragraph",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"kind": "text",
|
||||||
"ranges": [
|
"ranges": [
|
||||||
{
|
{
|
||||||
"text": "Order when you start a line with \"## \" you get a level-two heading, like this:"
|
"text": "Order when you start a line with \"## \" you get a level-two heading, like this:"
|
||||||
@@ -40,10 +43,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"kind": "block",
|
||||||
"type": "heading-two",
|
"type": "heading-two",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"kind": "text",
|
||||||
"ranges": [
|
"ranges": [
|
||||||
{
|
{
|
||||||
"text": "Try it out!"
|
"text": "Try it out!"
|
||||||
@@ -53,10 +57,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"kind": "block",
|
||||||
"type": "paragraph",
|
"type": "paragraph",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"kind": "text",
|
||||||
"ranges": [
|
"ranges": [
|
||||||
{
|
{
|
||||||
"text": "Try it out for yourself! Try starting a new line with \">\", \"-\", or \"#\"s."
|
"text": "Try it out for yourself! Try starting a new line with \">\", \"-\", or \"#\"s."
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import Editor, { Character, Document, Element, State, Text } from '../..'
|
import Editor, { Character, Document, Block, State, Text } from '../..'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import state from './state.json'
|
import state from './state.json'
|
||||||
@@ -19,13 +19,13 @@ function deserialize(string) {
|
|||||||
}, Character.createList())
|
}, Character.createList())
|
||||||
|
|
||||||
const text = Text.create({ characters })
|
const text = Text.create({ characters })
|
||||||
const texts = Element.createMap([text])
|
const texts = Block.createMap([text])
|
||||||
const node = Element.create({
|
const node = Block.create({
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
nodes: texts,
|
nodes: texts,
|
||||||
})
|
})
|
||||||
|
|
||||||
const nodes = Element.createMap([node])
|
const nodes = Block.createMap([node])
|
||||||
const document = Document.create({ nodes })
|
const document = Document.create({ nodes })
|
||||||
const state = State.create({ document })
|
const state = State.create({ document })
|
||||||
return state
|
return state
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
|
"kind": "block",
|
||||||
"type": "paragraph",
|
"type": "paragraph",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"kind": "text",
|
||||||
"ranges": [
|
"ranges": [
|
||||||
{
|
{
|
||||||
"text": "This is editable "
|
"text": "This is editable "
|
||||||
@@ -47,10 +48,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"kind": "block",
|
||||||
"type": "paragraph",
|
"type": "paragraph",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"kind": "text",
|
||||||
"ranges": [
|
"ranges": [
|
||||||
{
|
{
|
||||||
"text": "Since it's rich text, you can do things like turn a selection of text ",
|
"text": "Since it's rich text, you can do things like turn a selection of text ",
|
||||||
@@ -70,10 +72,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"kind": "block",
|
||||||
"type": "block-quote",
|
"type": "block-quote",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"kind": "text",
|
||||||
"ranges": [
|
"ranges": [
|
||||||
{
|
{
|
||||||
"text": "A wise quote."
|
"text": "A wise quote."
|
||||||
@@ -83,10 +86,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"kind": "block",
|
||||||
"type": "paragraph",
|
"type": "paragraph",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"type": "text",
|
"kind": "text",
|
||||||
"ranges": [
|
"ranges": [
|
||||||
{
|
{
|
||||||
"text": "Try it out for yourself!"
|
"text": "Try it out for yourself!"
|
||||||
|
@@ -10,9 +10,10 @@ export default Editor
|
|||||||
* Models.
|
* Models.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export { default as Block } from './models/block'
|
||||||
export { default as Character } from './models/character'
|
export { default as Character } from './models/character'
|
||||||
export { default as Element } from './models/element'
|
|
||||||
export { default as Document } from './models/document'
|
export { default as Document } from './models/document'
|
||||||
|
export { default as Inline } from './models/inline'
|
||||||
export { default as Mark } from './models/mark'
|
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'
|
||||||
|
97
lib/models/block.js
Normal file
97
lib/models/block.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
|
||||||
|
import Node from './node'
|
||||||
|
import uid from 'uid'
|
||||||
|
import { OrderedMap, Record } from 'immutable'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default properties.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
data: new Map(),
|
||||||
|
key: null,
|
||||||
|
nodes: new OrderedMap(),
|
||||||
|
type: null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Block extends Record(DEFAULTS) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new `Block` with `properties`.
|
||||||
|
*
|
||||||
|
* @param {Object} properties
|
||||||
|
* @return {Block} element
|
||||||
|
*/
|
||||||
|
|
||||||
|
static create(properties = {}) {
|
||||||
|
if (!properties.type) throw new Error('You must pass a block `type`.')
|
||||||
|
properties.key = uid(4)
|
||||||
|
let block = new Block(properties)
|
||||||
|
return block.normalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an ordered map of `Blocks` from an array of `Blocks`.
|
||||||
|
*
|
||||||
|
* @param {Array} elements
|
||||||
|
* @return {OrderedMap} map
|
||||||
|
*/
|
||||||
|
|
||||||
|
static createMap(elements = []) {
|
||||||
|
return elements.reduce((map, element) => {
|
||||||
|
return map.set(element.key, element)
|
||||||
|
}, new OrderedMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the node's kind.
|
||||||
|
*
|
||||||
|
* @return {String} kind
|
||||||
|
*/
|
||||||
|
|
||||||
|
get kind() {
|
||||||
|
return 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the length of the concatenated text of the node.
|
||||||
|
*
|
||||||
|
* @return {Number} length
|
||||||
|
*/
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this.text.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the concatenated text `string` of all child nodes.
|
||||||
|
*
|
||||||
|
* @return {String} text
|
||||||
|
*/
|
||||||
|
|
||||||
|
get text() {
|
||||||
|
return this.nodes
|
||||||
|
.map(node => node.text)
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mix in `Node` methods.
|
||||||
|
*/
|
||||||
|
|
||||||
|
for (const method in Node) {
|
||||||
|
Block.prototype[method] = Node[method]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default Block
|
@@ -7,7 +7,8 @@ import { OrderedMap, Record } from 'immutable'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
nodes: new OrderedMap()
|
nodes: new OrderedMap(),
|
||||||
|
parent: null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +25,18 @@ class Document extends Record(DEFAULTS) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
static create(properties = {}) {
|
static create(properties = {}) {
|
||||||
return new Document(properties)
|
let document = new Document(properties)
|
||||||
|
return document.normalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the node's kind.
|
||||||
|
*
|
||||||
|
* @return {String} kind
|
||||||
|
*/
|
||||||
|
|
||||||
|
get kind() {
|
||||||
|
return 'document'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,16 +61,6 @@ class Document extends Record(DEFAULTS) {
|
|||||||
.join('')
|
.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Type.
|
|
||||||
*
|
|
||||||
* @return {String} type
|
|
||||||
*/
|
|
||||||
|
|
||||||
get type() {
|
|
||||||
return 'document'
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -15,26 +15,27 @@ const DEFAULTS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Element.
|
* Inline.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Element extends Record(DEFAULTS) {
|
class Inline extends Record(DEFAULTS) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new `Element` with `properties`.
|
* Create a new `Inline` with `properties`.
|
||||||
*
|
*
|
||||||
* @param {Object} properties
|
* @param {Object} properties
|
||||||
* @return {Element} element
|
* @return {Inline} element
|
||||||
*/
|
*/
|
||||||
|
|
||||||
static create(properties = {}) {
|
static create(properties = {}) {
|
||||||
if (!properties.type) throw new Error('You must pass an element `type`.')
|
if (!properties.type) throw new Error('You must pass an inline `type`.')
|
||||||
properties.key = uid(4)
|
properties.key = uid(4)
|
||||||
return new Element(properties)
|
let inline = new Inline(properties)
|
||||||
|
return inline.normalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an ordered map of `Elements` from an array of `Elements`.
|
* Create an ordered map of `Inlines` from an array of `Inlines`.
|
||||||
*
|
*
|
||||||
* @param {Array} elements
|
* @param {Array} elements
|
||||||
* @return {OrderedMap} map
|
* @return {OrderedMap} map
|
||||||
@@ -46,6 +47,16 @@ class Element extends Record(DEFAULTS) {
|
|||||||
}, new OrderedMap())
|
}, new OrderedMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the node's kind.
|
||||||
|
*
|
||||||
|
* @return {String} kind
|
||||||
|
*/
|
||||||
|
|
||||||
|
get kind() {
|
||||||
|
return 'inline'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the length of the concatenated text of the node.
|
* Get the length of the concatenated text of the node.
|
||||||
*
|
*
|
||||||
@@ -75,7 +86,7 @@ class Element extends Record(DEFAULTS) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
for (const method in Node) {
|
for (const method in Node) {
|
||||||
Element.prototype[method] = Node[method]
|
Inline.prototype[method] = Node[method]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -83,4 +94,4 @@ for (const method in Node) {
|
|||||||
* Export.
|
* Export.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default Element
|
export default Inline
|
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
import Block from './block'
|
||||||
import Character from './character'
|
import Character from './character'
|
||||||
import Element from './element'
|
|
||||||
import Mark from './mark'
|
import Mark from './mark'
|
||||||
import Selection from './selection'
|
import Selection from './selection'
|
||||||
import Text from './text'
|
import Text from './text'
|
||||||
@@ -9,8 +9,8 @@ import { List, OrderedMap, OrderedSet, Set } from 'immutable'
|
|||||||
/**
|
/**
|
||||||
* Node.
|
* Node.
|
||||||
*
|
*
|
||||||
* And interface that `Document` and `Element` both implement, to make working
|
* And interface that `Document`, `Block` and `Inline` all implement, to make
|
||||||
* recursively easier with the tree easier.
|
* working with the recursive node tree easier.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Node = {
|
const Node = {
|
||||||
@@ -202,7 +202,7 @@ const Node = {
|
|||||||
if (shallow != null) return shallow
|
if (shallow != null) return shallow
|
||||||
|
|
||||||
return this.nodes
|
return this.nodes
|
||||||
.map(node => node.type == 'text' ? null : node.findNode(iterator))
|
.map(node => node.kind == 'text' ? null : node.findNode(iterator))
|
||||||
.filter(node => node)
|
.filter(node => node)
|
||||||
.first()
|
.first()
|
||||||
},
|
},
|
||||||
@@ -217,7 +217,7 @@ const Node = {
|
|||||||
filterNodes(iterator) {
|
filterNodes(iterator) {
|
||||||
const shallow = this.nodes.filter(iterator)
|
const shallow = this.nodes.filter(iterator)
|
||||||
const deep = this.nodes
|
const deep = this.nodes
|
||||||
.map(node => node.type == 'text' ? null : node.filterNodes(iterator))
|
.map(node => node.kind == 'text' ? null : node.filterNodes(iterator))
|
||||||
.filter(node => node)
|
.filter(node => node)
|
||||||
.reduce((all, map) => {
|
.reduce((all, map) => {
|
||||||
return all.concat(map)
|
return all.concat(map)
|
||||||
@@ -254,7 +254,7 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getFirstTextNode() {
|
getFirstTextNode() {
|
||||||
return this.findNode(node => node.type == 'text') || null
|
return this.findNode(node => node.kind == 'text') || null
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -264,7 +264,7 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getLastTextNode() {
|
getLastTextNode() {
|
||||||
const texts = this.filterNodes(node => node.type == 'text')
|
const texts = this.filterNodes(node => node.kind == 'text')
|
||||||
return texts.last() || null
|
return texts.last() || null
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ const Node = {
|
|||||||
// Get all of the nodes that come before the matching child.
|
// Get all of the nodes that come before the matching child.
|
||||||
const child = this.nodes.find((node) => {
|
const child = this.nodes.find((node) => {
|
||||||
if (node == match) return true
|
if (node == match) return true
|
||||||
return node.type == 'text'
|
return node.kind == 'text'
|
||||||
? false
|
? false
|
||||||
: node.hasNode(match)
|
: node.hasNode(match)
|
||||||
})
|
})
|
||||||
@@ -372,7 +372,7 @@ const Node = {
|
|||||||
if (shallow != null) return shallow
|
if (shallow != null) return shallow
|
||||||
|
|
||||||
return this.nodes
|
return this.nodes
|
||||||
.map(node => node.type == 'text' ? null : node.getNextNode(key))
|
.map(node => node.kind == 'text' ? null : node.getNextNode(key))
|
||||||
.filter(node => node)
|
.filter(node => node)
|
||||||
.first()
|
.first()
|
||||||
},
|
},
|
||||||
@@ -396,7 +396,7 @@ const Node = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.nodes
|
return this.nodes
|
||||||
.map(node => node.type == 'text' ? null : node.getPreviousNode(key))
|
.map(node => node.kind == 'text' ? null : node.getPreviousNode(key))
|
||||||
.filter(node => node)
|
.filter(node => node)
|
||||||
.first()
|
.first()
|
||||||
},
|
},
|
||||||
@@ -412,7 +412,7 @@ const Node = {
|
|||||||
key = normalizeKey(key)
|
key = normalizeKey(key)
|
||||||
|
|
||||||
// Create a new selection starting at the first text node.
|
// Create a new selection starting at the first text node.
|
||||||
const first = this.findNode(node => node.type == 'text')
|
const first = this.findNode(node => node.kind == 'text')
|
||||||
const range = Selection.create({
|
const range = Selection.create({
|
||||||
anchorKey: first.key,
|
anchorKey: first.key,
|
||||||
anchorOffset: 0,
|
anchorOffset: 0,
|
||||||
@@ -439,7 +439,7 @@ const Node = {
|
|||||||
let node = null
|
let node = null
|
||||||
|
|
||||||
this.nodes.forEach((child) => {
|
this.nodes.forEach((child) => {
|
||||||
if (child.type == 'text') return
|
if (child.kind == 'text') return
|
||||||
const match = child.getParentNode(key)
|
const match = child.getParentNode(key)
|
||||||
if (match) node = match
|
if (match) node = match
|
||||||
})
|
})
|
||||||
@@ -455,13 +455,11 @@ const Node = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
getTextNodeAtOffset(offset) {
|
getTextNodeAtOffset(offset) {
|
||||||
let match = null
|
let length = 0
|
||||||
let i
|
let texts = this.filterNodes(node => node.kind == 'text')
|
||||||
|
let match = texts.find((node) => {
|
||||||
this.nodes.forEach((node) => {
|
length += node.length
|
||||||
if (!node.length > offset + i) return
|
return length >= offset
|
||||||
match = node.type == 'text' ? node : node.getNodeAtOffset(offset - i)
|
|
||||||
i += node.length
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return match
|
return match
|
||||||
@@ -477,14 +475,17 @@ const Node = {
|
|||||||
getTextNodesAtRange(range) {
|
getTextNodesAtRange(range) {
|
||||||
range = range.normalize(this)
|
range = range.normalize(this)
|
||||||
const { startKey, endKey } = range
|
const { startKey, endKey } = range
|
||||||
|
|
||||||
|
// If the selection isn't formed, return an empty map.
|
||||||
if (startKey == null || endKey == null) return new OrderedMap()
|
if (startKey == null || endKey == null) return new OrderedMap()
|
||||||
|
|
||||||
|
// Assert that the nodes exist before searching.
|
||||||
this.assertHasNode(startKey)
|
this.assertHasNode(startKey)
|
||||||
this.assertHasNode(endKey)
|
this.assertHasNode(endKey)
|
||||||
|
|
||||||
// Return the text nodes after the start offset and before the end offset.
|
// Return the text nodes after the start offset and before the end offset.
|
||||||
const endNode = this.getNode(endKey)
|
const endNode = this.getNode(endKey)
|
||||||
const texts = this.filterNodes(node => node.type == 'text')
|
const texts = this.filterNodes(node => node.kind == 'text')
|
||||||
const afterStart = texts.skipUntil(node => node.key == startKey)
|
const afterStart = texts.skipUntil(node => node.key == startKey)
|
||||||
const upToEnd = afterStart.takeUntil(node => node.key == endKey)
|
const upToEnd = afterStart.takeUntil(node => node.key == endKey)
|
||||||
let matches = upToEnd.set(endNode.key, endNode)
|
let matches = upToEnd.set(endNode.key, endNode)
|
||||||
@@ -492,22 +493,51 @@ const Node = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all of the wrapping nodes in a `range`.
|
* Get the closets block nodes for each text node in a `range`.
|
||||||
*
|
*
|
||||||
* @param {Selection} range
|
* @param {Selection} range
|
||||||
* @return {OrderedMap} nodes
|
* @return {OrderedMap} nodes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getWrappingNodesAtRange(range) {
|
getBlockNodesAtRange(range) {
|
||||||
const node = this
|
const node = this
|
||||||
range = range.normalize(node)
|
range = range.normalize(node)
|
||||||
|
|
||||||
const texts = node.getTextNodesAtRange(range)
|
const texts = node.getTextNodesAtRange(range)
|
||||||
const parents = texts.map((text) => {
|
const blocks = texts.map(text => node.getClosestBlockNode(text))
|
||||||
return node.nodes.includes(text) ? node : node.getParentNode(text)
|
return blocks
|
||||||
})
|
},
|
||||||
|
|
||||||
return parents
|
/**
|
||||||
|
* Get the node's closest block parent node.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @return {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
getClosestBlockNode(node) {
|
||||||
|
let parent = this.getParentNode(node)
|
||||||
|
|
||||||
|
while (parent && parent.kind != 'block') {
|
||||||
|
parent = this.getParentNode(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the node's closest inline parent node.
|
||||||
|
*
|
||||||
|
* @return {Node} node
|
||||||
|
*/
|
||||||
|
|
||||||
|
getClosestInlineNode() {
|
||||||
|
let parent = this.getParentNode(node)
|
||||||
|
|
||||||
|
while (parent && parent.kind != 'inline') {
|
||||||
|
parent = this.getParentNode(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -524,7 +554,7 @@ const Node = {
|
|||||||
if (shallow) return true
|
if (shallow) return true
|
||||||
|
|
||||||
const deep = this.nodes
|
const deep = this.nodes
|
||||||
.map(node => node.type == 'text' ? false : node.hasNode(key))
|
.map(node => node.kind == 'text' ? false : node.hasNode(key))
|
||||||
.some(has => has)
|
.some(has => has)
|
||||||
if (deep) return true
|
if (deep) return true
|
||||||
|
|
||||||
@@ -625,29 +655,31 @@ const Node = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize the node, joining any two adjacent text child nodes.
|
* Normalize the node by joining any two adjacent text child nodes.
|
||||||
*
|
*
|
||||||
* @return {Node} node
|
* @return {Node} node
|
||||||
*/
|
*/
|
||||||
|
|
||||||
normalize() {
|
normalize() {
|
||||||
let node = this
|
let node = this
|
||||||
let first = node.findNode((child) => {
|
|
||||||
if (child.type != 'text') return
|
// See if there are any adjacent text nodes.
|
||||||
|
let firstAdjacent = node.findNode((child) => {
|
||||||
|
if (child.kind != 'text') return
|
||||||
const parent = node.getParentNode(child)
|
const parent = node.getParentNode(child)
|
||||||
const next = parent.getNextNode(child)
|
const next = parent.getNextNode(child)
|
||||||
return next && next.type == 'text'
|
return next && next.kind == 'text'
|
||||||
})
|
})
|
||||||
|
|
||||||
// If no text node was followed by another, do nothing.
|
// If no text nodes are adjacent, abort.
|
||||||
if (!first) return node
|
if (!firstAdjacent) return node
|
||||||
|
|
||||||
// Otherwise, add the text of the second node to the first...
|
// Fix an adjacent text node if one exists.
|
||||||
let parent = node.getParentNode(first)
|
let parent = node.getParentNode(firstAdjacent)
|
||||||
const second = parent.getNextNode(first)
|
const second = parent.getNextNode(firstAdjacent)
|
||||||
const characters = first.characters.concat(second.characters)
|
const characters = firstAdjacent.characters.concat(second.characters)
|
||||||
first = first.merge({ characters })
|
firstAdjacent = firstAdjacent.merge({ characters })
|
||||||
parent = parent.updateNode(first)
|
parent = parent.updateNode(firstAdjacent)
|
||||||
|
|
||||||
// Then remove the second node.
|
// Then remove the second node.
|
||||||
parent = parent.removeNode(second)
|
parent = parent.removeNode(second)
|
||||||
@@ -659,7 +691,7 @@ const Node = {
|
|||||||
node = parent
|
node = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, recurse by normalizing again.
|
// Recurse by normalizing again.
|
||||||
return node.normalize()
|
return node.normalize()
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -760,7 +792,7 @@ const Node = {
|
|||||||
|
|
||||||
// Create a brand new second element with the second set of characters.
|
// Create a brand new second element with the second set of characters.
|
||||||
let secondText = Text.create({})
|
let secondText = Text.create({})
|
||||||
let secondElement = Element.create({
|
let secondElement = Block.create({
|
||||||
type: firstElement.type,
|
type: firstElement.type,
|
||||||
data: firstElement.data
|
data: firstElement.data
|
||||||
})
|
})
|
||||||
@@ -852,7 +884,7 @@ const Node = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nodes = this.nodes.map((child) => {
|
const nodes = this.nodes.map((child) => {
|
||||||
return child.type == 'text' ? child : child.updateNode(key, node)
|
return child.kind == 'text' ? child : child.updateNode(key, node)
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.merge({ nodes })
|
return this.merge({ nodes })
|
||||||
@@ -872,7 +904,7 @@ const Node = {
|
|||||||
|
|
||||||
// Allow for the parent to by just a type.
|
// Allow for the parent to by just a type.
|
||||||
if (typeof parent == 'string') {
|
if (typeof parent == 'string') {
|
||||||
parent = Element.create({ type: parent })
|
parent = Block.create({ type: parent })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the child to the parent's nodes.
|
// Add the child to the parent's nodes.
|
||||||
|
@@ -109,7 +109,7 @@ class Selection extends SelectionRecord {
|
|||||||
|
|
||||||
isAtStartOf(node) {
|
isAtStartOf(node) {
|
||||||
const { startKey, startOffset } = this
|
const { startKey, startOffset } = this
|
||||||
const first = node.type == 'text' ? node : node.getFirstTextNode()
|
const first = node.kind == 'text' ? node : node.getFirstTextNode()
|
||||||
return startKey == first.key && startOffset == 0
|
return startKey == first.key && startOffset == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ class Selection extends SelectionRecord {
|
|||||||
|
|
||||||
isAtEndOf(node) {
|
isAtEndOf(node) {
|
||||||
const { endKey, endOffset } = this
|
const { endKey, endOffset } = this
|
||||||
const last = node.type == 'text' ? node : node.getLastTextNode()
|
const last = node.kind == 'text' ? node : node.getLastTextNode()
|
||||||
return endKey == last.key && endOffset == last.length
|
return endKey == last.key && endOffset == last.length
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,8 +148,8 @@ class Selection extends SelectionRecord {
|
|||||||
let focusNode = node.getNode(focusKey)
|
let focusNode = node.getNode(focusKey)
|
||||||
|
|
||||||
// If the anchor node isn't a text node, match it to one.
|
// If the anchor node isn't a text node, match it to one.
|
||||||
if (anchorNode.type != 'text') {
|
if (anchorNode.kind != 'text') {
|
||||||
anchorNode = node.getNodeAtOffset(anchorOffset)
|
anchorNode = node.getTextNodeAtOffset(anchorOffset)
|
||||||
let parent = node.getParentNode(anchorNode)
|
let parent = node.getParentNode(anchorNode)
|
||||||
let offset = parent.getNodeOffset(anchorNode)
|
let offset = parent.getNodeOffset(anchorNode)
|
||||||
anchorOffset = anchorOffset - offset
|
anchorOffset = anchorOffset - offset
|
||||||
@@ -157,8 +157,8 @@ class Selection extends SelectionRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the focus node isn't a text node, match it to one.
|
// If the focus node isn't a text node, match it to one.
|
||||||
if (focusNode.type != 'text') {
|
if (focusNode.kind != 'text') {
|
||||||
focusNode = node.getNodeAtOffset(focusOffset)
|
focusNode = node.getTextNodeAtOffset(focusOffset)
|
||||||
let parent = node.getParentNode(focusNode)
|
let parent = node.getParentNode(focusNode)
|
||||||
let offset = parent.getNodeOffset(focusNode)
|
let offset = parent.getNodeOffset(focusNode)
|
||||||
focusOffset = focusOffset - offset
|
focusOffset = focusOffset - offset
|
||||||
|
@@ -134,13 +134,13 @@ class State extends Record(DEFAULTS) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the wrapping nodes in the current selection.
|
* Get the block nodes in the current selection.
|
||||||
*
|
*
|
||||||
* @return {OrderedMap} nodes
|
* @return {OrderedMap} nodes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
get currentWrappingNodes() {
|
get currentBlockNodes() {
|
||||||
return this.document.getWrappingNodesAtRange(this.selection)
|
return this.document.getBlockNodesAtRange(this.selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -3,19 +3,20 @@ import uid from 'uid'
|
|||||||
import { List, Record } from 'immutable'
|
import { List, Record } from 'immutable'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record.
|
* Default properties.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TextRecord = new Record({
|
const DEFAULTS = {
|
||||||
characters: new List(),
|
characters: new List(),
|
||||||
key: null
|
key: null,
|
||||||
})
|
parent: null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text.
|
* Text.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class Text extends TextRecord {
|
class Text extends Record(DEFAULTS) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new `Text` with `properties`.
|
* Create a new `Text` with `properties`.
|
||||||
@@ -29,6 +30,16 @@ class Text extends TextRecord {
|
|||||||
return new Text(properties)
|
return new Text(properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the node's kind.
|
||||||
|
*
|
||||||
|
* @return {String} kind
|
||||||
|
*/
|
||||||
|
|
||||||
|
get kind() {
|
||||||
|
return 'text'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the length of the concatenated text of the node.
|
* Get the length of the concatenated text of the node.
|
||||||
*
|
*
|
||||||
@@ -51,16 +62,6 @@ class Text extends TextRecord {
|
|||||||
.join('')
|
.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Immutable type to match other nodes.
|
|
||||||
*
|
|
||||||
* @return {String} type
|
|
||||||
*/
|
|
||||||
|
|
||||||
get type() {
|
|
||||||
return 'text'
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
|
|
||||||
|
import Block from '../models/block'
|
||||||
import Character from '../models/character'
|
import Character from '../models/character'
|
||||||
import Document from '../models/document'
|
import Document from '../models/document'
|
||||||
|
import Inline from '../models/inline'
|
||||||
import Mark from '../models/mark'
|
import Mark from '../models/mark'
|
||||||
import Element from '../models/element'
|
|
||||||
import Text from '../models/text'
|
|
||||||
import State from '../models/state'
|
import State from '../models/state'
|
||||||
|
import Text from '../models/text'
|
||||||
import groupByMarks from '../utils/group-by-marks'
|
import groupByMarks from '../utils/group-by-marks'
|
||||||
import { Map } from 'immutable'
|
import { Map } from 'immutable'
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ function serialize(state) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function serializeNode(node) {
|
function serializeNode(node) {
|
||||||
switch (node.type) {
|
switch (node.kind) {
|
||||||
case 'document': {
|
case 'document': {
|
||||||
return {
|
return {
|
||||||
nodes: node.nodes.toArray().map(node => serializeNode(node))
|
nodes: node.nodes.toArray().map(node => serializeNode(node))
|
||||||
@@ -37,15 +38,17 @@ function serializeNode(node) {
|
|||||||
}
|
}
|
||||||
case 'text': {
|
case 'text': {
|
||||||
return {
|
return {
|
||||||
type: 'text',
|
kind: node.kind,
|
||||||
ranges: serializeCharacters(node.characters)
|
ranges: serializeCharacters(node.characters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default: {
|
case 'block':
|
||||||
|
case 'inline': {
|
||||||
return {
|
return {
|
||||||
type: node.type,
|
|
||||||
data: node.data.toJSON(),
|
data: node.data.toJSON(),
|
||||||
nodes: node.nodes.toArray().map(node => serializeNode(node))
|
kind: node.kind,
|
||||||
|
nodes: node.nodes.toArray().map(node => serializeNode(node)),
|
||||||
|
type: node.type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +96,7 @@ function serializeMark(mark) {
|
|||||||
function deserialize(object) {
|
function deserialize(object) {
|
||||||
return State.create({
|
return State.create({
|
||||||
document: Document.create({
|
document: Document.create({
|
||||||
nodes: Element.createMap(object.nodes.map(deserializeNode))
|
nodes: Block.createMap(object.nodes.map(deserializeNode))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -106,19 +109,26 @@ function deserialize(object) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function deserializeNode(object) {
|
function deserializeNode(object) {
|
||||||
switch (object.type) {
|
switch (object.kind) {
|
||||||
|
case 'block': {
|
||||||
|
return Block.create({
|
||||||
|
type: object.type,
|
||||||
|
data: new Map(object.data),
|
||||||
|
nodes: Block.createMap(object.nodes.map(deserializeNode))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case 'inline': {
|
||||||
|
return Inline.create({
|
||||||
|
type: object.type,
|
||||||
|
data: new Map(object.data),
|
||||||
|
nodes: Inline.createMap(object.nodes.map(deserializeNode))
|
||||||
|
})
|
||||||
|
}
|
||||||
case 'text': {
|
case 'text': {
|
||||||
return Text.create({
|
return Text.create({
|
||||||
characters: deserializeRanges(object.ranges)
|
characters: deserializeRanges(object.ranges)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
default: {
|
|
||||||
return Element.create({
|
|
||||||
type: object.type,
|
|
||||||
data: new Map(object.data),
|
|
||||||
nodes: Element.createMap(object.nodes.map(deserializeNode))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user