diff --git a/.prettierignore b/.prettierignore index bba0877552..dacd7e9d0f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,7 +5,6 @@ **/index.ts **/*.mdx **/__tests__/**/*.html -**/.contentlayer **/__registry__ **/og apps/www/src/app/globals.css \ No newline at end of file diff --git a/apps/www/content/blogs/affinity.cn.mdx b/apps/www/content/blogs/affinity.cn.mdx new file mode 100644 index 0000000000..5b025c2c10 --- /dev/null +++ b/apps/www/content/blogs/affinity.cn.mdx @@ -0,0 +1,98 @@ +--- +title: 精准掌控光标行为:用 Plate.js 解决样式边界跳跃问题 +description: 传统富文本编辑器在样式边界的光标移动上体验不佳,Plate.js 仅用一行配置即可带来更自然、可控的输入体验。本文详解其原理与用法。 +date: 2025-06-12 +author: Ziad Beyens +published: true +badgeLink: + href: "/docs/plugin-rules#rulesselection" + text: "查看文档" +--- + +## 前言 + +在下面字母x的右侧按下 ←,光标会直接跳进下方的代码块,因为两者之间 只有一个可停留的位置。 + +这带来了一个很实际的问题: + +> 我只是想在 code 的后面补充一些普通文本,结果光标跳进了 code,输入的内容却自动套上了代码样式。 + +用户并没有明确意图进入代码块,但编辑器却强行切换了上下文,导致写作体验割裂。 + +更令人意外的是:即便是像 Notion 或 Google Docs 这样的顶级编辑器,也没有很好地解决这个问题 + +代码块光标亲和力演示 + +类似的问题也存在于 加粗(bold)文本中。 + +当我的光标落在加粗和普通文本之间的边界时,编辑器该如何判断: + +我接下来要输入的是加粗的内容,还是普通样式的内容? + +大多数编辑器的处理是:继承左侧的样式,即便用户希望输入的是普通文本。 + +加粗文本光标亲和力演示 + +## 解决方案 + +不过在 plate v49 中,只需要一行配置,你就可以彻底解决光标在块级元素边界"跳进跳出不可控"的问题: + +```tsx +createSlatePlugin({ + rules: { selection: { affinity: 'hard' } }, +}) +``` + +这样设置之后,当你使用方向键在 code 标记(如 const a = 1;)周围移动光标时,系统会明确区分: + +从外部移入 → 第一次停在边缘; +再按一次 → 才进入 code 内部。 + +这就像是给光标加了一道"缓冲层",避免误触样式,让输入变得更精准、更符合预期。 + +如下图所示,code 的左右两侧各有一个独立的光标位置,不再是传统编辑器中的"边界即跳转"。 + +光标亲和力演示 + +### Affinity: 'directional' + +不过回到 加粗文本(bold) 的情况,事情又有点不同。 + +由于 bold 的左右两边 没有任何 padding,当你光标靠近边界时,第一下箭头其实已经起作用了,但用户看不到任何视觉反馈,这就造成了一种错觉: + +"我按下了左箭头,但光标好像没动?" + +这也意味着,如果我们在 bold 上使用 affinity: 'hard',反而会让用户觉得键盘"失灵"了。 + +为了解决这个问题,Plate.js 提供了另一种策略 还是一行代码: + +```ts +rules: { selection: { affinity: 'directional' } }, +``` + +使用 affinity: 'directional' 后,光标行为将根据移动方向智能判断: + +* 从右往左离开 text → 新输入会继承 普通样式; +* 从左往右离开 bold → 输入将是bold样式。 + +光标亲和力演示 + +这个策略利用了用户的操作意图,让输入行为更加自然和可预测,同时避免了视觉"卡顿"。 + +不过默认我们只为 Link 开启了 directional 策略,详情见 [Rules](https://platejs.org/docs/plugin-rules#rulesselection) + +## 最后 + +最重要的是: +这一切的控制权,完全掌握在你手里。 + +无论是 bold、italic、code,还是 link—— +你都可以为每一个样式(Mark),甚至每一个内联元素,指定最适合的光标行为策略。 + +是选择 hard,让光标拥有明确的边界感? +还是选择 directional,根据方向智能判断输入样式? +还是干脆保持默认行为,沿用编辑器的标准策略? + +选择权在你手上。每一种策略,都只需一行配置即可启用。 + +Plate.js 给你的不只是功能,而是掌控感。 diff --git a/apps/www/content/blogs/affinity.mdx b/apps/www/content/blogs/affinity.mdx new file mode 100644 index 0000000000..64b0617b4f --- /dev/null +++ b/apps/www/content/blogs/affinity.mdx @@ -0,0 +1,98 @@ +--- +title: Precise Cursor Control - Solving Style Boundary Jumping with Plate.js +description: Traditional rich text editors often provide a poor experience when moving the cursor across style boundaries. With just one line of configuration, Plate.js delivers a more natural and controllable input experience. This article explains the principles and usage in detail. +date: 2025-06-12 +author: Ziad Beyens +published: true +badgeLink: + href: "/docs/plugin-rules#rulesselection" + text: "View Docs" +--- + +## Introduction + +When you press ← to the right of the letter x below, the cursor jumps directly into the code block below, because there is only one position where the cursor can stop between them. + +This brings up a very practical problem: + +> I just want to add some plain text after the code, but the cursor jumps into the code, and what I type is automatically styled as code. + +The user did not intend to enter the code block, but the editor forcibly switched the context, resulting in a fragmented writing experience. + +Surprisingly, even top editors like Notion or Google Docs haven't solved this problem well. + +Code block cursor affinity demo + +A similar issue exists with bold text. + +When my cursor is at the boundary between bold and normal text, how should the editor determine: + +Will I type bold content next, or plain styled content? + +Most editors handle this by inheriting the style from the left, even if the user wants to type plain text. + +Bold text cursor affinity demo + +## Solution + +But in Plate v49, you can completely solve the uncontrollable "jumping in and out" of the cursor at block element boundaries with just one line of configuration: + +```tsx +createSlatePlugin({ + rules: { selection: { affinity: 'hard' } }, +}) +``` + +With this setting, when you use the arrow keys around a code mark (like `const a = 1;`), the system clearly distinguishes: + +Entering from outside → the first time, the cursor stops at the edge; +Press again → only then does it enter the code. + +It's like adding a "buffer layer" to the cursor, preventing accidental style switches and making input more precise and predictable. + +As shown below, there is a separate cursor position on each side of the code, no longer the "boundary means jump" behavior of traditional editors. + +Cursor affinity demo + +### Affinity: 'directional' + +But when it comes to bold text, things are a bit different. + +Since there is no padding on either side of bold, when your cursor approaches the boundary, the first arrow key press actually takes effect, but the user sees no visual feedback, creating the illusion: + +"I pressed the left arrow, but the cursor didn't move?" + +This means that if we use affinity: 'hard' on bold, it may make users feel like the keyboard is "unresponsive". + +To solve this, Plate.js provides another strategy—still just one line of code: + +```ts +rules: { selection: { affinity: 'directional' } }, +``` + +With affinity: 'directional', cursor behavior is intelligently determined by movement direction: + +* Move left out of text → new input will inherit plain style; +* Move right out of bold → input will be bold style. + +Cursor affinity demo + +This strategy leverages user intent, making input behavior more natural and predictable, while avoiding visual "stuttering". + +By default, we only enable the directional strategy for Link. See [Rules](https://platejs.org/docs/plugin-rules#rulesselection) for details. + +## Conclusion + +Most importantly: +You have full control over all of this. + +Whether it's bold, italic, code, or link— +You can specify the most suitable cursor behavior strategy for each style (Mark), or even for each inline element. + +Choose 'hard' for clear boundary feeling? +Or 'directional' for smart style selection based on direction? +Or just keep the default, using the editor's standard strategy? + +The choice is yours. Each strategy can be enabled with just one line of configuration. + +Plate.js gives you not just features, but a sense of control. diff --git a/apps/www/content/blogs/mutiple-nodes-dnd.cn.mdx b/apps/www/content/blogs/mutiple-nodes-dnd.cn.mdx new file mode 100644 index 0000000000..6444b65179 --- /dev/null +++ b/apps/www/content/blogs/mutiple-nodes-dnd.cn.mdx @@ -0,0 +1,50 @@ +--- +title: 拖拽体验全面升级:@platejs/dnd@49.1.7 新特性详解 +description: "@platejs/dnd@49.1.7 版本带来了多块拖拽和拖拽体验的显著提升。本文将详细解析本次更新的核心改动,并探讨后续可优化的方向。" +date: 2025-07-14 +author: Felix Feng +published: true +badgeLink: + href: "/docs/dnd" + text: "View Docs" +--- + +## 拖拽体验全面升级 + +> 本次更新新增多块同时拖拽,并修复了 margin-top 区域无法放置的问题,显著提升了拖拽体验。 + +### 1. 支持多块同时拖拽 + +你可以通过以下**两种方式**选中多个块: + +- **方式一:使用 block-selection** + 通过 block-selection 功能,选中多个块后一起拖动: + + ![block-selection](/blog/multiple-nodes-dnd/1.gif) + +- **方式二:利用编辑器原生 selection** + 直接用编辑器的原生 selection 选中多块内容,再一次性拖拽: + + ![selection](/blog/multiple-nodes-dnd/2.gif) + +--- + +### 2. 修复 margin-top 区域无法 drop 的问题 + +- 此前,当目标块上方存在 `margin-top` 时,用户无法在该区域完成 drop 操作,影响了拖拽的流畅性: + + ![issue](/blog/multiple-nodes-dnd/3.gif) + +- 在新版本中,这一问题已**被修复**。现在,drop 区域能够正确识别 margin,用户可以在 margin-top 区域顺利完成拖拽和插入,体验更加自然: + + ![fixed issue](/blog/multiple-nodes-dnd/4.gif) + + + +## 展望 + +本次更新,让整体拖拽体验更加顺滑自然。但要实现“随心所欲”的拖拽,还需进一步完善: + +- 支持在编辑器外 drop +- 优化 hover 表格边框时 drop line 的位置 + diff --git a/apps/www/content/blogs/mutiple-nodes-dnd.mdx b/apps/www/content/blogs/mutiple-nodes-dnd.mdx new file mode 100644 index 0000000000..00cd74252a --- /dev/null +++ b/apps/www/content/blogs/mutiple-nodes-dnd.mdx @@ -0,0 +1,49 @@ +--- +title: Comprehensive Drag-and-Drop Upgrade - @platejs/dnd@49.1.7 New Features Explained +description: "@platejs/dnd@49.1.7 introduces significant improvements to multi-block drag-and-drop and overall drag-and-drop experience. This article details the core changes in this update and discusses potential future enhancements." +date: 2025-07-14 +author: Felix Feng +published: true +badgeLink: + href: "/docs/dnd" + text: "View Docs" +--- + +## Comprehensive Drag-and-Drop Upgrade + +> This update adds multi-block drag-and-drop and fixes the issue where dropping was not possible in the margin-top area, greatly enhancing the drag-and-drop experience. + +### 1. Multi-block Drag-and-Drop Support + +You can select multiple blocks using **two methods**: + +- **Method 1: Using block-selection** + Use the block-selection feature to select multiple blocks and drag them together: + + ![block-selection](/blog/multiple-nodes-dnd/1.gif) + +- **Method 2: Using the editor's native selection** + Directly use the editor's native selection to select multiple blocks and drag them all at once: + + ![selection](/blog/multiple-nodes-dnd/2.gif) + +--- + +### 2. Fixed the Issue with Dropping in margin-top Areas + +- Previously, when there was a `margin-top` above the target block, users could not drop in that area, which affected the smoothness of the drag-and-drop experience: + + ![issue](/blog/multiple-nodes-dnd/3.gif) + +- In the new version, this issue has **been fixed**. Now, the drop area correctly recognizes margins, allowing users to drag and insert blocks smoothly in the margin-top area for a more natural experience: + + ![fixed issue](/blog/multiple-nodes-dnd/4.gif) + + + +## Looking Ahead + +This update makes the overall drag-and-drop experience smoother and more natural. However, to achieve truly "freeform" drag-and-drop, further improvements are needed: + +- Support dropping outside the editor +- Optimize the position of the drop line when hovering over table borders diff --git a/apps/www/content/docs/(guides)/controlled.cn.mdx b/apps/www/content/docs/(guides)/controlled.cn.mdx new file mode 100644 index 0000000000..0e0473e89e --- /dev/null +++ b/apps/www/content/docs/(guides)/controlled.cn.mdx @@ -0,0 +1,66 @@ +--- +title: 受控编辑器值 +description: 如何控制编辑器的值。 +--- + +在 Plate 中实现完全受控的编辑器值较为复杂,原因如下: + +1. 编辑器状态不仅包含内容 (`editor.children`),还包含 `editor.selection` 和 `editor.history`。 + +2. 直接替换 `editor.children` 可能会破坏选区(selection)和历史记录(history),导致意外行为或崩溃。 + +3. 所有对编辑器值的更改都应通过 [Transforms](https://docs.slatejs.org/api/transforms) 进行,以保持与选区(selection)和历史记录(history)的一致性。 + +鉴于这些挑战,通常建议将 Plate 作为非受控输入使用。但如果需要从外部更改编辑器内容,可以使用 `editor.tf.setValue(value)` 函数。 + + + 使用 `editor.tf.setValue` 会在每次调用时重新渲染所有节点,因此应谨慎使用。 + 如果频繁调用或处理大型文档,可能会影响性能。 + + +或者,您可以使用 `editor.tf.reset()` 来重置编辑器状态,这将同时重置选区(selection)和历史记录(history)。 + +### 异步初始值 + +您可以使用 `skipInitialization` 延迟编辑器初始化,直到异步数据准备就绪。然后调用 `editor.tf.init` 并传入您的值: + +```tsx +function AsyncControlledEditor() { + const [initialValue, setInitialValue] = React.useState(); + const [loading, setLoading] = React.useState(true); + const editor = usePlateEditor({ + skipInitialization: true, + }); + + React.useEffect(() => { + // 模拟异步获取 + setTimeout(() => { + setInitialValue([ + { + type: 'p', + children: [{ text: '已加载异步值!' }], + }, + ]); + setLoading(false); + }, 1000); + }, []); + + React.useEffect(() => { + if (!loading && initialValue) { + editor.tf.init({ value: initialValue, autoSelect: 'end' }); + } + }, [loading, initialValue, editor]); + + if (loading) return
加载中…
; + + return ( + + + + + + ); +} +``` + + \ No newline at end of file diff --git a/apps/www/content/docs/(guides)/controlled.mdx b/apps/www/content/docs/(guides)/controlled.mdx new file mode 100644 index 0000000000..ffefbb8ad1 --- /dev/null +++ b/apps/www/content/docs/(guides)/controlled.mdx @@ -0,0 +1,90 @@ +--- +title: Controlled Editor Value +description: How to control the editor value. +--- + +Implementing a fully controlled editor value in Plate is complex due to several factors: + +1. The editor state includes more than just the content (`editor.children`). It also includes `editor.selection` and `editor.history`. + +2. Directly replacing `editor.children` can break the selection and history, leading to unexpected behavior or crashes. + +3. All changes to the editor's value should ideally happen through [Transforms](https://docs.slatejs.org/api/transforms) to maintain consistency with selection and history. + +Given these challenges, it's generally recommended to use Plate as an uncontrolled input. However, if you need to make external changes to the editor's content, you can use `editor.tf.setValue(value)` function. + + + Using `editor.tf.setValue` will re-render all nodes on each call, so it + should be used carefully and sparingly. It may impact performance if used + frequently or with large documents. + + +Alternatively, you can use `editor.tf.reset()` to reset the editor state, which will reset the selection and history. + +### Async Initial Value + +You can pass an async function directly to the `value` option. The editor will handle the async loading automatically: + +```tsx +function AsyncControlledEditor() { + const editor = usePlateEditor({ + value: async () => { + // Simulate fetching data from an API + const response = await fetch('/api/document'); + const data = await response.json(); + return data.content; + }, + autoSelect: 'end', + }); + + return ( + + + + + + ); +} +``` + +For more control over initialization timing, you can use `shouldInitialize: false` and manually call `editor.tf.init`: + +```tsx +function AsyncControlledEditor() { + const [data, setData] = React.useState(null); + const editor = usePlateEditor({ + shouldInitialize: false, + }); + + React.useEffect(() => { + const loadData = async () => { + const response = await fetch('/api/document'); + const data = await response.json(); + setData(data); + + editor.tf.init({ + value: data.content, + autoSelect: 'end', + onReady: ({ editor, value }) => { + console.info('Editor ready with value:', value); + }, + }); + }; + + loadData(); + }, [editor]); + + if (!data) return
Loading…
; + + return ( + + + + + + ); +} +``` + + + diff --git a/apps/www/content/docs/(guides)/debugging.cn.mdx b/apps/www/content/docs/(guides)/debugging.cn.mdx new file mode 100644 index 0000000000..2f8720e2ed --- /dev/null +++ b/apps/www/content/docs/(guides)/debugging.cn.mdx @@ -0,0 +1,212 @@ +--- +title: 调试 +description: Plate 中的调试。 +--- + +## 使用 DebugPlugin + +当你创建 Plate 编辑器时,`DebugPlugin` 会自动包含在内。你可以通过编辑器的 API 访问其方法: + +```ts +const editor = createPlateEditor({ + plugins: [/* your plugins */], +}); + +editor.api.debug.log('This is a log message'); +editor.api.debug.info('This is an info message'); +editor.api.debug.warn('This is a warning'); +editor.api.debug.error('This is an error'); +``` + +### 日志级别 + +`DebugPlugin` 支持四种日志级别: + +1. `log`:用于一般日志记录 +2. `info`:用于信息性消息 +3. `warn`:用于警告 +4. `error`:用于错误 + +你可以设置最小日志级别来控制显示哪些消息: + +```ts +const editor = createPlateEditor({ + plugins: [ + DebugPlugin.configure({ + options: { + logLevel: 'warn', // 仅显示警告和错误 + }, + }), + ], +}); +``` + +### 配置选项 + +`DebugPlugin` 可以使用以下选项进行配置: + +- `isProduction`:设置为 `true` 以在生产环境中禁用日志记录。 +- `logLevel`:设置最小日志级别(`'error'`、`'warn'`、`'info'` 或 `'log'`)。 +- `logger`:为每个日志级别提供自定义日志记录函数。 +- `throwErrors`:设置为 `true` 以抛出错误而不是记录它们(默认:`true`)。 + +配置示例: + +```ts +const editor = createPlateEditor({ + plugins: [ + DebugPlugin.configure({ + options: { + isProduction: process.env.NODE_ENV === 'production', + logLevel: 'info', + logger: { + error: (message, type, details) => { + // 自定义错误日志记录 + console.error(`Custom Error: ${message}`, type, details); + }, + // ... 其他级别的自定义日志记录器 + }, + throwErrors: false, + }, + }), + ], +}); +``` + +### 错误处理 + +默认情况下,当调用 `error` 时,`DebugPlugin` 会抛出错误。你可以捕获这些错误并按需处理: + +```ts +try { + editor.api.debug.error('An error occurred', 'CUSTOM_ERROR', { details: 'Additional information' }); +} catch (error) { + if (error instanceof PlateError) { + console.debug(error.type); // 'CUSTOM_ERROR' + console.debug(error.message); // '[CUSTOM_ERROR] An error occurred' + } +} +``` + +要记录错误而不是抛出它们,请在配置中将 `throwErrors` 设置为 `false`。 + +### 最佳实践 + +1. 为不同类型的消息使用适当的日志级别。 +2. 在生产环境中,将 `isProduction` 设置为 `true` 以禁用非必要的日志记录。 +3. 使用自定义日志记录器与您首选的日志记录服务集成。 +4. 在记录时包含相关详细信息,以便于调试。 +5. 使用错误类型来分类和处理不同的错误场景。 + +## 其他调试策略 + +除了使用 DebugPlugin,还有其他有效的方法来调试你的 Plate 编辑器: + +### 1. 使用日志覆盖编辑器方法 + +你可以使用 `extendEditor` 选项来覆盖编辑器方法并添加日志记录: + +```ts +const LoggingPlugin = createPlatePlugin({ + key: 'logging', +}).overrideEditor(({ editor, tf: { apply } }) => ({ + transforms: { + apply(operation) { + console.debug('Operation:', operation); + apply(operation); + }, + }, +})); + +const editor = createPlateEditor({ + plugins: [LoggingPlugin], +}); +``` + +这种方法允许你记录操作、选择或任何你想要检查的编辑器行为。 + +### 2. 移除可疑插件 + +如果你遇到问题,尝试逐个移除插件以隔离问题: + +```ts +const editor = createPlateEditor({ + plugins: [ + // 注释或移除可疑插件 + // HeadingPlugin, + // BoldPlugin, + // ...其他插件 + ], +}); +``` + +逐步添加插件,直到找出导致问题的那个。 + +### 3. 使用 React DevTools + +React DevTools 对于调试 Plate 组件非常有用: + +1. 安装 React DevTools 浏览器扩展。 +2. 打开你的应用和 DevTools。 +3. 导航到 Components 标签页。 +4. 检查 Plate 组件、它们的 props 和状态。 + +### 4. 使用浏览器 DevTools 断点 + +使用浏览器 DevTools 在代码中设置断点: + +1. 在浏览器中打开你的应用并打开 DevTools。 +2. 导航到 Sources 标签页。 +3. 找到你的源文件并点击你想要设置断点的行号。 +4. 与编辑器交互以触发断点。 +5. 检查变量并逐步执行代码。 + +### 5. 创建最小可重现示例 + +如果你遇到复杂问题: + +1. 选择一个[模板](/docs/installation)。 +2. 仅添加必要的插件和组件来重现问题。 +3. 如果问题仍然存在,在 [GitHub 上提交问题](https://github.com/udecode/plate/issues/new?assignees=&labels=bug&projects=&template=bug.yml)或在 [Discord](https://discord.gg/mAZRuBzGM3) 上分享你的示例。 + +### 6. 使用 Redux DevTools 进行 zustand 存储 + +Zustand 以及 zustand-x 可以与 Redux DevTools 浏览器扩展一起使用。它对于跟踪 zustand 存储中的状态变化非常有用。 + +按照 [zustand 文档](https://zustand.docs.pmnd.rs/middlewares/devtools) 开始使用 Redux DevTools 和 zustand。 + +## 调试错误类型 + +Plate 使用几个预定义的错误类型来帮助识别开发过程中的特定问题。以下是这些错误类型及其描述的列表: + +### DEFAULT + +一个不适合其他特定类别的一般错误。当没有其他错误类型适用于该情况时使用。 + +### OPTION_UNDEFINED + +当尝试访问未定义的插件选项时抛出。这发生在尝试使用尚未设置或未定义的插件选项时。 + +### OVERRIDE_MISSING + +表示插件配置中缺少预期的覆盖。当插件期望提供某些覆盖但它们不在配置中时,会发生这种情况。 + +### PLUGIN_DEPENDENCY_MISSING + +当找不到必需的插件依赖项时发生。当插件依赖于尚未注册或包含在编辑器配置中的另一个插件时,会抛出此错误。 + +### PLUGIN_MISSING + +表示尝试使用尚未注册的插件。这发生在尝试访问或使用不属于当前编辑器配置的插件时。 + +### USE_CREATE_PLUGIN + +当插件不是使用 `createSlatePlugin` 或 `createPlatePlugin` 函数创建时抛出。当插件被添加到编辑器中但未使用指定函数正确创建时,会发生此错误。 + +### USE_ELEMENT_CONTEXT + +表示 `useElement` hook 在适当的 element 上下文之外使用。这发生在尝试在正确的组件上下文之外访问 element 特定的数据或功能时。 + +### PLUGIN_NODE_TYPE + +当插件被错误地配置为既是 element 又是 leaf 时抛出。当插件的配置通过同时将 `isElement` 和 `isLeaf` 设置为 true 而自相矛盾时,会发生此错误。 diff --git a/apps/www/content/docs/(guides)/debugging.mdx b/apps/www/content/docs/(guides)/debugging.mdx new file mode 100644 index 0000000000..6689e76313 --- /dev/null +++ b/apps/www/content/docs/(guides)/debugging.mdx @@ -0,0 +1,213 @@ +--- +title: Debugging +description: Debugging in Plate. +--- + +## Using the DebugPlugin + +The `DebugPlugin` is automatically included when you create a Plate editor. You can access its methods through the editor's API: + +```ts +const editor = createPlateEditor({ + plugins: [/* your plugins */], +}); + +editor.api.debug.log('This is a log message'); +editor.api.debug.info('This is an info message'); +editor.api.debug.warn('This is a warning'); +editor.api.debug.error('This is an error'); +``` + +### Log Levels + +The `DebugPlugin` supports four log levels: + +1. `log`: For general logging +2. `info`: For informational messages +3. `warn`: For warnings +4. `error`: For errors + +You can set the minimum log level to control which messages are displayed: + +```ts +const editor = createPlateEditor({ + plugins: [ + DebugPlugin.configure({ + options: { + logLevel: 'warn', // Only show warnings and errors + }, + }), + ], +}); +``` + +### Configuration Options + +The `DebugPlugin` can be configured with the following options: + +- `isProduction`: Set to `true` to disable logging in production environments. +- `logLevel`: Set the minimum log level (`'error'`, `'warn'`, `'info'`, or `'log'`). +- `logger`: Provide custom logging functions for each log level. +- `throwErrors`: Set to `true` to throw errors instead of logging them (default: `true`). + +Example configuration: + +```ts +const editor = createPlateEditor({ + plugins: [ + DebugPlugin.configure({ + options: { + isProduction: process.env.NODE_ENV === 'production', + logLevel: 'info', + logger: { + error: (message, type, details) => { + // Custom error logging + console.error(`Custom Error: ${message}`, type, details); + }, + // ... custom loggers for other levels + }, + throwErrors: false, + }, + }), + ], +}); +``` + +### Error Handling + +By default, the `DebugPlugin` throws errors when `error` is called. You can catch these errors and handle them as needed: + +```ts +try { + editor.api.debug.error('An error occurred', 'CUSTOM_ERROR', { details: 'Additional information' }); +} catch (error) { + if (error instanceof PlateError) { + console.debug(error.type); // 'CUSTOM_ERROR' + console.debug(error.message); // '[CUSTOM_ERROR] An error occurred' + } +} +``` + +To log errors instead of throwing them, set `throwErrors` to `false` in the configuration. + +### Best Practices + +1. Use appropriate log levels for different types of messages. +2. In production, set `isProduction` to `true` to disable non-essential logging. +3. Use custom loggers to integrate with your preferred logging service. +4. Include relevant details when logging to make debugging easier. +5. Use error types to categorize and handle different error scenarios. + +## Additional Debugging Strategies + +Besides using the DebugPlugin, there are other effective ways to debug your Plate editor: + +### 1. Override Editor Methods with Logging + +You can use the `extendEditor` option to override editor methods and add logging: + +```ts +const LoggingPlugin = createPlatePlugin({ + key: 'logging', +}).overrideEditor(({ editor, tf: { apply } }) => ({ + transforms: { + apply(operation) { + console.debug('Operation:', operation); + apply(operation); + }, + }, +})); + +const editor = createPlateEditor({ + plugins: [LoggingPlugin], +}); +``` + +This approach allows you to log operations, selections, or any other editor behavior you want to inspect. + +### 2. Remove Suspected Plugins + +If you're experiencing issues, try removing plugins one by one to isolate the problem: + +```ts +const editor = createPlateEditor({ + plugins: [ + // Comment out or remove suspected plugins + // HeadingPlugin, + // BoldPlugin, + // ...other plugins + ], +}); +``` + +Gradually add plugins back until you identify the one causing the issue. + +### 3. Use React DevTools + +React DevTools can be invaluable for debugging Plate components: + +1. Install the React DevTools browser extension. +2. Open your app and the DevTools. +3. Navigate to the Components tab. +4. Inspect Plate components, their props, and state. + +### 4. Use Browser DevTools Breakpoints + +Set breakpoints in your code using browser DevTools: + +1. Open your app in the browser and open DevTools. +2. Navigate to the Sources tab. +3. Find your source file and click on the line number where you want to set a breakpoint. +4. Interact with your editor to trigger the breakpoint. +5. Inspect variables and step through the code. + +### 5. Create Minimal Reproducible Examples + +If you're facing a complex issue: + +1. Pick a [template](/docs/installation). +2. Add only the essential plugins and components to reproduce the issue. +3. If the issue persists, [open an issue on GitHub](https://github.com/udecode/plate/issues/new?assignees=&labels=bug&projects=&template=bug.yml) or share your example on [Discord](https://discord.gg/mAZRuBzGM3). + +### 6. Use Redux DevTools for zustand stores + +Zustand and thus zustand-x works with the Redux DevTools browser extension. It can be very useful to help track state changes in zustand stores. + +Follow the [zustand documentation](https://zustand.docs.pmnd.rs/middlewares/devtools) to get going with Redux DevTools and zustand. + + +## Debug Error Types + +Plate uses several predefined error types to help identify specific issues during development. Here's a list of these error types and their descriptions: + +### DEFAULT + +A general error that doesn't fit into other specific categories. Used when no other error type is applicable to the situation. + +### OPTION_UNDEFINED + +Thrown when an attempt is made to access an undefined plugin option. This occurs when trying to use a plugin option that hasn't been set or is undefined. + +### OVERRIDE_MISSING + +Indicates that an expected override is missing in a plugin configuration. This happens when a plugin expects certain overrides to be provided, but they are not present in the configuration. + +### PLUGIN_DEPENDENCY_MISSING + +Occurs when a required plugin dependency is not found. This error is thrown when a plugin depends on another plugin that hasn't been registered or included in the editor configuration. + +### PLUGIN_MISSING + +Indicates an attempt to use a plugin that hasn't been registered. This happens when trying to access or use a plugin that is not part of the current editor configuration. + +### USE_CREATE_PLUGIN + +Thrown when a plugin wasn't created using `createSlatePlugin` or `createPlatePlugin` function. This error occurs when a plugin is added to the editor without being properly created using the designated function. + +### USE_ELEMENT_CONTEXT + +Indicates that the `useElement` hook is being used outside of the appropriate element context. This occurs when trying to access element-specific data or functionality outside of the correct component context. + +### PLUGIN_NODE_TYPE + +Thrown when a plugin is incorrectly configured as both an element and a leaf. This error occurs when a plugin's configuration contradicts itself by setting both `isElement` and `isLeaf` to true. diff --git a/apps/www/content/docs/(guides)/editor-methods.cn.mdx b/apps/www/content/docs/(guides)/editor-methods.cn.mdx new file mode 100644 index 0000000000..aefab1bab1 --- /dev/null +++ b/apps/www/content/docs/(guides)/editor-methods.cn.mdx @@ -0,0 +1,208 @@ +--- +title: 编辑器方法 +description: 探索可用于与Plate编辑器交互及自定义的各种方法。 +--- + +本指南涵盖了Plate编辑器实例上可用的各种方法。 + +## 访问编辑器 + +访问编辑器实例的方式取决于您需要的上下文。 + +### Plate组件内部 + +使用以下钩子之一: + +- `useEditorRef`: 永不重新渲染。 +- `useEditorSelector`: 仅当特定编辑器属性变化时重新渲染。 +- `useEditorState`: 每次变化都重新渲染。 + +```ts +import { useEditorRef, useEditorSelector, useEditorState } from 'platejs/react'; + +const MyComponent = () => { + const editor = useEditorRef(); + const hasSelection = useEditorSelector((editor) => !!editor.selection, []); + const editorState = useEditorState(); + + // ... +}; +``` + +#### useEditorRef + +- 使用永不替换的编辑器引用。这应该是默认选择。 +- 由于编辑器是一个通过引用更新的可变对象,`useEditorRef`总是足以在回调中访问编辑器。 +- `useEditorRef`不会导致组件重新渲染,因此是性能最佳的选择。 + +#### useEditorSelector + +- 订阅基于编辑器的特定选择器。这是订阅状态变化最高效的方式。 +- 示例用法:`const hasSelection = useEditorSelector((editor) => !!editor.selection, []);` +- 当您希望组件响应特定变化重新渲染时,可以使用`useEditorSelector`访问相关属性。 +- 选择器函数在每次编辑器变化时被调用(如每次按键或选择变化),但组件仅在返回值变化时重新渲染。 +- 为确保正常工作,返回值应能使用`===`进行比较。通常这意味着返回原始值(数字、字符串或布尔值)。 +- 可以为`useEditorSelector`提供自定义的`equalityFn`选项来处理`===`不适用的情况。 +- 如果选择器函数依赖局部作用域变量,应将它们包含在依赖项列表中。 + +#### useEditorState + +- 每次编辑器变化时都重新渲染。 +- 使用`useEditorState`会导致组件在用户每次按键或改变选择时重新渲染。 +- 对于大型文档或重渲染成本较高的情况,可能会引发性能问题。 + +### Plate组件外部 + +要在`Plate`组件外部访问编辑器或处理多个编辑器,使用`PlateController`组件: + +```ts +import { PlateController } from 'platejs/react'; + +const App = () => ( + + + + +); + +const Toolbar = () => { + const editor = useEditorState(); + const isMounted = useEditorMounted(); + // 在此使用编辑器方法 +}; +``` + +`PlateController`管理活动编辑器: +- 默认第一个挂载的编辑器为活动状态(可通过`Plate`上的`primary={false}`覆盖)。 +- 焦点改变会切换活动编辑器。 +- 编辑器保持活动状态直到另一个编辑器获得焦点或自身卸载。 + +`PlateController`内部的钩子如`useEditorRef`和`useEditorSelector`会与活动编辑器交互。若无活动编辑器,它们会返回一个回退编辑器,该编辑器: +- 为查询提供默认值。 +- 不可被修改。 +- 在状态变更操作时抛出错误。 + +回退编辑器场景: +- 没有挂载的`Plate`组件。 +- 所有`Plate`组件都标记为非主要。 +- 在`PlateContent`挂载过程中。 + +可使用`useEditorMounted`检查是否有编辑器已挂载: + +```ts +const Toolbar = () => { + const editor = useEditorState(); + const isMounted = useEditorMounted(); + + if (!isMounted) { + return
编辑器未就绪
; + } + + return
{/* 工具栏内容 */}
; +}; +``` + +也可通过`editor.meta.isFallback`检查是否为回退实例。 + +## API方法 + +### findPath + +查找节点的路径。默认为`findNodePath`(遍历)。被`withPlate`覆盖为使用`ReactEditor.findPath`(记忆化)。 + +```ts +const path = editor.findPath(node); +``` + +### getApi + +获取编辑器的类型化API: + +```ts +const api = editor.getApi(TablePlugin); +api.api.create.tableCell(); // 类型安全的API方法 +``` + +### getTransforms + +获取编辑器的类型化转换方法: + +```ts +const tf = editor.getTransforms(TablePlugin); +tf.insert.tableRow(); // 类型安全的转换方法 +``` + +## 插件方法 + +### getPlugin + +通过键或基础插件获取编辑器插件实例: + +```ts +const codeBlockPlugin = editor.getPlugin(CodeBlockPlugin); +const headingPlugin = editor.getPlugin({ key: KEYS.heading }); +``` + +### getType + +获取与插件关联的节点类型: + +```ts +const paragraphType = editor.getType(KEYS.p); +``` + +## 选项方法 + +### getOption + +获取插件的特定选项值: + +```ts +const search = editor.getOption(FindReplacePlugin, 'search'); +``` + +要订阅选项变化,可使用`usePluginOption`或`usePluginOptions`钩子。 + +### getOptions + +获取插件的所有选项: + +```ts +const linkOptions = editor.getOptions(LinkPlugin); +``` + +### setOption + +设置插件的特定选项值: + +```ts +editor.setOption(FindReplacePlugin, 'search', 'hello'); +``` + +### setOptions + +设置插件的多个选项: + +```ts +editor.setOptions(FindReplacePlugin, { + search: 'hello', + caseSensitive: true, +}); +``` + +也可使用Immer函数更新选项: + +```ts +editor.setOptions(FindReplacePlugin, (draft) => { + draft.search = 'hello'; + draft.caseSensitive = true; +}); +``` + +### getOptionsStore + +获取插件的[zustand-x](https://github.com/udecode/zustand-x)选项存储: + +```ts +const store = editor.getOptionsStore(FindReplacePlugin); +``` \ No newline at end of file diff --git a/apps/www/content/docs/(guides)/editor-methods.mdx b/apps/www/content/docs/(guides)/editor-methods.mdx new file mode 100644 index 0000000000..034c5eaded --- /dev/null +++ b/apps/www/content/docs/(guides)/editor-methods.mdx @@ -0,0 +1,208 @@ +--- +title: Editor Methods +description: Explore the various methods available for interacting with and customizing the Plate editor. +--- + +This guide covers the various methods available on the Plate editor instance. + +## Accessing the Editor + +How you access the editor instance depends on the context in which you need it. + +### Below Plate + +Use one of these hooks: + +- `useEditorRef`: Never re-render. +- `useEditorSelector`: Re-render only when a specific editor property changes. +- `useEditorState`: Re-render on every change. + +```ts +import { useEditorRef, useEditorSelector, useEditorState } from 'platejs/react'; + +const MyComponent = () => { + const editor = useEditorRef(); + const hasSelection = useEditorSelector((editor) => !!editor.selection, []); + const editorState = useEditorState(); + + // ... +}; +``` + +#### useEditorRef + +- Use a reference to editor that never gets replaced. This should be the default choice. +- Since editor is a mutable object that gets updated by reference, `useEditorRef` is always sufficient for accessing the editor inside callbacks. +- `useEditorRef` will never cause your component to re-render, so it's the best choice for performance. + +#### useEditorSelector + +- Subscribe to a specific selector based on editor. This is the most performant option for subscribing to state changes. +- Example usage: `const hasSelection = useEditorSelector((editor) => !!editor.selection, []);` +- When you want your component to re-render in response to a specific change that you're interested in, you can use `useEditorSelector` to access the relevant property. +- The selector function is called every time the editor changes (i.e. on every keystroke or selection change), but the component only re-renders when the return value changes. +- For this to work properly, you should make sure that the return value can be compared using `===`. In most cases, this means returning a primitive value, like a number, string or boolean. +- You can provide a custom `equalityFn` in the options to `useEditorSelector` for cases where `===` isn't sufficient. +- If the selector function depends on any locally scoped variables, you should include these in the dependency list. + +#### useEditorState + +- Re-render every time the editor changes. +- Using `useEditorState` will cause your component to re-render every time the user presses a key or changes the selection. +- This may cause performance issues for large documents, or when re-rendering is particularly expensive. + +### Outside Plate + +To access the editor outside the `Plate` component or work with multiple editors, use the `PlateController` component: + +```ts +import { PlateController } from 'platejs/react'; + +const App = () => ( + + + + +); + +const Toolbar = () => { + const editor = useEditorState(); + const isMounted = useEditorMounted(); + // Use editor methods here +}; +``` + +`PlateController` manages active editors: +- The first mounted editor is active by default (override with `primary={false}` on `Plate`). +- Focus changes the active editor. +- An editor remains active until another is focused or it unmounts. + +Hooks like `useEditorRef` and `useEditorSelector` work with the active editor inside `PlateController`. If no editor is active, they return a fallback editor, which: +- Provides default values for queries. +- Cannot be mutated. +- Throws errors on state-changing operations. + +Fallback editor scenarios: +- No mounted `Plate` components. +- All `Plate` components are non-primary. +- During `PlateContent` mounting. + +You can check if any editor is mounted using `useEditorMounted`: + +```ts +const Toolbar = () => { + const editor = useEditorState(); + const isMounted = useEditorMounted(); + + if (!isMounted) { + return
Editor not ready
; + } + + return
{/* Toolbar content */}
; +}; +``` + +You can also use `editor.meta.isFallback` to check if the editor is a fallback instance. + +## API Methods + +### findPath + +Find the path of a node. Default is `findNodePath` (traversal). Overridden by `withPlate` to use `ReactEditor.findPath` (memo). + +```ts +const path = editor.findPath(node); +``` + +### getApi + +Retrieve the typed API for the editor: + +```ts +const api = editor.getApi(TablePlugin); +api.api.create.tableCell(); // Type-safe API method +``` + +### getTransforms + +Get the typed transforms for the editor: + +```ts +const tf = editor.getTransforms(TablePlugin); +tf.insert.tableRow(); // Type-safe transform method +``` + +## Plugin Methods + +### getPlugin + +Retrieve the editor plugin instance by its key or base plugin: + +```ts +const codeBlockPlugin = editor.getPlugin(CodeBlockPlugin); +const headingPlugin = editor.getPlugin({ key: KEYS.heading }); +``` + +### getType + +Get the node type associated with a plugin: + +```ts +const paragraphType = editor.getType(KEYS.p); +``` + +## Option Methods + +### getOption + +Get a specific option value for a plugin: + +```ts +const search = editor.getOption(FindReplacePlugin, 'search'); +``` + +To subscribe to options changes, use `usePluginOption` or `usePluginOptions` hooks. + +### getOptions + +Get all options for a plugin: + +```ts +const linkOptions = editor.getOptions(LinkPlugin); +``` + +### setOption + +Set a specific option value for a plugin: + +```ts +editor.setOption(FindReplacePlugin, 'search', 'hello'); +``` + +### setOptions + +Set multiple options for a plugin: + +```ts +editor.setOptions(FindReplacePlugin, { + search: 'hello', + caseSensitive: true, +}); +``` + +You can also use a function to update options using Immer: + +```ts +editor.setOptions(FindReplacePlugin, (draft) => { + draft.search = 'hello'; + draft.caseSensitive = true; +}); +``` + +### getOptionsStore + +Get the [zustand-x](https://github.com/udecode/zustand-x) options store for a plugin: + +```ts +const store = editor.getOptionsStore(FindReplacePlugin); +``` diff --git a/apps/www/content/docs/(guides)/editor.cn.mdx b/apps/www/content/docs/(guides)/editor.cn.mdx new file mode 100644 index 0000000000..e0258bae81 --- /dev/null +++ b/apps/www/content/docs/(guides)/editor.cn.mdx @@ -0,0 +1,369 @@ +--- +title: 编辑器配置 +description: 学习如何配置和自定义Plate编辑器。 +--- + +本指南涵盖Plate编辑器的配置选项,包括基础设置、插件管理和高级配置技巧。 + +## 基础编辑器配置 + +创建基础Plate编辑器可以使用`createPlateEditor`函数,或在React组件中使用`usePlateEditor`: + +```ts +import { createPlateEditor } from 'platejs/react'; + +const editor = createPlateEditor({ + plugins: [HeadingPlugin], +}); +``` + +### 初始值 + +设置编辑器的初始内容: + +```ts +const editor = createPlateEditor({ + value: [ + { + type: 'p', + children: [{ text: '你好,Plate!' }], + }, + ], +}); +``` + +也可以使用HTML字符串和关联插件初始化编辑器: + +```ts +const editor = createPlateEditor({ + plugins: [BoldPlugin, ItalicPlugin], + value: '

这是加粗斜体文本!

', +}); +``` + +支持HTML字符串反序列化的完整插件列表,请参阅[插件反序列化规则](/docs/html#plugin-deserialization-rules)部分。 + +### 异步初始值 + +如果需要异步获取初始值(例如从API),可以使用`skipInitialization`选项并在数据就绪后调用`editor.tf.init`: + +```tsx +function AsyncEditor() { + const [initialValue, setInitialValue] = React.useState(); + const [loading, setLoading] = React.useState(true); + const editor = usePlateEditor({ + skipInitialization: true, + }); + + React.useEffect(() => { + // 模拟异步获取 + setTimeout(() => { + setInitialValue([ + { + type: 'p', + children: [{ text: '异步加载的值!' }], + }, + ]); + setLoading(false); + }, 1000); + }, []); + + React.useEffect(() => { + if (!loading && initialValue) { + editor.tf.init({ value: initialValue }); + } + }, [loading, initialValue, editor]); + + if (loading) return
加载中…
; + + return ( + + + + + + ); +} +``` + +这种模式适用于需要等待数据才能初始化编辑器的情况,例如从服务器或数据库加载内容时。 + +### 添加插件 + +通过`plugins`数组添加插件: + +```ts +const editor = createPlateEditor({ + plugins: [HeadingPlugin, ListPlugin], +}); +``` + +### 最大长度 + +设置编辑器的最大长度: + +```ts +const editor = createPlateEditor({ + maxLength: 100, +}); +``` + +## 高级配置 + +### 编辑器ID + +为编辑器设置自定义ID: + +```ts +const editor = createPlateEditor({ + id: 'my-custom-editor-id', +}); +``` + +如果定义了ID,在任何编辑器检索方法中都应该将其作为第一个参数传递。 + +### 节点ID + +Plate内置了为节点自动分配唯一ID的系统,这对于某些插件和依赖稳定标识符的数据持久化策略至关重要。 + +此功能默认启用。可以通过`nodeId`选项自定义其行为或完全禁用。 + +#### 配置 + +创建编辑器时,向`nodeId`属性传递对象来配置节点ID行为: + +```ts +const editor = usePlateEditor({ + // ... 其他插件和选项 + nodeId: { + // 生成ID的函数(默认:nanoid(10)) + idCreator: () => uuidv4(), + + // 排除内联元素获取ID(默认:true) + filterInline: true, + + // 排除文本节点获取ID(默认:true) + filterText: true, + + // 在撤销/重做和复制/粘贴时重用ID(默认:false) + // 设置为true可使ID在这些操作中保持稳定 + reuseId: false, + + // 规范化初始值中的所有节点(默认:false - 仅检查首尾) + // 设置为true确保所有初始节点在缺失时获取ID + normalizeInitialValue: false, + + // 禁止覆盖插入节点的现有ID(默认:false) + disableInsertOverrides: false, + + // 仅允许特定节点类型获取ID(默认:全部) + allow: ['p', 'h1'], + + // 排除特定节点类型获取ID(默认:[]) + exclude: ['code_block'], + + // 自定义过滤函数决定节点是否获取ID + filter: ([node, path]) => { + // 示例:仅对顶层块分配ID + return path.length === 1; + }, + }, +}); +``` + + + `NodeIdPlugin`(处理此功能)是核心插件的一部分,已自动包含。只有在需要自定义其默认行为时才需要指定`nodeId`选项。 + + +#### 禁用节点ID + +如果不需要自动节点ID,可以禁用此功能: + +```ts +const editor = usePlateEditor({ + // ... 其他插件和选项 + nodeId: false, // 这将禁用NodeIdPlugin +}); +``` + +禁用后,依赖节点ID的某些插件将无法正常工作。以下插件需要块ID才能工作: + +- **[块选择](/docs/block-selection)** - 需要ID来跟踪选中的块 +- **[块菜单](/docs/block-menu)** - 需要ID显示特定块的上下文菜单 +- **[拖放](/docs/dnd)** - 使用ID在拖拽操作中识别块 +- **[表格](/docs/table)** - 依赖ID进行单元格选择 +- **[目录](/docs/toc)** - 需要标题ID进行导航和滚动 +- **[折叠](/docs/toggle)** - 使用ID跟踪哪些折叠项是打开/关闭的 + +### 规范化 + +控制编辑器是否在初始化时规范化其内容: + +```ts +const editor = createPlateEditor({ + shouldNormalizeEditor: true, +}); +``` + +注意,对于大型文档(如playground值),规范化可能需要几十毫秒。 + +### 自动选择 + +配置编辑器自动选择范围: + +```ts +const editor = createPlateEditor({ + autoSelect: 'end', // 或 'start',或 true +}); +``` + +这与自动聚焦不同:可以在不聚焦编辑器的情况下选择文本。 + +### 组件覆盖 + +覆盖插件的默认组件: + +```ts +const editor = createPlateEditor({ + plugins: [HeadingPlugin], + components: { + [ParagraphPlugin.key]: CustomParagraphComponent, + [HeadingPlugin.key]: CustomHeadingComponent, + }, +}); +``` + +### 插件覆盖 + +覆盖特定插件配置: + +```ts +const editor = createPlateEditor({ + plugins: [HeadingPlugin], + override: { + plugins: { + [ParagraphPlugin.key]: { + options: { + customOption: true, + }, + }, + }, + }, +}); +``` + +### 禁用插件 + +禁用特定插件: + +```ts +const editor = createPlateEditor({ + plugins: [HeadingPlugin, ListPlugin], + override: { + enabled: { + [HistoryPlugin.key]: false, + }, + }, +}); +``` + +### 覆盖插件 + +可以通过添加具有相同键的插件来覆盖核心插件或先前定义的插件。最后一个具有给定键的插件将生效: + +```ts +const CustomParagraphPlugin = createPlatePlugin({ + key: 'p', + // 自定义实现 +}); + +const editor = createPlateEditor({ + plugins: [CustomParagraphPlugin], +}); +``` + +### 根插件 + +从根插件可以配置任何插件: + +```ts +const editor = createPlateEditor({ + plugins: [HeadingPlugin], + rootPlugin: (plugin) => + plugin.configurePlugin(LengthPlugin, { + options: { + maxLength: 100, + }, + }), +}); +``` + +## 类型化编辑器 + +`createPlateEditor`会根据传递的值和插件自动推断编辑器的类型。要显式创建类型,可以使用泛型: + +### 插件类型 + +```ts +const editor = createPlateEditor({ + plugins: [TablePlugin, LinkPlugin], +}); + +// 使用 +editor.tf.insert.tableRow() +``` + +### 值类型 + +对于更复杂的编辑器,可以在单独的文件中定义类型(例如`plate-types.ts`): + +```ts +import type { TElement, TText } from 'platejs'; +import type { TPlateEditor } from 'platejs/react'; + +// 定义自定义元素类型 +interface ParagraphElement extends TElement { + align?: 'left' | 'center' | 'right' | 'justify'; + children: RichText[]; + type: typeof ParagraphPlugin.key; +} + +interface ImageElement extends TElement { + children: [{ text: '' }] + type: typeof ImagePlugin.key; + url: string; +} + +// 定义自定义文本类型 +interface FormattedText extends TText { + bold?: boolean; + italic?: boolean; +} + +export type MyRootBlock = ParagraphElement | ImageElement; + +// 定义编辑器的值类型 +export type MyValue = MyRootBlock[]; + +// 定义自定义编辑器类型 +export type MyEditor = TPlateEditor; + +export const useMyEditorRef = () => useEditorRef(); + +// 使用 +const value: MyValue = [{ + type: 'p', + children: [{ text: '你好,Plate!' }], +}] + +const editorInferred = createPlateEditor({ + plugins: [TablePlugin, LinkPlugin], + value, +}); + +// 或 +const editorExplicit = createPlateEditor({ + plugins: [TablePlugin, LinkPlugin], + value, +}); +``` \ No newline at end of file diff --git a/apps/www/content/docs/(guides)/editor.mdx b/apps/www/content/docs/(guides)/editor.mdx new file mode 100644 index 0000000000..e8559b040c --- /dev/null +++ b/apps/www/content/docs/(guides)/editor.mdx @@ -0,0 +1,356 @@ +--- +title: Editor Configuration +description: Learn how to configure and customize the Plate editor. +--- + +This guide covers the configuration options for the Plate editor, including basic setup, plugin management, and advanced configuration techniques. + +## Basic Editor Configuration + +To create a basic Plate editor, you can use the `createPlateEditor` function, or `usePlateEditor` in a React component: + +```ts +import { createPlateEditor } from 'platejs/react'; + +const editor = createPlateEditor({ + plugins: [HeadingPlugin], +}); +``` + +### Initial Value + +Set the initial content of the editor: + +```ts +const editor = createPlateEditor({ + value: [ + { + type: 'p', + children: [{ text: 'Hello, Plate!' }], + }, + ], +}); +``` + +You can also initialize the editor with an HTML string and the associated plugins: + +```ts +const editor = createPlateEditor({ + plugins: [BoldPlugin, ItalicPlugin], + value: '

This is bold and italic text!

', +}); +``` + +For a comprehensive list of plugins that support HTML string deserialization, refer to the [Plugin Deserialization Rules](/docs/html#plugin-deserialization-rules) section. + +### Async Initial Value + +If you need to fetch the initial value asynchronously (e.g., from an API), you can pass an async function directly to the `value` option: + +```tsx +function AsyncEditor() { + const editor = usePlateEditor({ + value: async () => { + // Simulate fetching data from an API + const response = await fetch('/api/document'); + const data = await response.json(); + return data.content; + }, + autoSelect: 'end', + onReady: ({ editor, value }) => { + console.info('Editor ready with loaded value:', value); + }, + }); + + if (!editor.children.length) return
Loading…
; + + return ( + + + + + + ); +} +``` + +### Adding Plugins + +You can add plugins to your editor by including them in the `plugins` array: + +```ts +const editor = createPlateEditor({ + plugins: [HeadingPlugin, ListPlugin], +}); +``` + +### Max Length + +Set the maximum length of the editor: + +```ts +const editor = createPlateEditor({ + maxLength: 100, +}); +``` + +## Advanced Configuration + +### Editor ID + +Set a custom id for the editor: + +```ts +const editor = createPlateEditor({ + id: 'my-custom-editor-id', +}); +``` + +If defined, you should always pass the `id` as the first argument in any editor retrieval methods. + +### Node ID + +Plate includes a built-in system for automatically assigning unique IDs to nodes, which is crucial for certain plugins and for data persistence strategies that rely on stable identifiers. + +This feature is enabled by default. You can customize its behavior or disable it entirely through the `nodeId` option. + +#### Configuration + +To configure Node ID behavior, pass an object to the `nodeId` property when creating your editor: + +```ts +const editor = usePlateEditor({ + // ... other plugins and options + nodeId: { + // Function to generate IDs (default: nanoid(10)) + idCreator: () => uuidv4(), + + // Exclude inline elements from getting IDs (default: true) + filterInline: true, + + // Exclude text nodes from getting IDs (default: true) + filterText: true, + + // Reuse IDs on undo/redo and copy/paste if not in document (default: false) + // Set to true if IDs should be stable across such operations. + reuseId: false, + + // Normalize all nodes in initial value (default: false - only checks first/last) + // Set to true to ensure all initial nodes get IDs if missing. + normalizeInitialValue: false, + + // Prevent overriding IDs when inserting nodes with an existing id (default: false) + disableInsertOverrides: false, + + // Only allow specific node types to receive IDs (default: all) + allow: ['p', 'h1'], + + // Exclude specific node types from receiving IDs (default: []) + exclude: ['code_block'], + + // Custom filter function to determine if a node should get an ID + filter: ([node, path]) => { + // Example: Only ID on top-level blocks + return path.length === 1; + }, + }, +}); +``` + + + The `NodeIdPlugin` (which handles this) is part of the core plugins and is automatically included. You only need to specify the `nodeId` option if you want to customize its default behavior. + + +#### Disabling Node IDs + +If you don't need automatic node IDs, you can disable the feature: + +```ts +const editor = usePlateEditor({ + // ... other plugins and options + nodeId: false, // This will disable the NodeIdPlugin +}); +``` + +By disabling this, certain plugins that rely on node IDs will not function properly. The following plugins require block IDs to work: + +- **[Block Selection](/docs/block-selection)** - Needs IDs to track which blocks are selected +- **[Block Menu](/docs/block-menu)** - Requires IDs to show context menus for specific blocks +- **[Drag & Drop](/docs/dnd)** - Uses IDs to identify blocks during drag operations +- **[Table](/docs/table)** - Relies on IDs for cell selection +- **[Table of Contents](/docs/toc)** - Needs heading IDs for navigation and scrolling +- **[Toggle](/docs/toggle)** - Uses IDs to track which toggles are open/closed + +### Normalization + +Control whether the editor should normalize its content on initialization: + +```ts +const editor = createPlateEditor({ + shouldNormalizeEditor: true, +}); +``` + +Note that normalization may take a few dozen milliseconds for large documents, such as the playground value. + +### Auto-selection + +Configure the editor to automatically select a range: + +```ts +const editor = createPlateEditor({ + autoSelect: 'end', // or 'start', or true +}); +``` + +This is not the same as auto-focus: you can select text without focusing the editor. + +### Component Overrides + +Override default components for plugins: + +```ts +const editor = createPlateEditor({ + plugins: [HeadingPlugin], + components: { + [ParagraphPlugin.key]: CustomParagraphComponent, + [HeadingPlugin.key]: CustomHeadingComponent, + }, +}); +``` + +### Plugin Overrides + +Override specific plugin configurations: + +```ts +const editor = createPlateEditor({ + plugins: [HeadingPlugin], + override: { + plugins: { + [ParagraphPlugin.key]: { + options: { + customOption: true, + }, + }, + }, + }, +}); +``` + +### Disable Plugins + +Disable specific plugins: + +```ts +const editor = createPlateEditor({ + plugins: [HeadingPlugin, ListPlugin], + override: { + enabled: { + [HistoryPlugin.key]: false, + }, + }, +}); +``` + +### Overriding Plugins + +You can override core plugins or previously defined plugins by adding a plugin with the same key. The last plugin with a given key wins: + +```ts +const CustomParagraphPlugin = createPlatePlugin({ + key: 'p', + // Custom implementation +}); + +const editor = createPlateEditor({ + plugins: [CustomParagraphPlugin], +}); +``` + +### Root Plugin + +From the root plugin, you can configure any plugin: + +```ts +const editor = createPlateEditor({ + plugins: [HeadingPlugin], + rootPlugin: (plugin) => + plugin.configurePlugin(LengthPlugin, { + options: { + maxLength: 100, + }, + }), +}); +``` + +## Typed Editor + +`createPlateEditor` will automatically infer the types for your editor from the value and the plugins you pass in. For explicit type creation, use the generics: + +### Plugins Type + +```ts +const editor = createPlateEditor({ + plugins: [TablePlugin, LinkPlugin], +}); + +// Usage +editor.tf.insert.tableRow() +``` + +### Value Type + +For more complex editors, you can define your types in a separate file (e.g., `plate-types.ts`): + +```ts +import type { TElement, TText } from 'platejs'; +import type { TPlateEditor } from 'platejs/react'; + +// Define custom element types +interface ParagraphElement extends TElement { + align?: 'left' | 'center' | 'right' | 'justify'; + children: RichText[]; + type: typeof ParagraphPlugin.key; +} + +interface ImageElement extends TElement { + children: [{ text: '' }] + type: typeof ImagePlugin.key; + url: string; +} + +// Define custom text types +interface FormattedText extends TText { + bold?: boolean; + italic?: boolean; +} + +export type MyRootBlock = ParagraphElement | ImageElement; + +// Define the editor's value type +export type MyValue = MyRootBlock[]; + +// Define the custom editor type +export type MyEditor = TPlateEditor; + +export const useMyEditorRef = () => useEditorRef(); + +// Usage +const value: MyValue = [{ + type: 'p', + children: [{ text: 'Hello, Plate!' }], +}] + +const editorInferred = createPlateEditor({ + plugins: [TablePlugin, LinkPlugin], + value, +}); + +// or +const editorExplicit = createPlateEditor({ + plugins: [TablePlugin, LinkPlugin], + value, +}); +``` + diff --git a/apps/www/content/docs/(guides)/feature-kits.cn.mdx b/apps/www/content/docs/(guides)/feature-kits.cn.mdx new file mode 100644 index 0000000000..11a1a0fb78 --- /dev/null +++ b/apps/www/content/docs/(guides)/feature-kits.cn.mdx @@ -0,0 +1,71 @@ +--- +title: 功能套件 +description: 了解如何使用预配置插件和UI组件的功能套件快速搭建编辑器。 +--- + +功能套件将相关插件及其UI组件打包整合,提供最快捷的编辑器功能添加方式。 + +## 什么是功能套件? + +功能套件是预先组装的工具包,包含针对特定编辑器功能的多个插件和组件。 + +例如,`BasicBlocksKit`包含段落、标题、引用块和水平分割线的插件,以及对应的UI组件。 + +## 使用套件的优势 + +- **快速配置**:通过一次导入添加多个功能 +- **最佳实践**:套件采用合理的默认配置 +- **内置UI**:多数套件包含[Plate UI](/docs/installation/plate-ui)组件 +- **简化依赖**:套件自动处理插件依赖关系 + +## 套件使用 vs 手动配置 + +插件文档中展示两种使用方式: + +- **套件使用**:使用预配置套件,推荐大多数用户采用 +- **手动配置**:单独安装配置每个插件,适用于以下场景: + - 需要自定义插件配置(超出套件默认值) + - 需要使用不同UI组件或无UI(headless模式) + - 仅需套件中的特定插件 + +套件本质上是预配置好的手动方案。 + +## 查找可用套件 + +在插件文档中寻找"Kit Usage"章节,这里会说明使用的套件并展示套件源代码。 + +## 示例:BasicBlocksKit + +[`BasicBlocksKit`](/docs/basic-blocks#installation)包含以下插件和组件: + +- `ParagraphPlugin`及`ParagraphElement` +- `H1Plugin`及`H1Element` +- `H2Plugin`及`H2Element` +- `H3Plugin`及`H3Element` +- `BlockquotePlugin`及`BlockquoteElement` +- `HorizontalRulePlugin`及`HrElement` + +无需单独添加每个插件,直接使用套件: + +```tsx +import { createPlateEditor } from 'platejs/react'; +import { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit'; + +const editor = createPlateEditor({ + plugins: [ + // ...其他插件 + ...BasicBlocksKit, + ], +}); +``` + +即可一次性添加所有区块元素及其UI组件。 + +## 何时选择手动配置 + +以下情况建议手动配置: + +- **自定义配置**:套件默认值不满足需求时 +- **定制UI**:不使用[Plate UI](/docs/installation/plate-ui)组件时 +- **精简配置**:仅需套件中一两个插件时 +- **学习目的**:需要理解插件协作机制时 \ No newline at end of file diff --git a/apps/www/content/docs/(guides)/feature-kits.mdx b/apps/www/content/docs/(guides)/feature-kits.mdx new file mode 100644 index 0000000000..db79cac441 --- /dev/null +++ b/apps/www/content/docs/(guides)/feature-kits.mdx @@ -0,0 +1,72 @@ +--- +title: Feature Kits +description: Understand how to use Feature Kits for rapid editor setup with pre-configured plugins and UI components. +--- + +Feature Kits bundle related plugins and their UI components. They provide the fastest way to add editor features. + +## What are Feature Kits? + +Feature Kits are pre-assembled packages. They include multiple plugins and components for specific editor features. + +For example, `BasicBlocksKit` includes plugins for paragraphs, headings, blockquotes, and horizontal rules. It also includes their UI components. + +## Benefits of Using Kits + +- **Fast setup**: Add multiple features with one import +- **Best practices**: Kits use sensible defaults and configurations +- **UI included**: Most kits include [Plate UI](/docs/installation/plate-ui) components +- **Simple dependencies**: Kits handle plugin dependencies automatically + +## Kit Usage vs Manual Usage + +Plugin documentation shows two approaches: + +- **Kit Usage**: Use a pre-configured kit. This is the recommended approach for most users. +- **Manual Usage**: Install and configure each plugin individually. Use this when you need: + - Custom plugin options beyond kit defaults + - Different UI components or no UI (headless) + - Only specific plugins from a kit + +Kits are mostly the same as manual setup, but pre-configured. + +## Finding Available Kits + +Look for "Kit Usage" sections in plugin documentation. These sections show which kit to use and include the kit's source code. + +## Example: BasicBlocksKit + +The [`BasicBlocksKit`](/docs/basic-blocks#installation) includes these plugins and components: + +- `ParagraphPlugin` with `ParagraphElement` +- `H1Plugin` with `H1Element` +- `H2Plugin` with `H2Element` +- `H3Plugin` with `H3Element` +- `BlockquotePlugin` with `BlockquoteElement` +- `HorizontalRulePlugin` with `HrElement` + +Instead of adding each plugin separately, use the kit: + +```tsx +import { createPlateEditor } from 'platejs/react'; +import { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit'; + +const editor = createPlateEditor({ + plugins: [ + // ...otherPlugins, + ...BasicBlocksKit, + ], +}); +``` + +This adds all block elements with their UI components. + +## When to Use Manual Setup + +Use manual setup when you need: + +- **Custom options**: Kit defaults don't meet your needs +- **Custom UI**: You're not using [Plate UI](/docs/installation/plate-ui) components +- **Minimal setup**: You only need one or two plugins from a kit +- **Learning**: You want to understand how plugins work together + diff --git a/apps/www/content/docs/(guides)/form.cn.mdx b/apps/www/content/docs/(guides)/form.cn.mdx new file mode 100644 index 0000000000..0a6ca48f0d --- /dev/null +++ b/apps/www/content/docs/(guides)/form.cn.mdx @@ -0,0 +1,428 @@ +--- +title: 表单 +description: 如何将 Plate 编辑器与 react-hook-form 集成。 +--- + +虽然 Plate 通常用作**非受控**输入,但在某些场景下,您可能希望将编辑器集成到表单库中,比如 [**react-hook-form**](https://www.react-hook-form.com) 或 **shadcn/ui** 的 [**Form**](https://ui.shadcn.com/docs/components/form) 组件。本指南将介绍最佳实践和常见陷阱。 + +## 何时将 Plate 与表单集成 + +- **表单提交**:您希望在用户提交表单时,将编辑器内容与其他字段(如 ``、`