199 lines
5.5 KiB
Markdown
199 lines
5.5 KiB
Markdown
|
|
# `@workspace/lexical`
|
|||
|
|
|
|||
|
|
基于 Lexical 的声明式富文本编辑器。编辑器能力由
|
|||
|
|
`<LexicalActions />` 中出现的 action 组件决定;action 同时声明自己依赖的
|
|||
|
|
node、plugin 和 embed,`<LexicalRoot />` 会在创建 Composer 前自动收集并去重。
|
|||
|
|
|
|||
|
|
## 使用预设
|
|||
|
|
|
|||
|
|
`useDefaults` 提供 `minimal` 和 `full` 两套预设。使用预设时不再接收
|
|||
|
|
`children`,避免默认 action 与手动 action 的优先级不明确。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import {
|
|||
|
|
LexicalActions,
|
|||
|
|
LexicalBubbleToolbar,
|
|||
|
|
LexicalContent,
|
|||
|
|
LexicalFixedToolbar,
|
|||
|
|
LexicalFooter,
|
|||
|
|
LexicalRoot,
|
|||
|
|
} from "@workspace/lexical"
|
|||
|
|
|
|||
|
|
export function Editor() {
|
|||
|
|
return (
|
|||
|
|
<LexicalRoot value="" onChange={(html) => console.log(html)}>
|
|||
|
|
<LexicalActions useDefaults="full" />
|
|||
|
|
<LexicalFixedToolbar />
|
|||
|
|
<LexicalContent placeholder="开始输入…" />
|
|||
|
|
<LexicalBubbleToolbar />
|
|||
|
|
<LexicalFooter />
|
|||
|
|
</LexicalRoot>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`LexicalFixedToolbar`、`LexicalBubbleToolbar` 和 `LexicalFooter` 都是可选的。
|
|||
|
|
当对应区域没有 action,也没有自定义 children 时,组件不会产生 DOM。
|
|||
|
|
action、node 与 plugin 会在编辑器首次挂载时确定;需要切换整套能力时,应为
|
|||
|
|
`LexicalRoot` 提供新的 `key` 以重新创建编辑器。
|
|||
|
|
|
|||
|
|
## 国际化
|
|||
|
|
|
|||
|
|
内置控件会使用 `@workspace/i18n` 的当前语言;包提供英文与简体中文 catalog。
|
|||
|
|
应用应将对应 locale source 加入自己的 `i18n.config.json`:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"catalogSources": ["@workspace/lexical/locales/{locale}"]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
未置于 `I18nProvider` 内时,编辑器会回退到英文。外部 action 的 `label`
|
|||
|
|
仍可直接传字符串,也可以传 `{ id, message }` 消息描述符。
|
|||
|
|
|
|||
|
|
## 自定义 action 布局
|
|||
|
|
|
|||
|
|
action 默认显示在固定工具栏。通过 `in` 可以指定单个区域或多个区域;
|
|||
|
|
`ActionGroup` 可以只做普通组合,也可以渲染成下拉菜单。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import {
|
|||
|
|
ActionGroup,
|
|||
|
|
Bold,
|
|||
|
|
BulletList,
|
|||
|
|
CheckList,
|
|||
|
|
ClearFormatting,
|
|||
|
|
Date,
|
|||
|
|
Heading,
|
|||
|
|
LexicalActions,
|
|||
|
|
NormalText,
|
|||
|
|
OrderedList,
|
|||
|
|
Quote,
|
|||
|
|
Redo,
|
|||
|
|
Undo,
|
|||
|
|
} from "@workspace/lexical"
|
|||
|
|
|
|||
|
|
;<LexicalActions>
|
|||
|
|
<Undo />
|
|||
|
|
<Redo />
|
|||
|
|
|
|||
|
|
<ActionGroup type="menu" label="段落样式" showActiveAction>
|
|||
|
|
<NormalText />
|
|||
|
|
<Heading level={1} />
|
|||
|
|
<Heading level={2} />
|
|||
|
|
<Heading level={3} />
|
|||
|
|
<OrderedList />
|
|||
|
|
<BulletList />
|
|||
|
|
<CheckList />
|
|||
|
|
<Quote />
|
|||
|
|
</ActionGroup>
|
|||
|
|
|
|||
|
|
<Bold in={["toolbar", "bubble"]} />
|
|||
|
|
<Date in="bubble" />
|
|||
|
|
<ClearFormatting in="footer" />
|
|||
|
|
</LexicalActions>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 定义扩展 action
|
|||
|
|
|
|||
|
|
外部 action 把行为和 Composer 依赖放在同一份定义中,不需要额外注册
|
|||
|
|
feature。只要 action 出现在 `LexicalActions` 中,它声明的 node 和 plugin
|
|||
|
|
就会自动启用。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import {
|
|||
|
|
defineLexicalAction,
|
|||
|
|
type LexicalActionDefinition,
|
|||
|
|
} from "@workspace/lexical"
|
|||
|
|
|
|||
|
|
const mentionAction: LexicalActionDefinition = {
|
|||
|
|
name: "mention",
|
|||
|
|
label: "插入提及",
|
|||
|
|
nodes: [MentionNode],
|
|||
|
|
plugins: [MentionPopoverPlugin],
|
|||
|
|
execute: ({ editor }) => openMentionPicker(editor),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const Mention = defineLexicalAction(mentionAction)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
action 组件可以用 render function 替换默认控件,但依赖收集仍由同一个
|
|||
|
|
action 完成:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
<Mention>
|
|||
|
|
{({ disabled, execute }) => (
|
|||
|
|
<CustomMentionButton
|
|||
|
|
disabled={disabled}
|
|||
|
|
onSelect={(mention) => execute(mention)}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</Mention>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
render context 中的 `execute(value?)` 会调用该 action,并保留 value 类型。
|
|||
|
|
`onClick` 是 `execute()` 的无参快捷方式,适合普通按钮。
|
|||
|
|
|
|||
|
|
图片和视频 action 在不传 value 时继续使用内置输入弹窗;自定义上传器可以
|
|||
|
|
在上传结束后直接把结果交给 `execute`,不需要操作 Lexical editor:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
<Image>
|
|||
|
|
{({ execute }) => (
|
|||
|
|
<ImageUploader
|
|||
|
|
onUploaded={({ src, alt, caption }) =>
|
|||
|
|
execute({ src, alt, caption })
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</Image>
|
|||
|
|
|
|||
|
|
<Video>
|
|||
|
|
{({ execute }) => (
|
|||
|
|
<VideoUploader
|
|||
|
|
onUploaded={({ src, poster, caption }) =>
|
|||
|
|
execute({ src, poster, caption })
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</Video>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 粘贴图片
|
|||
|
|
|
|||
|
|
`full` 预设已包含 `<ClipboardImages />`:直接粘贴截图或拖入图片文件时,
|
|||
|
|
会立即插入本地预览;默认完成后把图片编码成可序列化的 `data:` URL。
|
|||
|
|
生产环境通常更适合传入 `resolveImage`,先把图片上传到对象存储,再返回
|
|||
|
|
持久 URL。上传期间可通过 `reportProgress` 汇报 `0` 到 `1` 的进度;不汇报
|
|||
|
|
时编辑器会显示不确定进度:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import { ClipboardImages, LexicalActions } from "@workspace/lexical"
|
|||
|
|
|
|||
|
|
;<LexicalActions>
|
|||
|
|
{/* 其他 action */}
|
|||
|
|
<ClipboardImages
|
|||
|
|
resolveImage={async (file, { reportProgress, signal }) => {
|
|||
|
|
const uploaded = await uploadImage(file, {
|
|||
|
|
signal,
|
|||
|
|
onProgress: reportProgress,
|
|||
|
|
})
|
|||
|
|
return {
|
|||
|
|
alt: file.name,
|
|||
|
|
src: uploaded.url,
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
onError={(error, file) => reportUploadError(error, file)}
|
|||
|
|
/>
|
|||
|
|
</LexicalActions>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
非图片文件不会被编辑器接管,粘贴仍由浏览器处理。图片被选中后可以通过
|
|||
|
|
控制点缩放、添加或修改说明、切换左/中/右对齐,也可以使用可见删除按钮、
|
|||
|
|
Delete 或 Backspace 删除。该能力同时复用 Lexical 的文件拖放命令,因此
|
|||
|
|
相同 resolver 也适用于拖入编辑器的图片。
|
|||
|
|
|
|||
|
|
没有可见控件、只提供编辑行为的能力也可以声明为 `hidden` action。包内的
|
|||
|
|
`<DraggableBlocks />` 就使用这种方式,因此它会启用拖拽 plugin,但不会占据
|
|||
|
|
任何工具栏位置。
|