1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-30 18:39:51 +02:00

distinguish between block and inline nodes

This commit is contained in:
Ian Storm Taylor
2016-06-21 16:44:11 -07:00
parent dbdf3760e9
commit c3257a37d4
13 changed files with 286 additions and 121 deletions

View File

@@ -186,7 +186,8 @@ class App extends React.Component {
onBackspace(e, state) {
if (state.isCurrentlyExpanded) return
if (state.currentStartOffset != 0) return
const node = state.currentWrappingNodes.first()
const node = state.currentBlockNodes.first()
if (!node) debugger
if (node.type == 'paragraph') return
e.preventDefault()
@@ -207,7 +208,8 @@ class App extends React.Component {
onEnter(e, state) {
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.currentEndOffset != node.length) return

View File

@@ -1,10 +1,11 @@
{
"nodes": [
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"type": "text",
"kind": "text",
"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:"
@@ -14,10 +15,11 @@
]
},
{
"kind": "block",
"type": "block-quote",
"nodes": [
{
"type": "text",
"kind": "text",
"ranges": [
{
"text": "A wise quote."
@@ -27,10 +29,11 @@
]
},
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"type": "text",
"kind": "text",
"ranges": [
{
"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",
"nodes": [
{
"type": "text",
"kind": "text",
"ranges": [
{
"text": "Try it out!"
@@ -53,10 +57,11 @@
]
},
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"type": "text",
"kind": "text",
"ranges": [
{
"text": "Try it out for yourself! Try starting a new line with \">\", \"-\", or \"#\"s."

View File

@@ -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 ReactDOM from 'react-dom'
import state from './state.json'
@@ -19,13 +19,13 @@ function deserialize(string) {
}, Character.createList())
const text = Text.create({ characters })
const texts = Element.createMap([text])
const node = Element.create({
const texts = Block.createMap([text])
const node = Block.create({
type: 'paragraph',
nodes: texts,
})
const nodes = Element.createMap([node])
const nodes = Block.createMap([node])
const document = Document.create({ nodes })
const state = State.create({ document })
return state

View File

@@ -1,10 +1,11 @@
{
"nodes": [
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"type": "text",
"kind": "text",
"ranges": [
{
"text": "This is editable "
@@ -47,10 +48,11 @@
]
},
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"type": "text",
"kind": "text",
"ranges": [
{
"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",
"nodes": [
{
"type": "text",
"kind": "text",
"ranges": [
{
"text": "A wise quote."
@@ -83,10 +86,11 @@
]
},
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"type": "text",
"kind": "text",
"ranges": [
{
"text": "Try it out for yourself!"

View File

@@ -10,9 +10,10 @@ export default Editor
* Models.
*/
export { default as Block } from './models/block'
export { default as Character } from './models/character'
export { default as Element } from './models/element'
export { default as Document } from './models/document'
export { default as Inline } from './models/inline'
export { default as Mark } from './models/mark'
export { default as Selection } from './models/selection'
export { default as State } from './models/state'

97
lib/models/block.js Normal file
View 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

View File

@@ -7,7 +7,8 @@ import { OrderedMap, Record } from 'immutable'
*/
const DEFAULTS = {
nodes: new OrderedMap()
nodes: new OrderedMap(),
parent: null
}
/**
@@ -24,7 +25,18 @@ class Document extends Record(DEFAULTS) {
*/
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('')
}
/**
* Type.
*
* @return {String} type
*/
get type() {
return 'document'
}
}
/**

View File

@@ -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
* @return {Element} element
* @return {Inline} element
*/
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)
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
* @return {OrderedMap} map
@@ -46,6 +47,16 @@ class Element extends Record(DEFAULTS) {
}, new OrderedMap())
}
/**
* Get the node's kind.
*
* @return {String} kind
*/
get kind() {
return 'inline'
}
/**
* Get the length of the concatenated text of the node.
*
@@ -75,7 +86,7 @@ class Element extends Record(DEFAULTS) {
*/
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 default Element
export default Inline

View File

@@ -1,6 +1,6 @@
import Block from './block'
import Character from './character'
import Element from './element'
import Mark from './mark'
import Selection from './selection'
import Text from './text'
@@ -9,8 +9,8 @@ import { List, OrderedMap, OrderedSet, Set } from 'immutable'
/**
* Node.
*
* And interface that `Document` and `Element` both implement, to make working
* recursively easier with the tree easier.
* And interface that `Document`, `Block` and `Inline` all implement, to make
* working with the recursive node tree easier.
*/
const Node = {
@@ -202,7 +202,7 @@ const Node = {
if (shallow != null) return shallow
return this.nodes
.map(node => node.type == 'text' ? null : node.findNode(iterator))
.map(node => node.kind == 'text' ? null : node.findNode(iterator))
.filter(node => node)
.first()
},
@@ -217,7 +217,7 @@ const Node = {
filterNodes(iterator) {
const shallow = this.nodes.filter(iterator)
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)
.reduce((all, map) => {
return all.concat(map)
@@ -254,7 +254,7 @@ const Node = {
*/
getFirstTextNode() {
return this.findNode(node => node.type == 'text') || null
return this.findNode(node => node.kind == 'text') || null
},
/**
@@ -264,7 +264,7 @@ const Node = {
*/
getLastTextNode() {
const texts = this.filterNodes(node => node.type == 'text')
const texts = this.filterNodes(node => node.kind == 'text')
return texts.last() || null
},
@@ -335,7 +335,7 @@ const Node = {
// 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'
return node.kind == 'text'
? false
: node.hasNode(match)
})
@@ -372,7 +372,7 @@ const Node = {
if (shallow != null) return shallow
return this.nodes
.map(node => node.type == 'text' ? null : node.getNextNode(key))
.map(node => node.kind == 'text' ? null : node.getNextNode(key))
.filter(node => node)
.first()
},
@@ -396,7 +396,7 @@ const Node = {
}
return this.nodes
.map(node => node.type == 'text' ? null : node.getPreviousNode(key))
.map(node => node.kind == 'text' ? null : node.getPreviousNode(key))
.filter(node => node)
.first()
},
@@ -412,7 +412,7 @@ const Node = {
key = normalizeKey(key)
// 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({
anchorKey: first.key,
anchorOffset: 0,
@@ -439,7 +439,7 @@ const Node = {
let node = null
this.nodes.forEach((child) => {
if (child.type == 'text') return
if (child.kind == 'text') return
const match = child.getParentNode(key)
if (match) node = match
})
@@ -455,13 +455,11 @@ const Node = {
*/
getTextNodeAtOffset(offset) {
let match = null
let i
this.nodes.forEach((node) => {
if (!node.length > offset + i) return
match = node.type == 'text' ? node : node.getNodeAtOffset(offset - i)
i += node.length
let length = 0
let texts = this.filterNodes(node => node.kind == 'text')
let match = texts.find((node) => {
length += node.length
return length >= offset
})
return match
@@ -477,14 +475,17 @@ const Node = {
getTextNodesAtRange(range) {
range = range.normalize(this)
const { startKey, endKey } = range
// If the selection isn't formed, return an empty map.
if (startKey == null || endKey == null) return new OrderedMap()
// Assert that the nodes exist before searching.
this.assertHasNode(startKey)
this.assertHasNode(endKey)
// Return the text nodes after the start offset and before the end offset.
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 upToEnd = afterStart.takeUntil(node => node.key == endKey)
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
* @return {OrderedMap} nodes
*/
getWrappingNodesAtRange(range) {
getBlockNodesAtRange(range) {
const node = this
range = range.normalize(node)
const texts = node.getTextNodesAtRange(range)
const parents = texts.map((text) => {
return node.nodes.includes(text) ? node : node.getParentNode(text)
})
const blocks = texts.map(text => node.getClosestBlockNode(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
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)
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
*/
normalize() {
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 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 (!first) return node
// If no text nodes are adjacent, abort.
if (!firstAdjacent) return node
// Otherwise, add the text of the second node to the first...
let parent = node.getParentNode(first)
const second = parent.getNextNode(first)
const characters = first.characters.concat(second.characters)
first = first.merge({ characters })
parent = parent.updateNode(first)
// Fix an adjacent text node if one exists.
let parent = node.getParentNode(firstAdjacent)
const second = parent.getNextNode(firstAdjacent)
const characters = firstAdjacent.characters.concat(second.characters)
firstAdjacent = firstAdjacent.merge({ characters })
parent = parent.updateNode(firstAdjacent)
// Then remove the second node.
parent = parent.removeNode(second)
@@ -659,7 +691,7 @@ const Node = {
node = parent
}
// Finally, recurse by normalizing again.
// Recurse by normalizing again.
return node.normalize()
},
@@ -760,7 +792,7 @@ const Node = {
// Create a brand new second element with the second set of characters.
let secondText = Text.create({})
let secondElement = Element.create({
let secondElement = Block.create({
type: firstElement.type,
data: firstElement.data
})
@@ -852,7 +884,7 @@ const Node = {
}
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 })
@@ -872,7 +904,7 @@ const Node = {
// Allow for the parent to by just a type.
if (typeof parent == 'string') {
parent = Element.create({ type: parent })
parent = Block.create({ type: parent })
}
// Add the child to the parent's nodes.

View File

@@ -109,7 +109,7 @@ class Selection extends SelectionRecord {
isAtStartOf(node) {
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
}
@@ -122,7 +122,7 @@ class Selection extends SelectionRecord {
isAtEndOf(node) {
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
}
@@ -148,8 +148,8 @@ class Selection extends SelectionRecord {
let focusNode = node.getNode(focusKey)
// If the anchor node isn't a text node, match it to one.
if (anchorNode.type != 'text') {
anchorNode = node.getNodeAtOffset(anchorOffset)
if (anchorNode.kind != 'text') {
anchorNode = node.getTextNodeAtOffset(anchorOffset)
let parent = node.getParentNode(anchorNode)
let offset = parent.getNodeOffset(anchorNode)
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 (focusNode.type != 'text') {
focusNode = node.getNodeAtOffset(focusOffset)
if (focusNode.kind != 'text') {
focusNode = node.getTextNodeAtOffset(focusOffset)
let parent = node.getParentNode(focusNode)
let offset = parent.getNodeOffset(focusNode)
focusOffset = focusOffset - offset

View File

@@ -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
*/
get currentWrappingNodes() {
return this.document.getWrappingNodesAtRange(this.selection)
get currentBlockNodes() {
return this.document.getBlockNodesAtRange(this.selection)
}
/**

View File

@@ -3,19 +3,20 @@ import uid from 'uid'
import { List, Record } from 'immutable'
/**
* Record.
* Default properties.
*/
const TextRecord = new Record({
const DEFAULTS = {
characters: new List(),
key: null
})
key: null,
parent: null
}
/**
* Text.
*/
class Text extends TextRecord {
class Text extends Record(DEFAULTS) {
/**
* Create a new `Text` with `properties`.
@@ -29,6 +30,16 @@ class Text extends TextRecord {
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.
*
@@ -51,16 +62,6 @@ class Text extends TextRecord {
.join('')
}
/**
* Immutable type to match other nodes.
*
* @return {String} type
*/
get type() {
return 'text'
}
}
/**

View File

@@ -1,10 +1,11 @@
import Block from '../models/block'
import Character from '../models/character'
import Document from '../models/document'
import Inline from '../models/inline'
import Mark from '../models/mark'
import Element from '../models/element'
import Text from '../models/text'
import State from '../models/state'
import Text from '../models/text'
import groupByMarks from '../utils/group-by-marks'
import { Map } from 'immutable'
@@ -29,7 +30,7 @@ function serialize(state) {
*/
function serializeNode(node) {
switch (node.type) {
switch (node.kind) {
case 'document': {
return {
nodes: node.nodes.toArray().map(node => serializeNode(node))
@@ -37,15 +38,17 @@ function serializeNode(node) {
}
case 'text': {
return {
type: 'text',
kind: node.kind,
ranges: serializeCharacters(node.characters)
}
}
default: {
case 'block':
case 'inline': {
return {
type: node.type,
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) {
return State.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) {
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': {
return Text.create({
characters: deserializeRanges(object.ranges)
})
}
default: {
return Element.create({
type: object.type,
data: new Map(object.data),
nodes: Element.createMap(object.nodes.map(deserializeNode))
})
}
}
}