if (view.someProp("handleKeyPress", f => f(view, event))) { event.preventDefault() return }
let sel = view.state.selection if (!(sel instanceof TextSelection) || !sel.$from.sameParent(sel.$to)) { let text = String.fromCharCode(event.charCode) if (!view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text))) view.dispatch(view.state.tr.insertText(text).scrollIntoView()) event.preventDefault() } }
insertText(text, from, to = from) { let schema = this.doc.type.schema if (from == null) { if (!text) return this.deleteSelection() return this.replaceSelectionWith(schema.text(text), true) } else { if (!text) return this.deleteRange(from, to) let marks = this.storedMarks if (!marks) { let $from = this.doc.resolve(from) marks = to == from ? $from.marks() : $from.marksAcross(this.doc.resolve(to)) } this.replaceRangeWith(from, to, schema.text(text, marks)) if (!this.selection.empty) this.setSelection(Selection.near(this.selection.$to)) return this } }
insertText可以选择在当前位置插入或者选中位置插入,插入的逻辑就是替换当前选中的内容,还有一些处理就是继承插入位置的 mark 属性,插入后选区的位置处理等。
replaceSelectionWith(node, inheritMarks) { let selection = this.selection if (inheritMarks !== false) node = node.mark(this.storedMarks || (selection.empty ? selection.$from.marks() : (selection.$from.marksAcross(selection.$to) || Mark.none))) selection.replaceWith(this, node) return this }
// selection replaceWith(tr, node) { let mapFrom = tr.steps.length, ranges = this.ranges for (let i = 0; i < ranges.length; i++) { let {$from, $to} = ranges[i], mapping = tr.mapping.slice(mapFrom) let from = mapping.map($from.pos), to = mapping.map($to.pos) if (i) { tr.deleteRange(from, to) } else { tr.replaceRangeWith(from, to, node) selectionToInsertionEnd(tr, mapFrom, node.isInline ? -1 : 1) } } }
// transform replaceRangeWith = function(from, to, node) { if (!node.isInline && from == to && this.doc.resolve(from).parent.content.size) { let point = insertPoint(this.doc, from, node.type) if (point != null) from = to = point } return this.replaceRange(from, to, new Slice(Fragment.from(node), 0, 0)) }
// model // 根据schema定义判断是否可以替换到当前节点 canReplaceWith(from, to, type, marks) { if (marks && !this.type.allowsMarks(marks)) return false let start = this.contentMatchAt(from).matchType(type) let end = start && start.matchFragment(this.content, to) return end ? end.validEnd : false }
// transform replaceRange = function(from, to, slice) { let preferredDepth, leftNodes, targetDepths; // ...
// 寻找适合插入的深度
for (let j = slice.openStart; j >= 0; j--) { // ... // 匹配到适合插入的位置 if (parent.canReplaceWith(index, index, insert.type, insert.marks)) return this.replace($from.before(targetDepth), expand ? $to.after(targetDepth) : to, new Slice(closeFragment(slice.content, 0, slice.openStart, openDepth), openDepth, slice.openEnd)) }
// 如果未匹配到合适的插入点,直接进入replace let startSteps = this.steps.length for (let i = targetDepths.length - 1; i >= 0; i--) { this.replace(from, to, slice) if (this.steps.length > startSteps) break let depth = targetDepths[i] if (i < 0) continue from = $from.before(depth); to = $to.after(depth) } return this } }
// model function close(node, content) { if (!node.type.validContent(content)) throw new ReplaceError("Invalid content for node " + node.type.name) return node.copy(content) }
export function replace($from, $to, slice) { if (slice.openStart > $from.depth) throw new ReplaceError("Inserted content deeper than insertion position") if ($from.depth - slice.openStart != $to.depth - slice.openEnd) throw new ReplaceError("Inconsistent open depths") return replaceOuter($from, $to, slice, 0) }
function replaceOuter($from, $to, slice, depth) { let index = $from.index(depth), node = $from.node(depth) if (index == $to.index(depth) && depth < $from.depth - slice.openStart) { let inner = replaceOuter($from, $to, slice, depth + 1) return node.copy(node.content.replaceChild(index, inner)) } else if (!slice.content.size) { return close(node, replaceTwoWay($from, $to, depth)) } else if (!slice.openStart && !slice.openEnd && $from.depth == depth && $to.depth == depth) { // Simple, flat case let parent = $from.parent, content = parent.content return close(parent, content.cut(0, $from.parentOffset).append(slice.content).append(content.cut($to.parentOffset))) } else { let {start, end} = prepareSliceForReplace(slice, $from) return close(node, replaceThreeWay($from, start, end, $to, depth)) } }
// targetDepths: [-2, 1],preferredDepth: 2, preferredTargetIndex: 1 for (let j = slice.openStart; j >= 0; j--) { let openDepth = (j + preferredDepth + 1) % (slice.openStart + 1) let insert = leftNodes[openDepth] if (!insert) continue for (let i = 0; i < targetDepths.length; i++) { // 从上面找到的默认深度开始,需要适合的节点 let targetDepth = targetDepths[(i + preferredTargetIndex) % targetDepths.length] let expand = true if (targetDepth < 0) { expand = false; targetDepth = -targetDepth } let parent = $from.node(targetDepth - 1) // 当前深度的上级元素 let index = $from.index(targetDepth - 1) // 当前深度的元素在父元素中的索引,即在parent中索引 if (parent.canReplaceWith(index, index, insert.type, insert.marks)) { return this.replace($from.before(targetDepth), expand ? $to.after(targetDepth) : to, new Slice(closeFragment(slice.content, 0, slice.openStart, openDepth), openDepth, slice.openEnd)) } } }
到这一步,就完成了插入位置寻找,剩下的工作就是节点内容的替换,在上文已经了解过。
三、总结
prosemirror通过维护自己的文档数据树,并把所有对 dom 的操作都转换成对state的操作,抹平了不同浏览器带来的数据结构不一致的问题,并提升了prosemirror本身的视图层的可移植性,理论上可以通过自身实现视图层在任何平台上都实现编辑器。维护自身文档数据结构也可以说是一个比较主流解决方案,因为它让内容可控,状态可以变,并且为实现协同编辑提供了结构上的支撑。