视图层: 分析
wangeditor
和slate-react
源码我们可以看出两者功能类似,都是将slate->createEditor()
生成的editor对象转化为vnode,然后挂载在带有contenteditable
属性的节点上。
model层:
slate
管理了一个 immer不可变类型的 nodes元素、selection选区slate editor
。它提供的其他API基本都是对 nodes元素、selection选区的操作。
为什么用 slate
我们前端的demo基于execCommand + contenteditable实现了基本的功能,但是有以下痛点:
execCommand
API 除了跨浏览器的差异之外,对于越复杂的功能例如表格、多级列表等,该 API 已经无法满足了,而且很容易出现我们无法控制的怪异问题。
这也是近几年像 Slate
这样优秀的框架抛弃了 execCommand
API的原因之一,把所有 DOM 插入通过高可用的 API 抽象出来。提供一个 editor对象,返回html对应的Node数据、选区Location,然后提供相应api操作 Node
和 Location
数据。
我们可以灵活定义html和Node的转换规则,灵活的使用editor api,这也是基于Slate的其他框架,如wangeditor
和 slate-react
实现的部分,最后它们将editor数据渲染成真实的视图。
immer
在解读源码前,先了解一下这个东东
immer概念:
immer 可以在需要使用不可变数据结构的任何上下文中使用。不可变的数据结构允许(高效)的变化检测:如果对对象的引用没有改变,那么对象本身也没有改变。此外,它使克隆对象相对便宜:数据树的未更改部分不需要复制,并且在内存中与相同状态的旧版本共享。
对 JSON 补丁的一流支持
immer 基本思想是,使用 Immer,您会将所有更改应用到临时 draft,它是 currentState 的代理。一旦你完成了所有的 mutations,Immer 将根据对 draft state 的 mutations 生成 nextState
immer基础使用:
import {produce} from "immer"
const todosObj = {
id1: {done: false, body: "Take out the trash"},
id2: {done: false, body: "Check Email"}
}
// 添加
const addedTodosObj = produce(todosObj, draft => {
draft["id3"] = {done: false, body: "Buy bananas"}
})
// 删除
const deletedTodosObj = produce(todosObj, draft => {
delete draft["id1"]
})
// 更新
const updatedTodosObj = produce(todosObj, draft => {
draft["id1"].done = true
})
slate源码结构
slate采用lerna多项目管理
- slate 实现editor对象,提供相应api
- slate-history 踪随着时间推移对 Slate 值状态的更改,并启用撤消和重做功能
- slate-hyperscript 将jsx语法转化为 slate 对象,
- slate-react Slate的文档数据是一颗类似 DOM 的节点树结构,slate-react 通过递归这颗树生成 children 数组,最终 react 将 children 数组中的组件渲染到页面上。这部分我们在 富文本editor- 3.4slate-react介绍
读源码前需要了解的概念
Nodes
editor.children的格式就是 Node 形式,类似一颗dom josn树结构,这使得我们很轻松 将nodes、vnode、html相互转化
type Node = Editor | Element | Text
Locations
指定在文档的哪个位置插入、删除等操作
type Location = Path | Point | Range //(Location可以是Path,Point,Range中的任何一个)
- Path:就是一个索引数组,将dom树按层次顺序存入数组,数字代表父节点第几个节点
- Point:就是焦点位置,由Path和offset组成,因为要确定一个位置不仅需要知道在哪个节点内,还需要知道在这个节点内的偏移位置
- Range:就是一段范围,两个点就可以组成一段范围,也就是两个Point,一个起始点,一个终止点
Operations
Slate的数据不能直接更改,Operations属于低级操作
export type BaseOperation = NodeOperation | SelectionOperation | TextOperation
- Node 节点、的操作
- Selection 选区的操作
- Node 下Text文本的操作
Transform
Slate的数据不能直接更改,可以通过Transforms操作对其更改。主要也是对 Node 节点、Selection 选区、 Node 下Text文本的操作,它是高级的封装,封装多个 Operations 操作,具体可看官网 Transform api
- Node Transform
- Selection Transform
- Text Transform
- editor-transforms
slate源码解读
我们阅读的版本是:"0.102.0",入口文件便是index.ts,它export下面所有模块,我们先大概介绍一下每个模块的内容
- create-editor 导出createEditor方法,创建
editor对象
,前面也多次用到过过该方法 - interfaces Node/Selection基本定义以及9种基础transform
- core apply综合执行4种transform命令、也提供其他基础transform操作
- editor 主要用于获取nodes/location数据
- transforms 封装node、selection、text操作
- transforms-node
- transforms-selection
- transforms-text
createEditor
我们可以看到它返回一个editor对象,该对象有很多方法,都是在其他目录实现的
- children属性包含构成编辑器内容的节点的文档树。
- selection属性包含用户的当前选择(如果有的话)。不要直接设置;使用transforms选择
- operations属性包含自上次刷新“更改”以来应用的所有操作。(由于Slate将操作批处理到事件循环的刻度中。)
- marks属性存储编辑器插入文本时要应用的格式。如果标记为null,则格式将取自当前所选内容。不要直接设置;使用Editor.addMark和Editor.removeMark。
- onChange 编辑器数据更改后执行该回调,暴露给需要监听编辑器更改的上层应用
export const createEditor = (): Editor => {
const editor: Editor = {
children: [],
operations: [],
selection: null,
marks: null,
onChange: () => {},
// Core
apply: (...args) => apply(editor, ...args),
getDirtyPaths: (...args) => getDirtyPaths(editor, ...args),
getFragment: (...args) => getFragment(editor, ...args),
// Editor
deleteBackward: (...args) => deleteBackward(editor, ...args),
insertBreak: (...args) => insertBreak(editor, ...args),
insertNode: (...args) => insertNode(editor, ...args),
// interface
node: (...args) => node(editor, ...args),
after: (...args) => after(editor, ...args),
collapse: (...args) => collapse(editor, ...args),
// transforms-text
delete: (...args) => deleteText(editor, ...args),
// transforms-selection
select: (...args) => select(editor, ...args),
setPoint: (...args) => setPoint(editor, ...args),
setSelection: (...args) => setSelection(editor, ...args),
// transforms-node
insertNodes: (...args) => insertNodes(editor, ...args),
liftNodes: (...args) => liftNodes(editor, ...args),
mergeNodes: (...args) => mergeNodes(editor, ...args),
...
}
return editor
}
interfaces
Nodes 与 Location 定义
目录:packages/slate/src/interfaces/
- Node 类的定义 packages/slate/src/interfaces/node.ts
- Point 类的定义 packages/slate/src/interfaces/point.ts
- Path 类的定义 packages/slate/src/interfaces/path.ts
- Range 类的定义 packages/slate/src/interfaces/range.ts
Operations 定义
根据isOperation 我们可以看出有下面9种基本类型,对应下面GeneralTransforms
- insert_node
- insert_text
- merge_node
- move_node
- remove_node
- remove_text
- set_node
- set_selection
- split_node
Transforms定义
目录:packages/slate/src/interfaces/transforms/
- NodeTransforms node基本操作,具体实现在 目录 transforms-node
- SelectionTransforms selection 基本操作 ,具体实现在 目录 transforms-selection
- TextTransforms text 基本操作,具体实现在 目录 transforms-text
- GeneralTransforms editor-transforms基本操作,具体如下:
packages/slate/src/interfaces/transforms/general.ts,主要看applyToDraft,9种基本Operations,可以看出都是对
Node 和 selection
的操作
const applyToDraft = (editor: Editor, selection: Selection, op: Operation) => {
switch (op.type) {
case 'insert_node': {
const { path, node } = op
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
parent.children.splice(index, 0, node)
if (selection) {
for (const [point, key] of Range.points(selection)) {
selection[key] = Point.transform(point, op)!
}
}
break
}
case 'insert_text': {
const { path, offset, text } = op
if (text.length === 0) break
const node = Node.leaf(editor, path)
const before = node.text.slice(0, offset)
const after = node.text.slice(offset)
node.text = before + text + after
if (selection) {
for (const [point, key] of Range.points(selection)) {
selection[key] = Point.transform(point, op)!
}
}
break
}
case 'merge_node': {
const { path } = op
const node = Node.get(editor, path)
const prevPath = Path.previous(path)
const prev = Node.get(editor, prevPath)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
if (Text.isText(node) && Text.isText(prev)) {
prev.text += node.text
} else if (!Text.isText(node) && !Text.isText(prev)) {
prev.children.push(...node.children)
}
parent.children.splice(index, 1)
if (selection) {
for (const [point, key] of Range.points(selection)) {
selection[key] = Point.transform(point, op)!
}
}
break
}
case 'move_node': {
const { path, newPath } = op
const node = Node.get(editor, path)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
parent.children.splice(index, 1)
const truePath = Path.transform(path, op)!
const newParent = Node.get(editor, Path.parent(truePath)) as Ancestor
const newIndex = truePath[truePath.length - 1]
newParent.children.splice(newIndex, 0, node)
if (selection) {
for (const [point, key] of Range.points(selection)) {
selection[key] = Point.transform(point, op)!
}
}
break
}
case 'remove_node': {
const { path } = op
const index = path[path.length - 1]
const parent = Node.parent(editor, path)
parent.children.splice(index, 1)
if (selection) {
for (const [point, key] of Range.points(selection)) {
const result = Point.transform(point, op)
if (selection != null && result != null) {
selection[key] = result
} else {
let prev: NodeEntry<Text> | undefined
let next: NodeEntry<Text> | undefined
for (const [n, p] of Node.texts(editor)) {
if (Path.compare(p, path) === -1) {
prev = [n, p]
} else {
next = [n, p]
break
}
}
let preferNext = false
if (prev && next) {
if (Path.equals(next[1], path)) {
preferNext = !Path.hasPrevious(next[1])
} else {
preferNext =
Path.common(prev[1], path).length <
Path.common(next[1], path).length
}
}
if (prev && !preferNext) {
point.path = prev[1]
point.offset = prev[0].text.length
} else if (next) {
point.path = next[1]
point.offset = 0
} else {
selection = null
}
}
}
}
break
}
case 'remove_text': {
const { path, offset, text } = op
if (text.length === 0) break
const node = Node.leaf(editor, path)
const before = node.text.slice(0, offset)
const after = node.text.slice(offset + text.length)
node.text = before + after
if (selection) {
for (const [point, key] of Range.points(selection)) {
selection[key] = Point.transform(point, op)!
}
}
break
}
case 'set_node': {
const { path, properties, newProperties } = op
if (path.length === 0) {
throw new Error(`Cannot set properties on the root node!`)
}
const node = Node.get(editor, path)
for (const key in newProperties) {
if (key === 'children' || key === 'text') {
throw new Error(`Cannot set the "${key}" property of nodes!`)
}
const value = newProperties[<keyof Node>key]
if (value == null) {
delete node[<keyof Node>key]
} else {
node[<keyof Node>key] = value
}
}
// properties that were previously defined, but are now missing, must be deleted
for (const key in properties) {
if (!newProperties.hasOwnProperty(key)) {
delete node[<keyof Node>key]
}
}
break
}
case 'set_selection': {
const { newProperties } = op
if (newProperties == null) {
selection = newProperties
} else {
if (selection == null) {
if (!Range.isRange(newProperties)) {
throw new Error(
`Cannot apply an incomplete "set_selection" operation properties ${Scrubber.stringify(
newProperties
)} when there is no current selection.`
)
}
selection = { ...newProperties }
}
for (const key in newProperties) {
const value = newProperties[<keyof Range>key]
if (value == null) {
if (key === 'anchor' || key === 'focus') {
throw new Error(`Cannot remove the "${key}" selection property`)
}
delete selection[<keyof Range>key]
} else {
selection[<keyof Range>key] = value
}
}
}
break
}
case 'split_node': {
const { path, position, properties } = op
if (path.length === 0) {
throw new Error(
`Cannot apply a "split_node" operation at path [${path}] because the root node cannot be split.`
)
}
const node = Node.get(editor, path)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
let newNode: Descendant
if (Text.isText(node)) {
const before = node.text.slice(0, position)
const after = node.text.slice(position)
node.text = before
newNode = {
...(properties as Partial<Text>),
text: after,
}
} else {
const before = node.children.slice(0, position)
const after = node.children.slice(position)
node.children = before
newNode = {
...(properties as Partial<Element>),
children: after,
}
}
parent.children.splice(index + 1, 0, newNode)
if (selection) {
for (const [point, key] of Range.points(selection)) {
selection[key] = Point.transform(point, op)!
}
}
break
}
}
return selection
}
Core -> apply
- 可以看出它是对 4中 transform 类型的综合更改。
- 更改完后触发 editor.onChange 回调
代码位置 packages/slate/src/core/apply.ts
export const apply: WithEditorFirstArg<Editor['apply']> = (editor, op) => {
for (const ref of Editor.pathRefs(editor)) {
PathRef.transform(ref, op)
}
for (const ref of Editor.pointRefs(editor)) {
PointRef.transform(ref, op)
}
for (const ref of Editor.rangeRefs(editor)) {
RangeRef.transform(ref, op)
}
// update dirty paths
if (!isBatchingDirtyPaths(editor)) {
const transform = Path.operationCanTransformPath(op)
? (p: Path) => Path.transform(p, op)
: undefined
updateDirtyPaths(editor, editor.getDirtyPaths(op), transform)
}
Transforms.transform(editor, op)
editor.operations.push(op)
Editor.normalize(editor, {
operation: op,
})
// Clear any formats applied to the cursor if the selection changes.
if (op.type === 'set_selection') {
editor.marks = null
}
if (!FLUSHING.get(editor)) {
FLUSHING.set(editor, true)
Promise.resolve().then(() => {
FLUSHING.set(editor, false)
editor.onChange({ operation: op })
editor.operations = []
})
}
}
normalizeNode
“Normalize”是指如何确保编辑器的内容始终具有特定的结构。它类似于“验证”,只是它的工作不是仅仅确定内容是有效还是无效,而是修复内容以使其再次有效。
代码位置 packages/slate/src/core/normalize-node.ts
export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
editor,
entry
) => {
const [node, path] = entry
// There are no core normalizations for text nodes.
if (Text.isText(node)) {
return
}
// Ensure that block and inline nodes have at least one text child.
if (Element.isElement(node) && node.children.length === 0) {
const child = { text: '' }
Transforms.insertNodes(editor, child, {
at: path.concat(0),
voids: true,
})
return
}
// Determine whether the node should have block or inline children.
const shouldHaveInlines = Editor.isEditor(node)
? false
: Element.isElement(node) &&
(editor.isInline(node) ||
node.children.length === 0 ||
Text.isText(node.children[0]) ||
editor.isInline(node.children[0]))
// Since we'll be applying operations while iterating, keep track of an
// index that accounts for any added/removed nodes.
let n = 0
for (let i = 0; i < node.children.length; i++, n++) {
const currentNode = Node.get(editor, path)
if (Text.isText(currentNode)) continue
const child = currentNode.children[n] as Descendant
const prev = currentNode.children[n - 1] as Descendant
const isLast = i === node.children.length - 1
const isInlineOrText =
Text.isText(child) || (Element.isElement(child) && editor.isInline(child))
// Only allow block nodes in the top-level children and parent blocks
// that only contain block nodes. Similarly, only allow inline nodes in
// other inline nodes, or parent blocks that only contain inlines and
// text.
if (isInlineOrText !== shouldHaveInlines) {
Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
n--
} else if (Element.isElement(child)) {
// Ensure that inline nodes are surrounded by text nodes.
if (editor.isInline(child)) {
if (prev == null || !Text.isText(prev)) {
const newChild = { text: '' }
Transforms.insertNodes(editor, newChild, {
at: path.concat(n),
voids: true,
})
n++
} else if (isLast) {
const newChild = { text: '' }
Transforms.insertNodes(editor, newChild, {
at: path.concat(n + 1),
voids: true,
})
n++
}
}
} else {
// Merge adjacent text nodes that are empty or match.
if (prev != null && Text.isText(prev)) {
if (Text.equals(child, prev, { loose: true })) {
Transforms.mergeNodes(editor, { at: path.concat(n), voids: true })
n--
} else if (prev.text === '') {
Transforms.removeNodes(editor, {
at: path.concat(n - 1),
voids: true,
})
n--
} else if (child.text === '') {
Transforms.removeNodes(editor, {
at: path.concat(n),
voids: true,
})
n--
}
}
}
}
}
transforms-node -> insertNodes
代码位置 packages/slate/src/transforms-node/insert-nodes.ts
export const insertNodes: NodeTransforms['insertNodes'] = (
editor,
nodes,
options = {}
) => {
Editor.withoutNormalizing(editor, () => {
const {
hanging = false,
voids = false,
mode = 'lowest',
batchDirty = true,
} = options
let { at, match, select } = options
if (Node.isNode(nodes)) {
nodes = [nodes]
}
if (nodes.length === 0) {
return
}
const [node] = nodes
if (!at) {
at = getDefaultInsertLocation(editor)
if (select !== false) {
select = true
}
}
if (select == null) {
select = false
}
if (Range.isRange(at)) {
if (!hanging) {
at = Editor.unhangRange(editor, at, { voids })
}
if (Range.isCollapsed(at)) {
at = at.anchor
} else {
const [, end] = Range.edges(at)
const pointRef = Editor.pointRef(editor, end)
Transforms.delete(editor, { at })
at = pointRef.unref()!
}
}
if (Point.isPoint(at)) {
if (match == null) {
if (Text.isText(node)) {
match = n => Text.isText(n)
} else if (editor.isInline(node)) {
match = n => Text.isText(n) || Editor.isInline(editor, n)
} else {
match = n => Element.isElement(n) && Editor.isBlock(editor, n)
}
}
const [entry] = Editor.nodes(editor, {
at: at.path,
match,
mode,
voids,
})
if (entry) {
const [, matchPath] = entry
const pathRef = Editor.pathRef(editor, matchPath)
const isAtEnd = Editor.isEnd(editor, at, matchPath)
Transforms.splitNodes(editor, { at, match, mode, voids })
const path = pathRef.unref()!
at = isAtEnd ? Path.next(path) : path
} else {
return
}
}
const parentPath = Path.parent(at)
let index = at[at.length - 1]
if (!voids && Editor.void(editor, { at: parentPath })) {
return
}
if (batchDirty) {
// PERF: batch update dirty paths
// batched ops used to transform existing dirty paths
const batchedOps: BaseInsertNodeOperation[] = []
const newDirtyPaths: Path[] = Path.levels(parentPath)
batchDirtyPaths(
editor,
() => {
for (const node of nodes as Node[]) {
const path = parentPath.concat(index)
index++
const op: BaseInsertNodeOperation = {
type: 'insert_node',
path,
node,
}
editor.apply(op)
at = Path.next(at as Path)
batchedOps.push(op)
if (!Text.isText) {
newDirtyPaths.push(path)
} else {
newDirtyPaths.push(
...Array.from(Node.nodes(node), ([, p]) => path.concat(p))
)
}
}
},
() => {
updateDirtyPaths(editor, newDirtyPaths, p => {
let newPath: Path | null = p
for (const op of batchedOps) {
if (Path.operationCanTransformPath(op)) {
newPath = Path.transform(newPath, op)
if (!newPath) {
return null
}
}
}
return newPath
})
}
)
} else {
for (const node of nodes as Node[]) {
const path = parentPath.concat(index)
index++
editor.apply({ type: 'insert_node', path, node })
at = Path.next(at as Path)
}
}
at = Path.previous(at)
if (select) {
const point = Editor.end(editor, at)
if (point) {
Transforms.select(editor, point)
}
}
})
}
transforms-selection -> move移动选区
代码位置 packages/slate/src/transforms-node/insert-nodes.ts
export const move: SelectionTransforms['move'] = (editor, options = {}) => {
const { selection } = editor
const { distance = 1, unit = 'character', reverse = false } = options
let { edge = null } = options
if (!selection) {
return
}
if (edge === 'start') {
edge = Range.isBackward(selection) ? 'focus' : 'anchor'
}
if (edge === 'end') {
edge = Range.isBackward(selection) ? 'anchor' : 'focus'
}
const { anchor, focus } = selection
const opts = { distance, unit, ignoreNonSelectable: true }
const props: Partial<Range> = {}
if (edge == null || edge === 'anchor') {
const point = reverse
? Editor.before(editor, anchor, opts)
: Editor.after(editor, anchor, opts)
if (point) {
props.anchor = point
}
}
if (edge == null || edge === 'focus') {
const point = reverse
? Editor.before(editor, focus, opts)
: Editor.after(editor, focus, opts)
if (point) {
props.focus = point
}
}
Transforms.setSelection(editor, props)
}
transforms-text -> insertFragment
代码位置 packages/slate/src/transforms-text/insert-fragment.ts
export const insertFragment: TextTransforms['insertFragment'] = (
editor,
fragment,
options = {}
) => {
Editor.withoutNormalizing(editor, () => {
const { hanging = false, voids = false } = options
let { at = getDefaultInsertLocation(editor), batchDirty = true } = options
if (!fragment.length) {
return
}
if (Range.isRange(at)) {
if (!hanging) {
at = Editor.unhangRange(editor, at, { voids })
}
if (Range.isCollapsed(at)) {
at = at.anchor
} else {
const [, end] = Range.edges(at)
if (!voids && Editor.void(editor, { at: end })) {
return
}
const pointRef = Editor.pointRef(editor, end)
Transforms.delete(editor, { at })
at = pointRef.unref()!
}
} else if (Path.isPath(at)) {
at = Editor.start(editor, at)
}
if (!voids && Editor.void(editor, { at })) {
return
}
// If the insert point is at the edge of an inline node, move it outside
// instead since it will need to be split otherwise.
const inlineElementMatch = Editor.above(editor, {
at,
match: n => Element.isElement(n) && Editor.isInline(editor, n),
mode: 'highest',
voids,
})
if (inlineElementMatch) {
const [, inlinePath] = inlineElementMatch
if (Editor.isEnd(editor, at, inlinePath)) {
const after = Editor.after(editor, inlinePath)!
at = after
} else if (Editor.isStart(editor, at, inlinePath)) {
const before = Editor.before(editor, inlinePath)!
at = before
}
}
const blockMatch = Editor.above(editor, {
match: n => Element.isElement(n) && Editor.isBlock(editor, n),
at,
voids,
})!
const [, blockPath] = blockMatch
const isBlockStart = Editor.isStart(editor, at, blockPath)
const isBlockEnd = Editor.isEnd(editor, at, blockPath)
const isBlockEmpty = isBlockStart && isBlockEnd
const mergeStart = !isBlockStart || (isBlockStart && isBlockEnd)
const mergeEnd = !isBlockEnd
const [, firstPath] = Node.first({ children: fragment }, [])
const [, lastPath] = Node.last({ children: fragment }, [])
const matches: NodeEntry[] = []
const matcher = ([n, p]: NodeEntry) => {
const isRoot = p.length === 0
if (isRoot) {
return false
}
if (isBlockEmpty) {
return true
}
if (
mergeStart &&
Path.isAncestor(p, firstPath) &&
Element.isElement(n) &&
!editor.isVoid(n) &&
!editor.isInline(n)
) {
return false
}
if (
mergeEnd &&
Path.isAncestor(p, lastPath) &&
Element.isElement(n) &&
!editor.isVoid(n) &&
!editor.isInline(n)
) {
return false
}
return true
}
for (const entry of Node.nodes({ children: fragment }, { pass: matcher })) {
if (matcher(entry)) {
matches.push(entry)
}
}
const starts = []
const middles = []
const ends = []
let starting = true
let hasBlocks = false
for (const [node] of matches) {
if (Element.isElement(node) && !editor.isInline(node)) {
starting = false
hasBlocks = true
middles.push(node)
} else if (starting) {
starts.push(node)
} else {
ends.push(node)
}
}
const [inlineMatch] = Editor.nodes(editor, {
at,
match: n => Text.isText(n) || Editor.isInline(editor, n),
mode: 'highest',
voids,
})!
const [, inlinePath] = inlineMatch
const isInlineStart = Editor.isStart(editor, at, inlinePath)
const isInlineEnd = Editor.isEnd(editor, at, inlinePath)
const middleRef = Editor.pathRef(
editor,
isBlockEnd && !ends.length ? Path.next(blockPath) : blockPath
)
const endRef = Editor.pathRef(
editor,
isInlineEnd ? Path.next(inlinePath) : inlinePath
)
Transforms.splitNodes(editor, {
at,
match: n =>
hasBlocks
? Element.isElement(n) && Editor.isBlock(editor, n)
: Text.isText(n) || Editor.isInline(editor, n),
mode: hasBlocks ? 'lowest' : 'highest',
always:
hasBlocks &&
(!isBlockStart || starts.length > 0) &&
(!isBlockEnd || ends.length > 0),
voids,
})
const startRef = Editor.pathRef(
editor,
!isInlineStart || (isInlineStart && isInlineEnd)
? Path.next(inlinePath)
: inlinePath
)
Transforms.insertNodes(editor, starts, {
at: startRef.current!,
match: n => Text.isText(n) || Editor.isInline(editor, n),
mode: 'highest',
voids,
batchDirty,
})
if (isBlockEmpty && !starts.length && middles.length && !ends.length) {
Transforms.delete(editor, { at: blockPath, voids })
}
Transforms.insertNodes(editor, middles, {
at: middleRef.current!,
match: n => Element.isElement(n) && Editor.isBlock(editor, n),
mode: 'lowest',
voids,
batchDirty,
})
Transforms.insertNodes(editor, ends, {
at: endRef.current!,
match: n => Text.isText(n) || Editor.isInline(editor, n),
mode: 'highest',
voids,
batchDirty,
})
if (!options.at) {
let path
if (ends.length > 0 && endRef.current) {
path = Path.previous(endRef.current)
} else if (middles.length > 0 && middleRef.current) {
path = Path.previous(middleRef.current)
} else if (startRef.current) {
path = Path.previous(startRef.current)
}
if (path) {
const end = Editor.end(editor, path)
Transforms.select(editor, end)
}
}
startRef.unref()
middleRef.unref()
endRef.unref()
})
}
写在最后!!!
富文本编辑器 系列文章:
通过对编辑器源码的解读,我学会了很多新思想,下面总结一下
- model层:
slate
管理了一个 immer不可变类型的 nodes元素、selection选区slate editor
- view层:
wangeditor
和slate-react
利用slate editor
的数据来渲染视图
-
文本标签
input 和 textarea
它们都不能设置丰富的样式,于是我们采用contenteditable
属性的编辑框,常规做法是结合document.execCommand
命令对编辑区域的元素进行设置,这就类似于我们初代的前端代码原生js/jquery,更改页面效果通过对真实dom的增删改查;而wangeditor
和slate-react
采用了一个新的思想,这就类似我们用 react/vue等框架开发页面,通过数据驱动的思想更改页面的元素。 -
MV:分析
wangeditor
和slate-react
源码我们可以看出两者功能类似,都是将slate editor
转化为vnode,然后将vnode挂载在带有contenteditable
属性的节点上;slate-react
是基于react,wangeditor
是通过snabbdom.js
,做到了与框架无关 -
VM:菜单工具的原理无非是渲染各种按钮,给按钮绑定点击事件。事件用于更改编辑器区域
slate editor
的内容,它主要思路就是 通过editor的api
对其nodes元素、selection选区更改
参考文档
欢迎关注我的前端自检清单,我和你一起成长