Lexicalさんはじめまして!
公式ドキュメントを読むだけのスクラップ
READMEから
Reactに限らず使用できるがReactのためのインターフェイスが用意されている。
サンプルコードに疑問を投げかけてみるか。
import {$getRoot, $getSelection} from 'lexical';
import {useEffect} from 'react';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
const theme = {
// Theme styling goes here
// ...
}
// When the editor changes, you can get notified via the
// LexicalOnChangePlugin!
function onChange(editorState) {
editorState.read(() => {
// Read the contents of the EditorState here.
const root = $getRoot();
const selection = $getSelection();
console.log(root, selection);
});
}
// Lexical React plugins are React components, which makes them
// highly composable. Furthermore, you can lazy load plugins if
// desired, so you don't pay the cost for plugins until you
// actually use them.
function MyCustomAutoFocusPlugin() {
const [editor] = useLexicalComposerContext();
useEffect(() => {
// Focus the editor when the effect fires!
editor.focus();
}, [editor]);
return null;
}
// Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will
// try to recover gracefully without losing user data.
function onError(error) {
console.error(error);
}
function Editor() {
const initialConfig = {
namespace: 'MyEditor',
theme,
onError,
};
return (
<LexicalComposer initialConfig={initialConfig}>
<PlainTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>Enter some text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin onChange={onChange} />
<HistoryPlugin />
<MyCustomAutoFocusPlugin />
</LexicalComposer>
);
}```
$
つきの関数が印象的
import {$getRoot, $getSelection} from 'lexical';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
plugin多いな〜。明示的でplugableなのはありがたいところ。
たしかにComponentの形を取ることで使いやすくなる側面もありそう。Conditional renderingとかできるし。
// Lexical React plugins are React components, which makes them
// highly composable. Furthermore, you can lazy load plugins if
// desired, so you don't pay the cost for plugins until you
// actually use them.
return (
<LexicalComposer initialConfig={initialConfig}>
<PlainTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>Enter some text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin onChange={onChange} />
<HistoryPlugin />
<MyCustomAutoFocusPlugin />
</LexicalComposer>
);
てか onChange
までpluginなんか。初期状態はめちゃめちゃ薄いんだな。textarea
のinterfaceを継承しているわけではない?
自然言語の説明に戻る。
An Editor State is the underlying data model that represents what you want to show on the DOM.
Editor Stateは仮想DOM的なあれ?
Editor States are also fully serializable to JSON and can easily be serialized back into the editor using editor.parseEditorState().
Editor StateはJSONと行き来できる。
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.
まじでa kind-of virtual DOMやったわ
Commands are the communication system used to wire everything together in Lexical. Custom commands can be created using createCommand() and dispatched to an editor using editor.dispatchCommand(command, payload). Lexical dispatches commands internally when key presses are triggered and when other important signals occur. Commands can also be handled using editor.registerCommand(handler, priority), and incoming commands are propagated through all handlers by priority until a handler stops the propagation (in a similar way to event propagation in the browser).
操作をCommandとして登録して、editor.dispatchCommand(command, payload)
を行うことで操作するらしい。これにも既視感ありますなあ。
あ、ここまではReactでLexicalを使用するときの説明だったらしい。だからReactに寄せているのか。
For those intending to use Lexical in their React applications, it's advisable to check out the source-code for the hooks that are shipped in @lexical/react.
やるなら別スクラップになりそうだけどはい。
こういうときまず触ってみるのとドキュメント読むのと迷うけど、長期的に見たらドキュメントじっくり読んでからの方が良い気がしているのでまず読む
The core package of Lexical is only 22kb in file size (min+gzip) and you only ever pay the cost for what you need. So Lexical can grow with your surface and the requirements. Furthermore, in frameworks that support lazy-loading, you can defer Lexical plugins until the user actually interacts with the editor itself – which can greatly help improve performance.
パフォーマンスにはかなり気を使っているみたい。WYSIWIGって重いがちやもんな。
At Meta, Lexical powers web text editing experiences for hundreds of millions of users everyday across Facebook, Workplace, Messenger, WhatsApp and Instagram.
ほーん、Meta製のアプリではLexical的なものが使われていると。
introの残りの部分はREADMEと同じ
Lexical tries to make theming straight-forward, by providing a way of passing a customizable theming object that maps CSS class names to the editor on creation. Here's an example of a plain-text theme:
className割当を行うことでスタイルへのアクセスを提供しているみたい
こんなかんじ
const exampleTheme = {
ltr: 'ltr',
rtl: 'rtl',
placeholder: 'editor-placeholder',
paragraph: 'editor-paragraph',
quote: 'editor-quote',
heading: {
h1: 'editor-heading-h1',
h2: 'editor-heading-h2',
h3: 'editor-heading-h3',
h4: 'editor-heading-h4',
h5: 'editor-heading-h5',
h6: 'editor-heading-h6',
},
list: {
nested: {
listitem: 'editor-nested-listitem',
},
ol: 'editor-list-ol',
ul: 'editor-list-ul',
listitem: 'editor-listItem',
listitemChecked: 'editor-listItemChecked',
listitemUnchecked: 'editor-listItemUnchecked',
},
hashtag: 'editor-hashtag',
image: 'editor-image',
link: 'editor-link',
text: {
bold: 'editor-textBold',
code: 'editor-textCode',
italic: 'editor-textItalic',
strikethrough: 'editor-textStrikethrough',
subscript: 'editor-textSubscript',
superscript: 'editor-textSuperscript',
underline: 'editor-textUnderline',
underlineStrikethrough: 'editor-textUnderlineStrikethrough',
},
code: 'editor-code',
codeHighlight: {
atrule: 'editor-tokenAttr',
attr: 'editor-tokenAttr',
boolean: 'editor-tokenProperty',
builtin: 'editor-tokenSelector',
cdata: 'editor-tokenComment',
char: 'editor-tokenSelector',
class: 'editor-tokenFunction',
'class-name': 'editor-tokenFunction',
comment: 'editor-tokenComment',
constant: 'editor-tokenProperty',
deleted: 'editor-tokenProperty',
doctype: 'editor-tokenComment',
entity: 'editor-tokenOperator',
function: 'editor-tokenFunction',
important: 'editor-tokenVariable',
inserted: 'editor-tokenSelector',
keyword: 'editor-tokenAttr',
namespace: 'editor-tokenVariable',
number: 'editor-tokenProperty',
operator: 'editor-tokenOperator',
prolog: 'editor-tokenComment',
property: 'editor-tokenProperty',
punctuation: 'editor-tokenPunctuation',
regex: 'editor-tokenVariable',
selector: 'editor-tokenSelector',
string: 'editor-tokenSelector',
symbol: 'editor-tokenProperty',
tag: 'editor-tokenProperty',
url: 'editor-tokenOperator',
variable: 'editor-tokenVariable',
},
};
Concept
いちばん大事なところ
After an update, the editor state is then locked and deemed immutable from there on. This editor state can therefore be thought of as a "snapshot".
snapshotてまんまReactと同じやん
Editor states contain two core things:
- The editor node tree (starting from the root node).
- The editor selection (which can be null).
node treeとselectionがcoreらしい。
というかそれ以外の要素はpluginで提供しているよってことかな?
<Button label="Save" onPress={() => {
if (editorStateRef.current) {
saveContent(JSON.stringify(editorStateRef.current))
}
}} />
できればmdでも保存したいな。
...this means that Lexical leverages a technique called double-buffering during updates.
double-buffering
だけでもいい検索ワードになりそう
updateListenerをこれで設定できるらしい
editor.registerUpdateListener(({editorState}) => {
// The latest EditorState can be found as `editorState`.
// To read the contents of the EditorState, use the following API:
editorState.read(() => {
// Just like editor.update(), .read() expects a closure where you can use
// the $ prefixed helper functions.
});
});
LexicalNode
っていうのをベースのノードにして作られている。自分で拡張するときはElementNode
,
TextNode
, DecoratorNode
を使うと良い。
To avoid selection issues, Lexical forbids insertion of text nodes directly into a RootNode.
RootNode
は他のノードと明確に区別されている。
You should never have '\n' in your text nodes, instead you should use the LineBreakNode which represents '\n', and more importantly, can work consistently between browsers and operating systems.
改行ノード
ElementNodeの一部にTextNodeが入ってくるイメージかな。
特殊要素を作るときはDecoratorNodeを使う。
It's important that these properties are JSON serializable too, so you should never be assigning a property to a node that is a function, Symbol, Map, Set, or any other object that has a different prototype than the built-ins.
NodeのpropertyにはJSON serializableを保つための制限がある。
By convention, we prefix properties with __ (double underscore) so that it makes it clear that these properties are private and their access should be avoided directly. We opted for __ instead of _ because of the fact that some build tooling mangles and minifies single _ prefixed properties to improve code size. However, this breaks down if you're exposing a node to be extended outside of your build.
propertiesには__のprefix
カスタムノードにはgetType()
, clone()
, set*()
, get*()
が必要。
class MyCustomNode extends SomeOtherNode {
__foo: string;
static getType(): string {
return 'custom-node';
}
static clone(node: MyCustomNode): MyCustomNode {
return new MyCustomNode(node.__foo, node.__key);
}
constructor(foo: string, key?: NodeKey) {
super(key);
this.__foo = foo;
}
setFoo(foo: string) {
// getWritable() creates a clone of the node
// if needed, to ensure we don't try and mutate
// a stale version of this node.
const self = this.getWritable();
self.__foo = foo;
}
getFoo(): string {
// getLatest() ensures we are getting the most
// up-to-date value from the EditorState.
const self = this.getLatest();
return self.__foo;
}
}
エチケットらしい
export function $createCustomParagraphNode(): ParagraphNode {
return new CustomParagraph();
}
export function $isCustomParagraphNode(node: ?LexicalNode): boolean {
return node instanceof CustomParagraph;
}
それぞれのNodeで拡張の例が載っている。
pluginで提供されているものをoverrideしたくなったときはconfigからできるらしい
const editorConfig = {
...
nodes=[
// Don't forget to register your custom node separately!
CustomParagraphNode,
{
replace: ParagraphNode,
with: (node: ParagraphNode) => {
return new CustomParagraphNode();
}
}
]
}
editor.registerUpdateListener(({editorState}) => {
// Read the editorState and maybe get some value.
editorState.read(() => {
// ...
});
// Then schedule another update.
editor.update(() => {
// ...
});
});
このパターンは余計なDOM更新をしているからやめろと
色んな種類のListenerがあるな
-
registerUpdateListener
: LexicalがDOMの変更をしたときの通知 -
registerTextContentListener
: DOMの変更をしてText Contentに差分があったときの通知 -
registerMutationListener
: 特定のNodeが created / destroyed / updated されたときの通知 -
registerEditableListener
:editor.setEditable(boolean)
が走ったときの通知 -
registerDecoratorListener
: decorator objectに変更があったときの通知 -
registerRootListener
: root要素に変更があったときの通知
For example: User types a character and you want to color the word blue if the word is now equal to "congrats". We programmatically add an @Mention to the editor, the @Mention is immediately next to another @Mention (@Mention@Mention). Since we believe this makes mentions hard to read, we want to destroy/replace both mentions and render them as plain TextNode's instead.
ん?話の流れが掴めない。例もめちゃくちゃじゃない?
const removeTransform = editor.registerNodeTransform(TextNode, (textNode) => {
if (textNode.getTextContent() === 'blue') {
textNode.setTextContent('green');
}
});
Transformはバッチ処理されるらしい。
updateListenerの中でupdateしても似たような結果にはなるけど、DOMの更新が2回行われてしまうのでやるなと。ハイコストだしHistoryに干渉する可能性があると。
editor.registerUpdateListener(() => {
editor.update(() => {
// Don't do this
});
});
Transforms are designed to run when nodes have been modified (aka marking nodes dirty). For the most part, transforms only need to run once after the update but the sequential nature of transforms makes it possible to have order bias. Hence, transforms are run over and over until this particular type of Node is no longer marked as dirty by any of the transforms.
Transformはどいつも反応しなくなるまで何周もするらしいな。
いい例が載ってる。
// Plugin 1
editor.registerNodeTransform(TextNode, textNode => {
// This transform runs twice but does nothing the first time because it doesn't meet the preconditions
if (textNode.getTextContent() === 'modified') {
textNode.setTextContent('re-modified');
}
})
// Plugin 2
editor.registerNodeTransform(TextNode, textNode => {
// This transform runs only once
if (textNode.getTextContent() === 'original') {
textNode.setTextContent('modified');
}
})
// App
editor.addListener('update', ({editorState}) => {
const text = editorState.read($textContent);
// text === 're-modified'
});
続きはちょっとよくわからんかった、
Selectionには4種類ある
RangeSelection
This is the most common type of selection, and is a normalization of the browser's DOM Selection and Range APIs.
カーソル引っ張ってできるあれ。
RangeSelection consists of three main properties:
anchor
representing a RangeSelection pointfocus
representing a RangeSelection pointformat
numeric bitwise flag, representing any active text formats
なぜanchorとfocusの2つあるんだろう。同じ内容ではない?
The main properties of a RangeSelection point are:
key
representing the NodeKey of the selected Lexical nodeoffset
representing the position from within its selected Lexical node. For the text type this is the character, and for the element type this is the child index from within the ElementNodetype
representing either element or text.
ElementNodeのときとTextNodeのときでだいぶ違うな
NodeSelection
NodeSelection represents a selection of multiple arbitrary nodes.
複数のNodeにまたがる選択。getNodes()
で内容を取れるらしい。
GridSelection
Tableっぽいものを選択するときの規格らしい。
For example, a table where you select row = 1 col = 1 to row 2 col = 2 could be stored as follows:
- gridKey = 2 table key
- anchor = 4 table cell (key may vary)
- focus = 10 table cell (key may vary)
どうしてfocusが10になるのかイマイチわからない
null
選択範囲が無い or Lexicalで取り扱えないものを選択している状態
Selectionは$getSelection()
でとってこれるらしい。これはオブジェクトがとれるのかな?
ここにあるな。ふつうに4種類のSelectionのいずれかだった。
ほんで大もとの型定義はここやわ。
むりくり選択範囲を作る例
import {$setSelection, $createRangeSelection, $createNodeSelection} from 'lexical';
editor.update(() => {
// Set a range selection
const rangeSelection = $createRangeSelection();
$setSelection(rangeSelection);
// You can also indirectly create a range selection, by calling some of the selection
// methods on Lexical nodes.
const someNode = $getNodeByKey(someKey);
// On element nodes, this will create a RangeSelection with type "element",
// referencing an offset relating to the child within the element.
// On text nodes, this will create a RangeSelection with type "text",
// referencing the text character offset.
someNode.select();
someNode.selectPrevious();
someNode.selectNext();
// On element nodes, you can use these.
someNode.selectStart();
someNode.selectEnd();
// Set a node selection
const nodeSelection = $createNodeSelection();
// Add a node key to the selection.
nodeSelection.add(someKey);
$setSelection(nodeSelection);
// You can also clear selection by setting it to `null`.
$setSelection(null);
});
Read mode / Edit modeがあるらしい
これで取る
const isEditable = editor.isEditable(); // Returns true or false
そもそもシリアライズってなんやねん、と思う
調べたら出てきた
結局Wikiの言葉に腹落ちしたらしい?
直列化(ちょくれつか)は、オブジェクト指向プログラミングにおいて使われる用語で、ある環境において存在しているオブジェクトを、バイト列やXMLフォーマットに変換することをいう。
インターネット言論の最大公約数ことChatGPTさんに聞いても同じような答えだった
シリアライズ (serialize) とは、IT用語の1つで、あるデータ構造やオブジェクトを、連続したバイト列などの形式に変換することを指します。
例えば、プログラム内で扱っているオブジェクトをファイルやネットワーク越しに別のプログラムやシステムに渡す場合、オブジェクトそのままでは扱えないことがあります。そのため、オブジェクトをシリアライズして、バイト列の形式に変換することで、別のプログラムやシステムでも扱えるようにします。
また、逆に、シリアライズされたバイト列を元のオブジェクトに戻すことをデシリアライズ (deserialize) と呼びます。データの永続化や通信などで広く使用されている概念です。
戻る。
we also offer generic utilities for converting Lexical -> HTML and HTML -> Lexical in our @lexical/html package.
HTML <=> Lexicalは提供されているらしい。Nodeごととかもできそう。
exportJSON()
/ importJSON()
ももちろんできる
It's important to note that you should avoid making breaking changes to existing fields in your JSON object, especially if backwards compatibility is an important part of your editor. That's why we recommend using a version field to separate the different changes in your node as you add or change functionality of custom nodes.
カスタムノードにはバージョニングしとけと。たしかに。
mdでexport/importできるかどうかはpluginのところで見る
For instance, you might want to to show a popover when a user mouses over a specific node or open a modal when they click on a node.
こういうときの、ネイティブのDOMイベントとの紐付けについて。
まずはrootListenerにアタッチしておくことが考えられる。たしかに。
function myListener(event) {
// You may want to filter on the event target here
// to only include clicks on certain types of DOM Nodes.
alert('Nice!');
}
const removeRootListener = editor.registerRootListener((rootElement, prevRootElement) => {
// add the listener to the current root element
rootElement.addEventListener('click', myListener);
// remove the listener from the old root element - make sure the ref to myListener
// is stable so the removal works and you avoid a memory leak.
prevRootElement.removeEventListener('click', myListener);
});
// teardown the listener - return this from your useEffect callback if you're using React.
removeRootListener();
特定のNodeに直接アタッチすることもできる。こっちのほうが普通な感じする。
const removeMutationListener = editor.registerMutationListener(nodeType, (mutations) => {
const registeredElements: WeakSet<HTMLElement> = new WeakSet();
editor.getEditorState().read(() => {
for (const [key, mutation] of mutations) {
const element: null | HTMLElement = editor.getElementByKey(key);
if (
// Updated might be a move, so that might mean a new DOM element
// is created. In this case, we need to add and event listener too.
(mutation === 'created' || mutation === 'updated') &&
element !== null &&
!registeredElements.has(element)
) {
registeredElements.add(element);
element.addEventListener('click', (event: Event) => {
alert('Nice!');
});
}
}
});
});
// teardown the listener - return this from your useEffect callback if you're using React.
removeMutationListener();
↑Reactだったらこれでできるらしい。あざす。
<LexicalComposer>
<NodeEventPlugin
nodeType={LinkNode}
eventType={'click'}
eventListener={(e: Event) => {
alert('Nice!');
}}
/>
</LexicalComposer>
Packages
めちゃあるな
core
基本のやーつ
clipboard
Nodeのコピペとかできるようになるんやろな、多分
code
code block対応
dragon
スピーチインターフェイスのなんかかな?ドキュメントが近日公開とのこと
file
file操作ねー、画像処理とかどうなるんやろ、要調査。
hashtag
hashtagを提供してくれるらしい。別にLexicalで管理せんくても良い気がするけど...。
あ、Instagramとかのあれか。
headless
DOMに依存するAPIを使わないならサーバー上でもこねこねできるようになるプラグイン。
history
History管理。ありがてー。
link
リンク処理。「内部リンクはNextLink」みたいなことできるといいな。できそう。
list
逆にこれは標準じゃないんか
markdown
headlessの項目でも見えたけどimport / export as md あるね。
おなじみのショートカット操作も使えるみたい。##
でh2になるとかね。
offset
This package contains selection offset helpers for Lexical.
ちょっとこれだけではまだわかりかねる
plain-text
基本的なコマンドリスナーたちらしい。
rich-text
リッチテキストがほしければこっち、いらなければplain-textの方を使え、みたいなことらしい
selection
Selectionに関するヘルパー関数群
table
Tableって表現の幅広げるからなー、欲しい
text
plain-textやrich-textとの関係性がよくわからない。
utils
何が入ってるんやろか