Lexical 触ってみるぞ
公式ドキュメントはこの回で大体読んだので、実際に触ってみる
Setup
Next.js appDirにて。Storybookの動作とかも見たいのでいろいろはいってるテンプレートを使う。
npm install
ののちに、READMEの"an example of a basic plain text editor"とやらを適当なファイルに貼り付け。appDirで使うので"use client"を書く。
"use client";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
import { $getRoot, $getSelection, EditorState } from "lexical";
import { useEffect } from "react";
const theme = {
// Theme styling goes here
// ...
};
// When the editor changes, you can get notified via the
// LexicalOnChangePlugin!
function onChange(editorState: 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: any) {
console.error(error);
}
const initialConfig = {
namespace: "MyEditor",
theme,
onError,
};
export function Editor() {
return (
<LexicalComposer initialConfig={initialConfig}>
<PlainTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>Enter some text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin onChange={onChange} />
<HistoryPlugin />
<MyCustomAutoFocusPlugin />
</LexicalComposer>
);
}
こんな感じ。helper textが下にあるのが紛らわしいけど一応動く。
Theme切り替えもいけるな...。文字色の設定をどこかでした覚えはないのだが、
こういうコードで
<div className="py-20 px-6 max-w-2xl mx-auto">
<ThemeSwitcher />
<Editor />
<p>こんにちは</p>
</div>
);
こうなったから
たぶんTailwindが勝手にglobalでやっているのだと思う。
たぶんてなんだよ、ちゃんと調べろ
たぶん入れてるpluginがdefaultのstyleを当てている...
たぶんの域を出ることができなかった。本題じゃないので戻るぞ。
ReloadしたらLayout shift起きてるな
"hello world"と入力してworldを選択した状態のRootNodeとSelection
PlainTextPlugin
の代わりにRichTextPlugin
にしたら⌘Bで太字ができるようになった
HistoryPlugin
が入っているからか、⌘Zで太字を戻すことができる
AutoFocusPlugin
を入れてAutoFocusをげっちゅ
mdを追加しようと思って以下のようにした。
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
するとerrorが出る。
Error: MarkdownShortcuts: missing dependency for transformer. Ensure node dependency is included in editor initial config.
なんかnode
に色々書かなきゃらしい?
このnodes
をinitialConfig
にわたすことで解決した。import散らばっててややこしすぎる。
import { CodeNode, CodeHighlightNode } from "@lexical/code";
import { AutoLinkNode, LinkNode } from "@lexical/link";
import { ListNode, ListItemNode } from "@lexical/list";
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
import { Klass, LexicalNode } from "lexical";
export const nodes: Array<Klass<LexicalNode>> = [
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
CodeHighlightNode,
AutoLinkNode,
LinkNode,
];
適当なglobal styleを当てるとmd editorな書き方でこんなふうにできた。
公式ドキュメントにPluginの情報なさすぎて野良を探している。みんなどうやって実装してるの?
ContentEditable
にclassName
を渡すことができた。
<RichTextPlugin
contentEditable={<ContentEditable className="p-4 min-h-[30rem]" />}
placeholder={<div>Enter some text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
適当なwraperをComposerの中に置いて、placeholderのstyleをいじったらいい感じの配置になった。
<LexicalComposer initialConfig={initialConfig}>
<div className="relative">
<RichTextPlugin
contentEditable={<ContentEditable className="p-6 min-h-[30rem]" />}
placeholder={
<div className="absolute pt-[2px] pl-1 top-6 left-6 pointer-events-none select-none">
Enter some text...
</div>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<OnChangePlugin onChange={onChange} />
<HistoryPlugin />
<AutoFocusPlugin />
<MyCustomAutoFocusPlugin />
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
</div>
</LexicalComposer>
style をglobalに当ててたけど、themeに移すことができた。
import { EditorThemeClasses } from "lexical";
export const theme: EditorThemeClasses = {
heading: {
h1: "scroll-m-20 mt-12 mb-6 text-4xl font-extrabold tracking-tight lg:text-5xl [&:nth-child(1)]:mt-0",
h2: "scroll-m-20 border-b mt-8 mb-2 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0",
h3: "scroll-m-20 my-4 text-2xl font-semibold tracking-tight",
h4: "scroll-m-20 my-3 text-xl font-semibold tracking-tight",
},
paragraph: "leading-7 [&:not(:first-child)]:mt-6",
quote: "mt-6 border-l-2 pl-6",
list: {
ul: "my-6 ml-6 list-disc [&>li]:mt-2",
ol: "my-6 ml-6 list-decimal [&>li]:mt-2",
},
code: "relative rounded bg-sage-3 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
};
あ、MyCustomAutoFocusPlugin
が書かれていたので消した。
import文をなるべく整理したいので、1つのpluginに対して複数importあるやつはファイルを分割してもいいかもしれない。
import { TRANSFORMERS } from "@lexical/markdown";
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
export const MarkdownPlugin = () => (
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
);
賛否分かれそう
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import { RichTextPlugin as RichTextPluginPrimitive } from "@lexical/react/LexicalRichTextPlugin";
export const RichTextPlugin = () => (
<RichTextPluginPrimitive
contentEditable={<ContentEditable className="p-6 min-h-[30rem]" />}
placeholder={
<div className="absolute pt-[2px] pl-1 top-6 left-6 pointer-events-none select-none">
Enter some text...
</div>
}
ErrorBoundary={LexicalErrorBoundary}
/>
);
だいぶ整理されてはいる
"use client";
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
import { $getRoot, $getSelection, EditorState } from "lexical";
import { nodes } from "./node";
import { MarkdownPlugin } from "./plugins/markdown";
import { RichTextPlugin } from "./plugins/richtext";
import { theme } from "./theme";
// When the editor changes, you can get notified via the
// LexicalOnChangePlugin!
function onChange(editorState: EditorState) {
editorState.read(() => {
// Read the contents of the EditorState here.
const root = $getRoot();
const selection = $getSelection();
console.log(root, selection);
});
}
const initialConfig = {
namespace: "MyEditor",
theme,
onError: (error: any) => console.error(error),
nodes,
};
export function Editor() {
return (
<LexicalComposer initialConfig={initialConfig}>
<div className="relative">
<RichTextPlugin />
<OnChangePlugin onChange={onChange} />
<HistoryPlugin />
<AutoFocusPlugin />
<MarkdownPlugin />
</div>
</LexicalComposer>
);
}
List/CheckListがあったので入れた。
<ListPlugin />
<CheckListPlugin />
Tab/Shift Tabでネストはまだできない。
リストに文字を入力せず改行することでリストから抜けることはできる。
リストを触ってるとスタイルが気になり始めたので微調整した。
import { EditorThemeClasses } from "lexical";
export const theme: EditorThemeClasses = {
heading: {
h1: "scroll-m-20 mt-12 mb-6 text-4xl font-extrabold tracking-tight lg:text-5xl [&:nth-child(1)]:mt-0",
h2: "scroll-m-20 border-b mt-8 mb-2 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0",
h3: "scroll-m-20 my-4 text-2xl font-semibold tracking-tight",
h4: "scroll-m-20 my-3 text-xl font-semibold tracking-tight",
},
paragraph: "leading-7 [&:not(:first-child)]:mt-2",
quote: "mt-2 border-l-2 pl-6",
list: {
ul: "my-2 ml-6 list-disc [&>li]:mt-2 [&:first-child]:mt-0",
ol: "my-2 ml-6 list-decimal [&>li]:mt-2 [&:first-child]:mt-0",
listitem: "[&:first-child]:mt-0",
},
code: "relative rounded bg-sage-3 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
};
checkboxも作れないな
checkListはこの表現に当てはまる表現をしたら出てくるはずなんだけどな
一旦わからん
- Tabを押すとlistのnestではなく、他の要素へfocusが移動してしまう
- CheckListが出てこない
CodeHighlight
すてぃんさんの記事を読みながら。
公式Plugin用意されてないらしい?ありがたく記事内容を貼らせてもらう。
import { FC, useEffect } from "react";
import { registerCodeHighlighting } from "@lexical/code";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
export const CodeHighlightPlugin: FC = () => {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return registerCodeHighlighting(editor);
}, [editor]);
return null;
};
まてよ、そもそもcodeのスタイルがびみょいぞ
themeはちゃんとしている気がするのだが
code: "relative rounded bg-sage-3 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
tailwind直スタイリングはいけるな
code {
@apply relative rounded bg-sage-3 text-green-9 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold;
}
とりあえず例に習ってlexicalのコード真似してみたけどうまくスタイリングされてない。
.code {
background-color: rgb(240, 242, 245);
font-family: Menlo, Consolas, Monaco, monospace;
display: block;
padding: 8px 8px 8px 52px;
line-height: 1.53;
font-size: 13px;
margin: 0;
margin-top: 8px;
margin-bottom: 8px;
tab-size: 2;
/* white-space: pre; */
overflow-x: auto;
position: relative;
}
.code:before {
content: attr(data-gutter);
position: absolute;
background-color: #eee;
left: 0;
top: 0;
border-right: 1px solid #ccc;
padding: 8px;
color: #777;
white-space: pre-wrap;
text-align: right;
min-width: 25px;
}
.tokenComment {
color: slategray;
}
.tokenPunctuation {
color: #999;
}
.tokenProperty {
color: #905;
}
.tokenSelector {
color: #690;
}
.tokenOperator {
color: #9a6e3a;
}
.tokenAttr {
color: #07a;
}
.tokenVariable {
color: #e90;
}
.tokenFunction {
color: #dd4a68;
}
code: styles.code,
codeHighlight: {
atrule: styles.tokenAttr,
attr: styles.tokenAttr,
boolean: styles.tokenProperty,
builtin: styles.tokenSelector,
cdata: styles.tokenComment,
char: styles.tokenSelector,
class: styles.tokenFunction,
"class-name": styles.tokenFunction,
comment: styles.tokenComment,
constant: styles.tokenProperty,
deleted: styles.tokenProperty,
doctype: styles.tokenComment,
entity: styles.tokenOperator,
function: styles.tokenFunction,
important: styles.tokenVariable,
inserted: styles.tokenSelector,
keyword: styles.tokenAttr,
namespace: styles.tokenVariable,
number: styles.tokenProperty,
operator: styles.tokenOperator,
prolog: styles.tokenComment,
property: styles.tokenProperty,
punctuation: styles.tokenPunctuation,
regex: styles.tokenVariable,
selector: styles.tokenSelector,
string: styles.tokenSelector,
symbol: styles.tokenProperty,
tag: styles.tokenProperty,
url: styles.tokenOperator,
variable: styles.tokenVariable,
「```」でコードブロック出現も今の所ないな
こいつはごつそうなので別スクラップに回す
あ、適当にコードブロック貼り付けたら出たわ
inlineコードだけイマイチ
inline codeのstyleはこっちでした
text: {
code: "relative rounded bg-sage-3 text-green-9 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold;",
},
AutoLinkを導入する
import { AutoLinkPlugin as AutoLinkPluginPrimitive } from "@lexical/react/LexicalAutoLinkPlugin";
const URL_MATCHER =
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
const EMAIL_MATCHER =
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
const MATCHERS = [
(text: string) => {
const match = URL_MATCHER.exec(text);
if (match === null) {
return null;
}
const fullMatch = match[0];
return {
index: match.index,
length: fullMatch.length,
text: fullMatch,
url: fullMatch.startsWith("http") ? fullMatch : `https://${fullMatch}`,
};
},
(text: string) => {
const match = EMAIL_MATCHER.exec(text);
return (
match && {
index: match.index,
length: match[0].length,
text: match[0],
url: `mailto:${match[0]}`,
}
);
},
];
export const AutoLinkPlugin = () => (
<AutoLinkPluginPrimitive matchers={MATCHERS} />
);
a tagになってるけどクリックしてジャンプできない
別スクラップに分割してやりたいこと一覧
- lexical-playgrounのコードリーディングしてPlugin探求
- 理想のlink体験
- リロードしてもテキストが残るようにStateの永続化
- syntax highlightを全部Tailwindで書いてみる
- Notionみたいに
/
を押したらコマンドパレット出現 - 美しいTypographyのスタイリングを学ぶためにNotion Typography分析