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:
530
packages/slate-hyperscript/src/creators.js
Normal file
530
packages/slate-hyperscript/src/creators.js
Normal 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)
|
||||
}
|
||||
}
|
@@ -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}`
|
||||
)
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user