Files
simple-react-app-kit/packages/lexical
Maofeng 263b400860 feat(lexical): support local image data URLs
Allow the image insertion dialog to select a local image and embed it as a Base64 data URL, while retaining URL-based insertion and leaving video URL-only.

Extract shared abortable FileReader handling for clipboard uploads and dialog selection; add English and Simplified Chinese messages plus an interaction test covering file selection through insertion.
2026-07-31 16:03:54 +08:00
..

@workspace/lexical

基于 Lexical 的声明式富文本编辑器。编辑器能力由 <LexicalActions /> 中出现的 action 组件决定;action 同时声明自己依赖的 node、plugin 和 embed<LexicalRoot /> 会在创建 Composer 前自动收集并去重。

使用预设

useDefaults 提供 minimalfull 两套预设。使用预设时不再接收 children,避免默认 action 与手动 action 的优先级不明确。

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>
  )
}

LexicalFixedToolbarLexicalBubbleToolbarLexicalFooter 都是可选的。 当对应区域没有 action,也没有自定义 children 时,组件不会产生 DOM。 action、node 与 plugin 会在编辑器首次挂载时确定;需要切换整套能力时,应为 LexicalRoot 提供新的 key 以重新创建编辑器。

国际化

内置控件会使用 @workspace/i18n 的当前语言;包提供英文与简体中文 catalog。 应用应将对应 locale source 加入自己的 i18n.config.json

{
  "catalogSources": ["@workspace/lexical/locales/{locale}"]
}

未置于 I18nProvider 内时,编辑器会回退到英文。外部 action 的 label 仍可直接传字符串,也可以传 { id, message } 消息描述符。

自定义 action 布局

action 默认显示在固定工具栏。通过 in 可以指定单个区域或多个区域; ActionGroup 可以只做普通组合,也可以渲染成下拉菜单。

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 就会自动启用。

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 完成:

<Mention>
  {({ disabled, execute }) => (
    <CustomMentionButton
      disabled={disabled}
      onSelect={(mention) => execute(mention)}
    />
  )}
</Mention>

render context 中的 execute(value?) 会调用该 action,并保留 value 类型。 onClickexecute() 的无参快捷方式,适合普通按钮。

图片和视频 action 在不传 value 时继续使用内置输入弹窗;自定义上传器可以 在上传结束后直接把结果交给 execute,不需要操作 Lexical editor

<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 汇报 01 的进度;不汇报 时编辑器会显示不确定进度:

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,但不会占据 任何工具栏位置。