1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-28 09:29:49 +02:00

wow big refactor, added raw serializer

This commit is contained in:
Ian Storm Taylor
2016-06-17 18:20:26 -07:00
parent 8b9f5f0c00
commit 2d46528aae
16 changed files with 363 additions and 170 deletions

View File

@@ -1,5 +1,5 @@
import Editor, { State } from '../..'
import Editor, { State, Raw } from '../..'
import React from 'react'
import ReactDOM from 'react-dom'
@@ -33,7 +33,11 @@ const state = {
},
{
text: 'simple',
marks: ['bold']
marks: [
{
type: 'bold'
}
]
},
{
text: ' paragraph of text.'
@@ -78,14 +82,14 @@ function renderNode(node) {
}
function renderMark(mark) {
switch (mark) {
switch (mark.type) {
case 'bold': {
return {
fontWeight: 'bold'
}
}
default: {
throw new Error(`Unknown mark type "${mark}".`)
throw new Error(`Unknown mark type "${mark.type}".`)
}
}
}
@@ -97,7 +101,7 @@ function renderMark(mark) {
class App extends React.Component {
state = {
state: State.create(state)
state: Raw.deserialize(state)
};
render() {

View File

@@ -1,7 +1,8 @@
import Content from './content'
import React from 'react'
import CORE_PLUGIN from '../plugins/core'
import State from '../models/state'
import corePlugin from '../plugins/core'
/**
* Editor.
@@ -19,20 +20,38 @@ class Editor extends React.Component {
static defaultProps = {
plugins: [],
state: {}
state: new State()
};
/**
* When created, compute the plugins from `props`.
*
* @param {Object} props
*/
constructor(props) {
super(props)
this.state = {}
this.state.plugins = this.resolvePlugins(props)
}
/**
* When the `props` are updated, recompute the plugins.
*
* @param {Object} props
*/
componentWillReceiveProps(props) {
const plugins = this.resolvePlugins(props)
this.setState({ plugins })
}
/**
* When the `state` changes, pass through plugins, then bubble up.
*
* @param {State} state
*/
onChange(state) {
if (state == this.props.state) return
@@ -57,19 +76,20 @@ class Editor extends React.Component {
}
/**
* Handle the `keydown` event.
* When an event by `name` fires, pass it through the plugins, and update the
* state if one of them chooses to.
*
* @param {String} name
* @param {Event} e
*/
onKeyDown(e) {
onEvent(name, e) {
for (const plugin of this.state.plugins) {
if (plugin.onKeyDown) {
const newState = plugin.onKeyDown(e, this.props.state, this)
if (newState == null) continue
this.props.onChange(newState)
break
}
if (!plugin[name]) continue
const newState = plugin[name](e, this.props.state, this)
if (!newState) continue
this.props.onChange(newState)
break
}
}
@@ -86,15 +106,31 @@ class Editor extends React.Component {
renderNode={this.props.renderNode}
state={this.props.state}
onChange={state => this.onChange(state)}
onKeyDown={e => this.onKeyDown(e)}
onKeyDown={e => this.onEvent('keyDown', e)}
/>
)
}
/**
* Resolve the editor's current plugins from `props` when they change.
*
* Add a plugin made from the editor's own `props` at the beginning of the
* stack. That way, you can add a `onKeyDown` handler to the editor itself,
* and it will override all of the existing plugins.
*
* Also add the "core" functionality plugin that handles the most basic events
* for the editor, like delete characters and such.
*
* @param {Object} props
* @return {Array} plugins
*/
resolvePlugins(props) {
const { onChange, plugins, ...editorPlugin } = props
return [
...props.plugins,
CORE_PLUGIN
editorPlugin,
...plugins,
corePlugin
]
}

View File

@@ -2,7 +2,6 @@
import OffsetKey from '../utils/offset-key'
import React from 'react'
import ReactDOM from 'react-dom'
import createOffsetKey from '../utils/create-offset-key'
/**
* Leaf.
@@ -11,10 +10,13 @@ import createOffsetKey from '../utils/create-offset-key'
class Leaf extends React.Component {
static propTypes = {
marks: React.PropTypes.array.isRequired,
node: React.PropTypes.object.isRequired,
range: React.PropTypes.object.isRequired,
start: React.PropTypes.number.isRequired,
end: React.PropTypes.number.isRequired,
renderMark: React.PropTypes.func.isRequired,
state: React.PropTypes.object.isRequired,
text: React.PropTypes.string.isRequired
};
componentDidMount() {
@@ -33,11 +35,8 @@ class Leaf extends React.Component {
if (!selection.isFocused) return
const { anchorKey, anchorOffset, focusKey, focusOffset } = selection
const { node, range } = this.props
const { node, start, end } = this.props
const { key } = node
const { offset, text } = range
const start = offset
const end = offset + text.length
// If neither matches, the selection doesn't start or end here, so exit.
const hasStart = key == anchorKey && start <= anchorOffset && anchorOffset <= end
@@ -95,13 +94,12 @@ class Leaf extends React.Component {
}
render() {
const { node, range } = this.props
const { text } = range
const { node, start, end, text, marks } = this.props
const styles = this.renderStyles()
const offsetKey = OffsetKey.stringify({
key: node.key,
start: range.offset,
end: range.offset + range.text.length
start,
end
})
return (
@@ -110,14 +108,13 @@ class Leaf extends React.Component {
data-offset-key={offsetKey}
data-type='leaf'
>
{text == '' ? <br/> : text}
{text || <br/>}
</span>
)
}
renderStyles() {
const { range, renderMark } = this.props
const { marks } = range
const { marks, renderMark } = this.props
return marks.reduce((styles, mark) => {
return {
...styles,

View File

@@ -1,8 +1,8 @@
import Leaf from './leaf'
import OffsetKey from '../utils/offset-key'
import Raw from '../serializers/raw'
import React from 'react'
import convertCharactersToRanges from '../utils/convert-characters-to-ranges'
import createOffsetKey from '../utils/create-offset-key'
/**
* Text.
@@ -18,33 +18,47 @@ class Text extends React.Component {
render() {
const { node } = this.props
const { characters } = node
const ranges = convertCharactersToRanges(characters)
const leaves = ranges.length
? ranges.map(range => this.renderLeaf(range))
: this.renderSpacerLeaf()
return (
<span
key={node.key}
data-key={node.key}
data-type='text'
>
{leaves}
{this.renderLeaves()}
</span>
)
}
renderLeaves() {
const { node } = this.props
const { characters } = node
const ranges = Raw.serializeCharacters(characters)
return ranges.length
? ranges.map((range) => this.renderLeaf(range))
: this.renderSpacerLeaf()
}
renderLeaf(range) {
const { node, renderMark, state } = this.props
const key = createOffsetKey(node, range)
const { marks, offset, text } = range
const start = offset
const end = offset + text.length
const offsetKey = OffsetKey.stringify({
key: node.key,
start,
end
})
return (
<Leaf
key={key}
range={range}
key={offsetKey}
marks={marks}
node={node}
start={start}
end={end}
renderMark={renderMark}
state={state}
text={text}
/>
)
}

View File

@@ -15,6 +15,12 @@ import Selection from './models/selection'
import State from './models/state'
import Text from './models/text'
/**
* Serializers.
*/
import Raw from './serializers/raw'
/**
* Export.
*/
@@ -23,6 +29,7 @@ export default Editor
export {
Character,
Node,
Raw,
Selection,
State,
Text

View File

@@ -17,28 +17,25 @@ const CharacterRecord = new Record({
class Character extends CharacterRecord {
/**
* Create a character record from a Javascript `object`.
* Create a character record with `properties`.
*
* @param {Object} object
* @param {Object} properties
* @return {Character} character
*/
static create(object) {
return new Character({
text: object.text,
marks: new List(object.marks)
})
static create(properties = {}) {
return new Character(properties)
}
/**
* Create a list of characters from a Javascript `array`.
* Create a characters list from an array of characters.
*
* @param {Array} array
* @return {List} characters
*/
static createList(array) {
return new List(array.map(object => Character.create(object)))
static createList(array = []) {
return new List(array)
}
}

48
lib/models/mark.js Normal file
View File

@@ -0,0 +1,48 @@
import { List, Map, Record } from 'immutable'
/**
* Record.
*/
const MarkRecord = new Record({
data: new Map(),
type: null
})
/**
* Mark.
*/
class Mark extends MarkRecord {
/**
* Create a new `Mark` with `properties`.
*
* @param {Object} properties
* @return {Mark} mark
*/
static create(properties = {}) {
if (!properties.type) throw new Error('You must provide a `type` for the mark.')
return new Mark(properties)
}
/**
* Create a marks list from an array of marks.
*
* @param {Array} array
* @return {List} marks
*/
static createList(array = []) {
return new List(array)
}
}
/**
* Export.
*/
export default Mark

View File

@@ -21,36 +21,30 @@ const NodeRecord = new Record({
class Node extends NodeRecord {
/**
* Create a node record from a Javascript `object`.
* Create a new `Node` with `properties`.
*
* @param {Object} object
* @param {Object} properties
* @return {Node} node
*/
static create(object) {
return new Node({
data: new Map(object.data || {}),
key: uid(4),
nodes: Node.createMap(object.nodes || []),
type: object.type
})
static create(properties = {}) {
if (!properties.type) throw new Error('You must pass a node `type`.')
properties.key = uid(4)
return new Node(properties)
}
/**
* Create an ordered map of nodes from a Javascript `array` of nodes.
* Create an ordered map of `Nodes` from an array of `Nodes`.
*
* @param {Array} array
* @return {OrderedMap} nodes
* @param {Array} nodes
* @return {OrderedMap} map
*/
static createMap(array) {
return new OrderedMap(array.reduce((map, object) => {
const node = object.type == 'text'
? Text.create(object)
: Node.create(object)
map[node.key] = node
static createMap(nodes = []) {
return nodes.reduce((map, node) => {
map = map.set(node.key, node)
return map
}, {}))
}, new OrderedMap())
}
/**

View File

@@ -21,13 +21,14 @@ const SelectionRecord = new Record({
class Selection extends SelectionRecord {
/**
* Create a new `Selection` from `attrs`.
* Create a new `Selection` with `properties`.
*
* @param {Object} properties
* @return {Selection} selection
*/
static create(attrs) {
return new Selection(attrs)
static create(properties = {}) {
return new Selection(properties)
}
/**

View File

@@ -60,17 +60,14 @@ const SELECTION_LIKE_METHODS = [
class State extends StateRecord {
/**
* Create a new `State` from a Javascript `object`.
* Create a new `State` with `properties`.
*
* @param {Objetc} object
* @param {Objetc} properties
* @return {State} state
*/
static create(attrs) {
return new State({
nodes: Node.createMap(attrs.nodes),
selection: Selection.create(attrs.selection)
})
static create(properties = {}) {
return new State(properties)
}
/**

View File

@@ -19,18 +19,15 @@ const TextRecord = new Record({
class Text extends TextRecord {
/**
* Create a text record from a Javascript `object`.
* Create a new `Text` with `properties`.
*
* @param {Object} object
* @param {Object} properties
* @return {Node} node
*/
static create(attrs) {
const characters = convertRangesToCharacters(attrs.ranges || [])
return new Text({
key: uid(4),
characters
})
static create(properties = {}) {
properties.key = uid(4)
return new Text(properties)
}
/**

View File

@@ -3,10 +3,10 @@ import keycode from 'keycode'
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
/**
* The core plugin.
* Export.
*/
const CORE_PLUGIN = {
export default {
/**
* The core `onKeyDown` handler.
@@ -124,9 +124,3 @@ function isCommand(e) {
? e.metaKey && !e.altKey
: e.ctrlKey && !e.altKey
}
/**
* Export.
*/
export default CORE_PLUGIN

179
lib/serializers/raw.js Normal file
View File

@@ -0,0 +1,179 @@
import Character from '../models/character'
import Mark from '../models/mark'
import Node from '../models/node'
import Text from '../models/text'
import State from '../models/state'
import xor from 'lodash/xor'
import { Map } from 'immutable'
/**
* Serialize a `state`.
*
* @param {State} state
* @return {Object} object
*/
function serialize(state) {
return {
nodes: state.nodes.toArray().map(node => serializeNode(node))
}
}
/**
* Serialize a `node`.
*
* @param {Node} node
* @return {Object} object
*/
function serializeNode(node) {
switch (node.type) {
case 'text': {
return {
type: 'text',
ranges: serializeCharacters(node.characters)
}
}
default: {
return {
type: node.type,
data: node.data.toJSON(),
nodes: node.nodes.toArray().map(node => serializeNode(node))
}
}
}
}
/**
* Serialize a list of `characters`.
*
* @param {List} characters
* @return {Array}
*/
function serializeCharacters(characters) {
return characters
.toArray()
.reduce((ranges, char, i) => {
const previous = i == 0 ? null : characters.get(i - 1)
const { text } = char
const marks = char.marks.toArray().map(mark => serializeMark(mark))
if (previous) {
const previousMarks = previous.marks.toArray()
const diff = xor(marks, previousMarks)
if (!diff.length) {
const previousRange = ranges[ranges.length - 1]
previousRange.text += text
return ranges
}
}
const offset = ranges.map(range => range.text).join('').length
ranges.push({ text, marks, offset })
return ranges
}, [])
}
/**
* Serialize a `mark`.
*
* @param {Mark} mark
* @return {Object} Object
*/
function serializeMark(mark) {
return {
type: mark.type,
data: mark.data.toJSON()
}
}
/**
* Deserialize a state JSON `object`.
*
* @param {Object} object
* @return {State} state
*/
function deserialize(object) {
return State.create({
nodes: Node.createMap(object.nodes.map(deserializeNode))
})
}
/**
* Deserialize a node JSON `object`.
*
* @param {Object} object
* @return {Node} node
*/
function deserializeNode(object) {
switch (object.type) {
case 'text': {
return Text.create({
characters: deserializeRanges(object.ranges)
})
}
default: {
return Node.create({
type: object.type,
data: new Map(object.data),
nodes: Node.createMap(object.nodes.map(deserializeNode))
})
}
}
}
/**
* Deserialize a JSON `array` of ranges.
*
* @param {Array} array
* @return {List} characters
*/
function deserializeRanges(array) {
return array.reduce((characters, object) => {
const marks = object.marks || []
const chars = object.text
.split('')
.map(char => {
return Character.create({
text: char,
marks: Mark.createList(marks.map(deserializeMark))
})
})
return characters.push(...chars)
}, Character.createList())
}
/**
* Deserialize a mark JSON `object`.
*
* @param {Object} object
* @return {Mark} mark
*/
function deserializeMark(object) {
return Mark.create({
type: object.type,
data: new Map(object.data)
})
}
/**
* Export.
*/
export default {
serialize,
serializeCharacters,
serializeMark,
serializeNode,
deserialize,
deserializeNode,
deserializeRanges
}

View File

@@ -1,33 +0,0 @@
import xor from 'lodash/xor'
/**
* Convert a `characters` list to `ranges`.
*
* @param {CharacterList} characters
* @return {Array} ranges
*/
export default function convertCharactersToRanges(characters) {
return characters
.toArray()
.reduce((ranges, char, i) => {
const previous = i == 0 ? null : characters.get(i - 1)
const { text } = char
const marks = char.marks.toArray()
if (previous) {
const previousMarks = previous.marks.toArray()
const diff = xor(marks, previousMarks)
if (!diff.length) {
const previousRange = ranges[ranges.length - 1]
previousRange.text += text
return ranges
}
}
const offset = ranges.map(range => range.text).join('').length
ranges.push({ text, marks, offset })
return ranges
}, [])
}

View File

@@ -1,23 +0,0 @@
import Character from '../models/character'
/**
* Convert a `characters` list to `ranges`.
*
* @param {CharacterList} characters
* @return {Array} ranges
*/
export default function convertRangesToCharacters(ranges) {
return Character.createList(ranges.reduce((characters, range) => {
const chars = range.text
.split('')
.map(char => {
return {
text: char,
marks: range.marks
}
})
return characters.concat(chars)
}, []))
}

View File

@@ -1,16 +0,0 @@
/**
* Create an offset key from a `node` and a `range`.
*
* @param {Node} node
* @param {Object} range
* @property {Number} offset
* @property {String} text
* @return {String} offsetKey
*/
export default function createOffsetKey(node, range) {
const start = range.offset
const end = range.offset + range.text.length
return `${node.key}.${start}-${end}`
}