ContentEditable
通常使用 L1 方案的富文本编辑器都是基于浏览器自身 contentEditable 属性实现的,共用了浏览器的光标和选区;对数据层进行了抽象,依赖 DOM 对内容进行渲染。
L1 富文本编辑器的重点在于实现视图层和数据层的双向绑定,确保视图层的改动。
Background: Why ContentEditable is Terrible
ContentEditable is the native widget for editing rich text in a web browser.
A good WYSIWYG editor should satisfy the following 3 axioms:
The mapping between DOM content and Visible content should be well-behaved. The mapping between DOM selection and Visible selection should be well-behaved. All visible edits should map onto an algebraically closed and complete set of visible content.
You can read more in the ContentEditable is Terrible article.
https://medium.engineering/why-contenteditable-is-terrible-122d8a40e480
Brief Description
Prosemirror
The WYSIWYM rich content editor for the web which begin in 2015. the author is Marijn Haverbeke, the author of CodeMirror.
Lexical
Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance. (by facebook) ,
Opensouce start at May 26, 2022.
you can read more in the https://www.libhunt.com/compare-lexical-vs-prosemirror article
Main Concepts
The core of Lexical is a dependency-free text editor framework that allows developers to build powerful, simple and complex, editor surfaces.
You can attach a contenteditable DOM element to editor instances, and also register listeners and commands.
Editor States are immutable once created,
双缓存技术
Lexical has its own DOM reconciler that takes a set of Editor States (always the “current” and the “pending”) and applies a “diff” on them. I
- Listeners
- Node Transforms
- Commands
https://lexical.dev/docs/concepts/transforms
thanks to [lexical-react] It can take advantage of react 18’s new features to improve performance.
lexical/headless
Lexical https://lexical.dev › docs › packages › lexical-headless This package allows you to interact with Lexical in a headless environment
// When a TextNode changes (marked as dirty) make it bold
editor.registerNodeTransform(TextNode, textNode => {
// Important: Check current format state
if (!textNode.hasFormat('bold')) {
textNode.toggleFormat('bold');
}
}
example plugin
https://github.dev/sodenn/lexical-beautiful-mentions
explain
mature yes you are
collaboration
prosemirror and lexical both support collaboration, but the underlying data structure is different.
function collabEditor(authority, place) {
let view = new EditorView(place, {
state: EditorState.create({
doc: authority.doc,
plugins: [collab.collab({version: authority.steps.length})]
}),
dispatchTransaction(transaction) {
let newState = view.state.apply(transaction)
view.updateState(newState)
let sendable = collab.sendableSteps(newState)
if (sendable)
authority.receiveSteps(sendable.version, sendable.steps,
sendable.clientID)
}
})
authority.onNewSteps.push(function() {
let newData = authority.stepsSince(collab.getVersion(view.state))
view.dispatch(
collab.receiveTransaction(view.state, newData.steps, newData.clientIDs))
})
return view
}
prosemirror
prosemirror-model defines the editor’s document model, the data structure used to describe the content of the editor.
prosemirror-state provides the data structure that describes the editor’s whole state, including the selection, and a transaction system for moving from one state to the next.
prosemirror-view implements a user interface component that shows a given editor state as an editable element in the browser, and handles user interaction with that element.
prosemirror-transform contains functionality for modifying documents in a way that can be recorded and replayed, which is the basis for the transactions in the state module, and which makes the undo history and collaborative editing possible.
const trivialSchema = new Schema({
nodes: {
doc: {content: "paragraph+"},
paragraph: {content: "text*"},
text: {inline: true},
/* ... and so on */
}
})
marks
nodes: {
doc: {content: "block+"},
paragraph: {group: "block", content: "text*", marks: "_"},
heading: {group: "block", content: "text*", marks: ""},
text: {inline: true}
},
marks: {
strong: {},
em: {}
}
})
Yjs: A CRDT framework for shared editing Enable .
Reacter’s View
https://remirror.io/docs/getting-started/create-manager
Plugin you are right
这三款编辑器都支持使用 Yjs 实现协同编辑,底层满足 CRDT 的数据结构模型,
class Operation {
constructor(pm) {
this.doc = pm.doc
this.sel = pm.sel.range
this.scrollIntoView = false
this.focus = false
this.composingAtStart = !!pm.input.composing
}
}
Excited to release BlockNote, an open source Notion / Coda style block-based text editor component
We actually took a lot of inspiration from ProseMirror when creating parts of Lexical. However, we also had different requirements for Lexical – and some of the requirements we needed for Facebook, Instagram and WhatsApp came from tight requirements around code size, accessibility and compatibility with React 18 (using @lexical/react).
discussion
https://discuss.prosemirror.net/t/differences-between-prosemirror-and-lexical/4557/8
Yes you are right
headless feature
[] prosemirror [x] lexical [] slate
yjs
https://github.com/facebook/lexical/blob/main/packages/lexical-yjs/src/SyncEditorStates.ts
https://github.dev/yjs/y-prosemirror
https://demos.yjs.dev/prosemirror/prosemirror.html
https://lexical.dev/docs/react/plugins
https://docs.yjs.dev/getting-started/working-with-shared-types
VideoNode
hashtag
export class HashtagNode extends TextNode {
static getType(): string {
return 'hashtag';
}
static clone(node: HashtagNode): HashtagNode {
return new HashtagNode(node.__text, node.__key);
}
constructor(text: string, key?: NodeKey) {
super(text, key);
}
createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config);
addClassNamesToElement(element, config.theme.hashtag);
return element;
}
static importJSON(serializedNode: SerializedTextNode): HashtagNode {
const node = $createHashtagNode(serializedNode.text);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}
exportJSON(): SerializedTextNode {
return {
...super.exportJSON(),
type: 'hashtag',
};
}
canInsertTextBefore(): boolean {
return false;
}
isTextEntity(): true {
return true;
}
}
export class VideoNode extends DecoratorNode<ReactNode> {
__id: string;
static getType(): string {
return 'video';
}
static clone(node: VideoNode): VideoNode {
return new VideoNode(node.__id, node.__key);
}
constructor(id: string, key?: NodeKey) {
super(key);
this.__id = id;
}
createDOM(): HTMLElement {
return document.createElement('div');
}
updateDOM(): false {
return false;
}
decorate(): ReactNode {
return <VideoPlayer videoID={this.__id} />;
}
}
export function $createVideoNode(id: string): VideoNode {
return new VideoNode(id);
}
export function $isVideoNode(node: LexicalNode | null | undefined): node is VideoNode {
return node instanceof VideoNode;
}
editor
useAutoLink
lexical/packages/lexical-react/src
/LexicalAutoLinkPlugin.ts
handleNodeTransform
editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
const parent = textNode.getParentOrThrow();
const previous = textNode.getPreviousSibling();
if ($isAutoLinkNode(parent)) {
handleLinkEdit(parent, matchers, onChangeWrapped);
} else if (!$isLinkNode(parent)) {
if (
textNode.isSimpleText() &&
(startsWithSeparator(textNode.getTextContent()) ||
!$isAutoLinkNode(previous))
) {
handleLinkCreation(textNode, matchers, onChangeWrapped);
}
handleBadNeighbors(textNode, matchers, onChangeWrapped);
}
})
parseDom
React 18
We actually took a lot of inspiration from ProseMirror when creating parts of Lexical. However, we also had different requirements for Lexical – and some of the requirements we needed for Facebook, Instagram and WhatsApp came from tight requirements around code size, accessibility and compatibility with React 18 (using @lexical/react).
Performance
https://discuss.prosemirror.net/t/differences-between-prosemirror-and-lexical/4557
DOM Reconciler Lexical has its own DOM reconciler that takes a set of Editor States (always the “current” and the “pending”) and applies a “diff” on them. It then uses this diff to update only the parts of the DOM that need changing. You can think of this as a kind-of virtual DOM, except Lexical is able to skip doing much of the diffing work, as it knows what was mutated in a given update. The DOM reconciler adopts performance optimizations that benefit the typical heuristics of a content editable – and is able to ensure consistency for LTR and RTL languages automatically.
prosemirror projects
https://www.blocknotejs.org/docs/converting-blocks
inline block & block based custom node
https://juejin.cn/post/7140921781380415501
Lexical 中存储的数据结构是散列表映射,因此对于这个数据结构来说,只需要进行映射记录之间的更新即可让数据实现同步。
Lexical 中使用了 CollabElementNode 作为共享数据类型的存储,通过 $createCollabNodeFromLexicalNode() 函数将普通的节点转化为共享数据类型节点,该节点上会挂载一个实现了 Y.Map 类的 _map 的属性。
作者:JohnnyPan
链接:https://juejin.cn/post/7140921781380415501
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
本文通过对比不同富文本编辑器框架的一些实现,分析了编辑器实例、选区、规范化、原子操作等。
ProseMirror 登场比较早,使用文档详尽,插件丰富,功能强大,但是 API 略显晦涩。
Slate 最受欢迎(star 数领先),支持纯 JS 对象作为文档结构、个性化组件、丰富的 API、上手成本低,是很多编辑器的灵感来源,如语雀、Aomao。
Lexical 新兴力量,背靠 Facebook,映射结构、可以基于状态实现协同。此外,它的 DOM 节点不受外部插件影响以及原生支持 React 18+ 的 Cocurrency 实现局部渲染性能优化。
replace Map
Lexical 的节点是通过 Map 存储的(如下图),这和 Slate、ProseMirror 的树状数据结构有本质差异,主要体现在单个节点修改的效率和内存占用上。
优点:Map 结构存储的内容能够很快增删改某个特定节点,而对于树状数据结构,为了保证数据是持久化的 Single source of truth,必须按照不可变数据的理念(Immutable)去生成一个新对象,造成内存占用增大的问题。
缺点:相应地,由于存储 Map 的结构不能够很好地表达实际渲染出来 DOM 结果的层次,所以在每次渲染的时候,需要做一次协调(Reconcilation)去生成层次结构,可以把它想象成 React,它通过双重缓存实现单向数据流渲染。
作者:JohnnyPan
链接:https://juejin.cn/post/7140921781380415501
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
选择 Lexical
- 设计理念比较熟悉
- 最大利用 React18 的特性
- 自定义拓展比较简单