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

Add controller (#2221)

* fold Stack into Editor

* switch Change objects to be tied to editors, not values

* introduce controller

* add the "commands" concept

* convert history into commands on `value.data`

* add the ability to not normalize on editor creation/setting

* convert schema to a mutable constructor

* add editor.command method

* convert plugin handlers to receive `next`

* switch commands to use the onCommand middleware

* add queries support, convert schema to queries

* split out browser plugin

* remove noop util

* fixes

* fixes

* start fixing tests, refactor hyperscript to be more literal

* fix slate-html-serializer tests

* fix schema tests with hyperscript

* fix text model tests with hyperscript

* fix more tests

* get all tests passing

* fix lint

* undo decorations example update

* update examples

* small changes to the api to make it nicer

* update docs

* update commands/queries plugin logic

* change normalizeNode and validateNode to be middleware

* fix decoration removal

* rename commands tests

* add useful errors to existing APIs

* update changelogs

* cleanup

* fixes

* update docs

* add editor docs
This commit is contained in:
Ian Storm Taylor
2018-10-09 14:03:27 -07:00
committed by GitHub
parent e6372d829a
commit 7a71de387c
709 changed files with 6073 additions and 5927 deletions

View File

@@ -0,0 +1,530 @@
import {
Decoration,
Document,
Leaf,
Mark,
Node,
Point,
Selection,
Text,
Value,
} from 'slate'
/**
* Auto-incrementing ID to keep track of paired decorations.
*
* @type {Number}
*/
let uid = 0
/**
* Create an anchor point.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {AnchorPoint}
*/
export function createAnchor(tagName, attributes, children) {
return new AnchorPoint(attributes)
}
/**
* Create a block.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Block}
*/
export function createBlock(tagName, attributes, children) {
const attrs = { ...attributes, object: 'block' }
const block = createNode('node', attrs, children)
return block
}
/**
* Create a cursor point.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {CursorPoint}
*/
export function createCursor(tagName, attributes, children) {
return new CursorPoint(attributes)
}
/**
* Create a decoration point, or wrap a list of leaves and set the decoration
* point tracker on them.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {DecorationPoint|List<Leaf>}
*/
export function createDecoration(tagName, attributes, children) {
const { key, data } = attributes
const type = tagName
if (key) {
return new DecorationPoint({ id: key, type, data })
}
const leaves = createLeaves('leaves', {}, children)
const first = leaves.first()
const last = leaves.last()
const id = `__decoration_${uid++}__`
const start = new DecorationPoint({ id, type, data })
const end = new DecorationPoint({ id, type, data })
setPoint(first, start, 0)
setPoint(last, end, last.text.length)
return leaves
}
/**
* Create a document.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Document}
*/
export function createDocument(tagName, attributes, children) {
const attrs = { ...attributes, object: 'document' }
const document = createNode('node', attrs, children)
return document
}
/**
* Create a focus point.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {FocusPoint}
*/
export function createFocus(tagName, attributes, children) {
return new FocusPoint(attributes)
}
/**
* Create an inline.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Inline}
*/
export function createInline(tagName, attributes, children) {
const attrs = { ...attributes, object: 'inline' }
const inline = createNode('node', attrs, children)
return inline
}
/**
* Create a list of leaves.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {List<Leaf>}
*/
export function createLeaves(tagName, attributes, children) {
const { marks = Mark.createSet() } = attributes
let length = 0
let leaves = Leaf.createList([])
let leaf
children.forEach(child => {
if (Leaf.isLeafList(child)) {
if (leaf) {
leaves = leaves.push(leaf)
leaf = null
}
child.forEach(l => {
l = preservePoint(l, obj => obj.addMarks(marks))
leaves = leaves.push(l)
})
} else {
if (!leaf) {
leaf = Leaf.create({ marks, text: '' })
length = 0
}
if (typeof child === 'string') {
const offset = leaf.text.length
leaf = preservePoint(leaf, obj => obj.insertText(offset, child))
length += child.length
}
if (isPoint(child)) {
setPoint(leaf, child, length)
}
}
})
if (!leaves.size && !leaf) {
leaf = Leaf.create({ marks, text: '' })
}
if (leaf) {
leaves = leaves.push(leaf)
}
return leaves
}
/**
* Create a list of leaves from a mark.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {List<Leaf>}
*/
export function createMark(tagName, attributes, children) {
const marks = Mark.createSet([attributes])
const leaves = createLeaves('leaves', { marks }, children)
return leaves
}
/**
* Create a node.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Node}
*/
export function createNode(tagName, attributes, children) {
const { object } = attributes
if (object === 'text') {
return createText('text', {}, children)
}
const nodes = []
let others = []
children.forEach(child => {
if (Node.isNode(child)) {
if (others.length) {
const text = createText('text', {}, others)
nodes.push(text)
}
nodes.push(child)
others = []
} else {
others.push(child)
}
})
if (others.length) {
const text = createText('text', {}, others)
nodes.push(text)
}
const node = Node.create({ ...attributes, nodes })
return node
}
/**
* Create a selection.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Selection}
*/
export function createSelection(tagName, attributes, children) {
const anchor = children.find(c => c instanceof AnchorPoint)
const focus = children.find(c => c instanceof FocusPoint)
const { marks, focused } = attributes
const selection = Selection.create({
marks,
isFocused: focused,
anchor: anchor && {
key: anchor.key,
offset: anchor.offset,
path: anchor.path,
},
focus: focus && {
key: focus.key,
offset: focus.offset,
path: focus.path,
},
})
return selection
}
/**
* Create a text node.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Text}
*/
export function createText(tagName, attributes, children) {
const { key } = attributes
const leaves = createLeaves('leaves', {}, children)
const text = Text.create({ key, leaves })
let length = 0
leaves.forEach(leaf => {
incrementPoint(leaf, length)
preservePoint(leaf, () => text)
length += leaf.text.length
})
return text
}
/**
* Create a value.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Value}
*/
export function createValue(tagName, attributes, children) {
const { data } = attributes
const document = children.find(Document.isDocument)
let selection = children.find(Selection.isSelection)
let anchor
let focus
let decorations = []
const partials = {}
// Search the document's texts to see if any of them have the anchor or
// focus information saved, or decorations applied.
if (document) {
document.getTexts().forEach(text => {
if (text.__anchor != null) {
anchor = Point.create({ key: text.key, offset: text.__anchor.offset })
}
if (text.__focus != null) {
focus = Point.create({ key: text.key, offset: text.__focus.offset })
}
if (text.__decorations != null) {
for (const dec of text.__decorations) {
const { id } = dec
const partial = partials[id]
delete partials[id]
if (!partial) {
dec.key = text.key
partials[id] = dec
continue
}
const decoration = Decoration.create({
anchor: {
key: partial.key,
offset: partial.offset,
},
focus: {
key: text.key,
offset: dec.offset,
},
mark: {
type: dec.type,
data: dec.data,
},
})
decorations.push(decoration)
}
}
})
}
if (Object.keys(partials).length > 0) {
throw new Error(
`Slate hyperscript must have both a start and an end defined for each decoration using the \`key=\` prop.`
)
}
if (anchor && !focus) {
throw new Error(
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<anchor />\`. For collapsed selections, use \`<cursor />\` instead.`
)
}
if (!anchor && focus) {
throw new Error(
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<focus />\`. For collapsed selections, use \`<cursor />\` instead.`
)
}
if (anchor || focus) {
if (!selection) {
selection = Selection.create({ anchor, focus, isFocused: true })
} else {
selection = selection.setPoints([anchor, focus])
}
} else if (!selection) {
selection = Selection.create()
}
selection = selection.normalize(document)
if (decorations.length > 0) {
decorations = decorations.map(d => d.normalize(document))
}
const value = Value.fromJSON({
data,
decorations,
document,
selection,
...attributes,
})
return value
}
/**
* Point classes that can be created at different points in the document and
* then searched for afterwards, for creating ranges.
*
* @type {Class}
*/
class CursorPoint {
constructor() {
this.offset = null
}
}
class AnchorPoint {
constructor(attrs = {}) {
const { key = null, offset = null, path = null } = attrs
this.key = key
this.offset = offset
this.path = path
}
}
class FocusPoint {
constructor(attrs = {}) {
const { key = null, offset = null, path = null } = attrs
this.key = key
this.offset = offset
this.path = path
}
}
class DecorationPoint {
constructor(attrs) {
const { id = null, data = {}, type } = attrs
this.id = id
this.offset = null
this.type = type
this.data = data
}
}
/**
* Increment any existing `point` on object by `n`.
*
* @param {Any} object
* @param {Number} n
*/
function incrementPoint(object, n) {
const { __anchor, __focus, __decorations } = object
if (__anchor != null) {
__anchor.offset += n
}
if (__focus != null && __focus !== __anchor) {
__focus.offset += n
}
if (__decorations != null) {
__decorations.forEach(d => (d.offset += n))
}
}
/**
* Check whether an `object` is a point.
*
* @param {Any} object
* @return {Boolean}
*/
function isPoint(object) {
return (
object instanceof AnchorPoint ||
object instanceof CursorPoint ||
object instanceof DecorationPoint ||
object instanceof FocusPoint
)
}
/**
* Preserve any point information on an object.
*
* @param {Any} object
* @param {Function} updator
* @return {Any}
*/
function preservePoint(object, updator) {
const { __anchor, __focus, __decorations } = object
const next = updator(object)
if (__anchor != null) next.__anchor = __anchor
if (__focus != null) next.__focus = __focus
if (__decorations != null) next.__decorations = __decorations
return next
}
/**
* Set a `point` on an `object`.
*
* @param {Any} object
* @param {*Point} point
* @param {Number} offset
*/
function setPoint(object, point, offset) {
if (point instanceof AnchorPoint || point instanceof CursorPoint) {
point.offset = offset
object.__anchor = point
}
if (point instanceof FocusPoint || point instanceof CursorPoint) {
point.offset = offset
object.__focus = point
}
if (point instanceof DecorationPoint) {
point.offset = offset
object.__decorations = object.__decorations || []
object.__decorations = object.__decorations.concat(point)
}
}

View File

@@ -1,286 +1,19 @@
import isPlainObject from 'is-plain-object'
import {
Block,
Decoration,
Document,
Inline,
Mark,
Node,
Point,
Selection,
Text,
Value,
} from 'slate'
/**
* Point classes that can be created at different points in the document and
* then searched for afterwards, for creating ranges.
*
* @type {Class}
*/
class CursorPoint {
constructor() {
this.offset = null
}
}
class AnchorPoint {
constructor(attrs = {}) {
const { key = null, offset = null, path = null } = attrs
this.key = key
this.offset = offset
this.path = path
}
}
class FocusPoint {
constructor(attrs = {}) {
const { key = null, offset = null, path = null } = attrs
this.key = key
this.offset = offset
this.path = path
}
}
class DecorationPoint {
constructor(attrs) {
const { key = null, data = {}, type } = attrs
this.id = key
this.offset = 0
this.type = type
this.data = data
}
combine = focus => {
if (!(focus instanceof DecorationPoint)) {
throw new Error('misaligned decorations')
}
return Decoration.create({
anchor: {
key: this.key,
offset: this.offset,
},
focus: {
key: focus.key,
offset: focus.offset,
},
mark: {
type: this.type,
data: this.data,
},
})
}
}
/**
* The default Slate hyperscript creator functions.
*
* @type {Object}
*/
const CREATORS = {
anchor(tagName, attributes, children) {
return new AnchorPoint(attributes)
},
block(tagName, attributes, children) {
return Block.create({
...attributes,
nodes: createChildren(children),
})
},
cursor(tagName, attributes, children) {
return new CursorPoint()
},
decoration(tagName, attributes, children) {
const { key, data } = attributes
const type = tagName
if (key) {
return new DecorationPoint({ key, type, data })
}
const nodes = createChildren(children)
const node = nodes[0]
const { __decorations = [] } = node
const __decoration = {
anchorOffset: 0,
focusOffset: nodes.reduce((len, n) => len + n.text.length, 0),
type,
data,
}
__decorations.push(__decoration)
node.__decorations = __decorations
return nodes
},
document(tagName, attributes, children) {
return Document.create({
...attributes,
nodes: createChildren(children),
})
},
focus(tagName, attributes, children) {
return new FocusPoint(attributes)
},
inline(tagName, attributes, children) {
return Inline.create({
...attributes,
nodes: createChildren(children),
})
},
mark(tagName, attributes, children) {
const marks = Mark.createSet([attributes])
const nodes = createChildren(children, { marks })
return nodes
},
selection(tagName, attributes, children) {
const anchor = children.find(c => c instanceof AnchorPoint)
const focus = children.find(c => c instanceof FocusPoint)
const { marks, focused } = attributes
const selection = Selection.create({
marks,
isFocused: focused,
anchor: anchor && {
key: anchor.key,
offset: anchor.offset,
path: anchor.path,
},
focus: focus && {
key: focus.key,
offset: focus.offset,
path: focus.path,
},
})
return selection
},
text(tagName, attributes, children) {
const nodes = createChildren(children, { key: attributes.key })
return nodes
},
value(tagName, attributes, children) {
const { data, normalize = true } = attributes
const document = children.find(Document.isDocument)
let selection = children.find(Selection.isSelection) || Selection.create()
let anchor
let focus
let decorations = []
const partials = {}
// Search the document's texts to see if any of them have the anchor or
// focus information saved, or decorations applied.
if (document) {
document.getTexts().forEach(text => {
if (text.__anchor != null) {
anchor = Point.create({ key: text.key, offset: text.__anchor.offset })
}
if (text.__focus != null) {
focus = Point.create({ key: text.key, offset: text.__focus.offset })
}
if (text.__decorations != null) {
text.__decorations.forEach(dec => {
const { id } = dec
let range
if (!id) {
range = Decoration.create({
anchor: {
key: text.key,
offset: dec.anchorOffset,
},
focus: {
key: text.key,
offset: dec.focusOffset,
},
mark: {
type: dec.type,
data: dec.data,
},
})
} else if (partials[id]) {
const partial = partials[id]
delete partials[id]
range = Decoration.create({
anchor: {
key: partial.key,
offset: partial.offset,
},
focus: {
key: text.key,
offset: dec.offset,
},
mark: {
type: dec.type,
data: dec.data,
},
})
} else {
dec.key = text.key
partials[id] = dec
}
if (range) {
decorations.push(range)
}
})
}
})
}
if (Object.keys(partials).length > 0) {
throw new Error(
`Slate hyperscript must have both a start and an end defined for each decoration using the \`key=\` prop.`
)
}
if (anchor && !focus) {
throw new Error(
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<anchor />\`. For collapsed selections, use \`<cursor />\` instead.`
)
}
if (!anchor && focus) {
throw new Error(
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<focus />\`. For collapsed selections, use \`<cursor />\` instead.`
)
}
let value = Value.fromJSON(
{ data, document, selection, ...attributes },
{ normalize }
)
if (anchor || focus) {
selection = selection.setPoints([anchor, focus])
selection = selection.setIsFocused(true)
selection = selection.normalize(value.document)
value = value.set('selection', selection)
}
if (decorations.length > 0) {
decorations = decorations.map(d => d.normalize(value.document))
decorations = Decoration.createList(decorations)
value = value.set('decorations', decorations)
}
return value
},
}
createAnchor,
createBlock,
createCursor,
createDecoration,
createDocument,
createFocus,
createInline,
createMark,
createNode,
createSelection,
createText,
createValue,
} from './creators'
/**
* Create a Slate hyperscript function with `options`.
@@ -290,7 +23,39 @@ const CREATORS = {
*/
function createHyperscript(options = {}) {
const creators = resolveCreators(options)
const { blocks = {}, inlines = {}, marks = {}, decorations = {} } = options
const creators = {
anchor: createAnchor,
block: createBlock,
cursor: createCursor,
decoration: createDecoration,
document: createDocument,
focus: createFocus,
inline: createInline,
mark: createMark,
node: createNode,
selection: createSelection,
text: createText,
value: createValue,
...(options.creators || {}),
}
for (const key in blocks) {
creators[key] = normalizeCreator(blocks[key], createBlock)
}
for (const key in inlines) {
creators[key] = normalizeCreator(inlines[key], createInline)
}
for (const key in marks) {
creators[key] = normalizeCreator(marks[key], createMark)
}
for (const key in decorations) {
creators[key] = normalizeCreator(decorations[key], createDecoration)
}
function create(tagName, attributes, ...children) {
const creator = creators[tagName]
@@ -312,195 +77,22 @@ function createHyperscript(options = {}) {
.filter(child => Boolean(child))
.reduce((memo, child) => memo.concat(child), [])
const element = creator(tagName, attributes, children)
return element
const ret = creator(tagName, attributes, children)
return ret
}
return create
}
/**
* Create an array of `children`, storing selection anchor and focus.
*
* @param {Array} children
* @param {Object} options
* @return {Array}
*/
function createChildren(children, options = {}) {
const array = []
let length = 0
// When creating the new node, try to preserve a key if one exists.
const firstNodeOrText = children.find(c => typeof c !== 'string')
const firstText = Text.isText(firstNodeOrText) ? firstNodeOrText : null
const key = options.key ? options.key : firstText ? firstText.key : undefined
let node = Text.create({ key, leaves: [{ text: '', marks: options.marks }] })
// Create a helper to update the current node while preserving any stored
// anchor or focus information.
function setNode(next) {
const { __anchor, __focus, __decorations } = node
if (__anchor != null) next.__anchor = __anchor
if (__focus != null) next.__focus = __focus
if (__decorations != null) next.__decorations = __decorations
node = next
}
children.forEach((child, index) => {
const isLast = index === children.length - 1
// If the child is a non-text node, push the current node and the new child
// onto the array, then creating a new node for future selection tracking.
if (Node.isNode(child) && !Text.isText(child)) {
if (
node.text.length ||
node.__anchor != null ||
node.__focus != null ||
node.getMarksAtIndex(0).size
) {
array.push(node)
}
array.push(child)
node = isLast
? null
: Text.create({ leaves: [{ text: '', marks: options.marks }] })
length = 0
}
// If the child is a string insert it into the node.
if (typeof child == 'string') {
setNode(node.insertText(node.text.length, child, options.marks))
length += child.length
}
// If the node is a `Text` add its text and marks to the existing node. If
// the existing node is empty, and the `key` option wasn't set, preserve the
// child's key when updating the node.
if (Text.isText(child)) {
const { __anchor, __focus, __decorations } = child
let i = node.text.length
if (!options.key && node.text.length == 0) {
setNode(node.set('key', child.key))
}
child.getLeaves().forEach(leaf => {
let { marks } = leaf
if (options.marks) marks = marks.union(options.marks)
setNode(node.insertText(i, leaf.text, marks))
i += leaf.text.length
})
if (__anchor != null) {
node.__anchor = new AnchorPoint()
node.__anchor.offset = __anchor.offset + length
}
if (__focus != null) {
node.__focus = new FocusPoint()
node.__focus.offset = __focus.offset + length
}
if (__decorations != null) {
__decorations.forEach(d => {
if (d instanceof DecorationPoint) {
d.offset += length
} else {
d.anchorOffset += length
d.focusOffset += length
}
})
node.__decorations = node.__decorations || []
node.__decorations = node.__decorations.concat(__decorations)
}
length += child.text.length
}
if (child instanceof AnchorPoint || child instanceof CursorPoint) {
child.offset = length
node.__anchor = child
}
if (child instanceof FocusPoint || child instanceof CursorPoint) {
child.offset = length
node.__focus = child
}
if (child instanceof DecorationPoint) {
child.offset = length
node.__decorations = node.__decorations || []
node.__decorations = node.__decorations.concat(child)
}
})
// Make sure the most recent node is added.
if (node != null) {
array.push(node)
}
return array
}
/**
* Resolve a set of hyperscript creators an `options` object.
*
* @param {Object} options
* @return {Object}
*/
function resolveCreators(options) {
const {
blocks = {},
inlines = {},
marks = {},
decorations = {},
schema,
} = options
const creators = {
...CREATORS,
...(options.creators || {}),
}
Object.keys(blocks).map(key => {
creators[key] = normalizeNode(blocks[key], 'block')
})
Object.keys(inlines).map(key => {
creators[key] = normalizeNode(inlines[key], 'inline')
})
Object.keys(marks).map(key => {
creators[key] = normalizeMark(marks[key])
})
Object.keys(decorations).map(key => {
creators[key] = normalizeNode(decorations[key], 'decoration')
})
creators.value = (tagName, attributes = {}, children) => {
const attrs = { schema, ...attributes }
return CREATORS.value(tagName, attrs, children)
}
return creators
}
/**
* Normalize a node creator of `value` and `object`.
* Normalize a `creator` of `value`.
*
* @param {Function|Object|String} value
* @param {String} object
* @param {Function} creator
* @return {Function}
*/
function normalizeNode(value, object) {
function normalizeCreator(value, creator) {
if (typeof value == 'function') {
return value
}
@@ -514,7 +106,6 @@ function normalizeNode(value, object) {
const { key, ...rest } = attributes
const attrs = {
...value,
object,
key,
data: {
...(value.data || {}),
@@ -522,47 +113,12 @@ function normalizeNode(value, object) {
},
}
return CREATORS[object](tagName, attrs, children)
return creator(tagName, attrs, children)
}
}
throw new Error(
`Slate hyperscript ${object} creators can be either functions, objects or strings, but you passed: ${value}`
)
}
/**
* Normalize a mark creator of `value`.
*
* @param {Function|Object|String} value
* @return {Function}
*/
function normalizeMark(value) {
if (typeof value == 'function') {
return value
}
if (typeof value == 'string') {
value = { type: value }
}
if (isPlainObject(value)) {
return (tagName, attributes, children) => {
const attrs = {
...value,
data: {
...(value.data || {}),
...attributes,
},
}
return CREATORS.mark(tagName, attrs, children)
}
}
throw new Error(
`Slate hyperscript mark creators can be either functions, objects or strings, but you passed: ${value}`
`Slate hyperscript creators can be either functions, objects or strings, but you passed: ${value}`
)
}