可聚焦元素 (Tabbable)

Loading...
Files
components/tabbable-demo.tsx
'use client';

import * as React from 'react';

import type { PlateElementProps } from 'platejs/react';

import { TabbablePlugin } from '@platejs/tabbable/react';
import {
  Plate,
  PlateElement,
  useFocused,
  usePlateEditor,
  useSelected,
} from 'platejs/react';

import { cn } from '@/lib/utils';
import { EditorKit } from '@/components/editor/editor-kit';
import { tabbableValue } from '@/registry/examples/values/tabbable-value';
import { Editor, EditorContainer } from '@/components/ui/editor';

export default function TabbableDemo() {
  const editor = usePlateEditor({
    plugins: [
      ...EditorKit,
      TabbablePlugin.configure({
        node: { component: TabbableElement, isElement: true, isVoid: true },
      }),
    ],
    value: tabbableValue,
  });

  return (
    <Plate editor={editor}>
      <EditorContainer variant="demo">
        <Editor />
      </EditorContainer>
    </Plate>
  );
}

export function TabbableElement({ children, ...props }: PlateElementProps) {
  const selected = useSelected();
  const focused = useFocused();

  return (
    <PlateElement {...props}>
      <div
        className={cn(
          'mb-2 p-2',
          selected && focused
            ? 'border-2 border-blue-500'
            : 'border border-gray-200'
        )}
        contentEditable={false}
      >
        <p>This is a void element.</p>
        <button type="button">Button 1</button>{' '}
        <button type="button">Button 2</button>
      </div>
      {children}
    </PlateElement>
  );
}

功能特性

  • 确保编辑器中可聚焦元素之间的标签顺序一致
  • 管理空元素与外部DOM元素之间的焦点切换

套件使用

安装

最快捷的方式是使用 TabbableKit,它包含预配置的 TabbablePlugin 和智能查询逻辑,可避免与其他插件冲突。

'use client';
 
import { TabbablePlugin } from '@platejs/tabbable/react';
import { KEYS } from 'platejs';
 
export const TabbableKit = TabbablePlugin.configure(({ editor }) => ({
  node: {
    isElement: true,
  },
  options: {
    query: () => {
      if (editor.api.isAt({ start: true }) || editor.api.isAt({ end: true }))
        return false;
 
      return !editor.api.some({
        match: (n) => {
          return !!(
            (n.type &&
              [
                KEYS.codeBlock,
                KEYS.li,
                KEYS.listTodoClassic,
                KEYS.table,
              ].includes(n.type as any)) ||
            n.listStyleType
          );
        },
      });
    },
  },
  override: {
    enabled: {
      indent: false,
    },
  },
}));

添加套件

import { createPlateEditor } from 'platejs/react';
import { TabbableKit } from '@/components/editor/plugins/tabbable-kit';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件,
    ...TabbableKit,
  ],
});

手动配置

安装

pnpm add @platejs/tabbable

添加插件

import { TabbablePlugin } from '@platejs/tabbable/react';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件,
    TabbablePlugin,
  ],
});

配置插件

import { TabbablePlugin } from '@platejs/tabbable/react';
import { createPlateEditor } from 'platejs/react';
import { KEYS } from 'platejs';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件,
    TabbablePlugin.configure({
      options: {
        query: (event) => {
          // 在列表或代码块中禁用
          const inList = editor.api.some({ match: { type: KEYS.li } });
          const inCodeBlock = editor.api.some({ match: { type: KEYS.codeBlock } });
          return !inList && !inCodeBlock;
        },
        globalEventListener: true,
        isTabbable: (tabbableEntry) => 
          editor.api.isVoid(tabbableEntry.slateNode),
      },
    }),
  ],
});
  • options.query: 根据编辑器状态动态启用/禁用插件的函数
  • options.globalEventListener: 为true时,将事件监听器添加到document而非编辑器
  • options.isTabbable: 判断哪些元素应包含在标签顺序中的函数

高级用法

与其他插件的冲突

Tabbable插件可能会与处理Tab键的其他插件产生冲突,例如:

  • 列表插件
  • 代码块插件
  • 缩进插件

使用query选项在Tab键应由其他插件处理时禁用Tabbable插件:

query: (event) => {
  const inList = editor.api.some({ match: { type: KEYS.li } });
  const inCodeBlock = editor.api.some({ match: { type: KEYS.codeBlock } });
  return !inList && !inCodeBlock;
},

如果使用缩进插件,可以仅在选中特定类型节点(如void节点)时启用Tabbable插件:

query: (event) => !!editor.api.some({
  match: (node) => editor.api.isVoid(node),
}),

非空Slate节点

将为编辑器中的每个可聚焦DOM元素创建一个TabbableEntry,使用tabbable NPM包确定。然后使用isTabbable过滤可聚焦列表。

默认情况下,isTabbable仅对void Slate节点内的entry返回true。可以覆盖isTabbable以支持其他类型Slate节点中包含的DOM元素:

// 启用CUSTOM_ELEMENT内的可聚焦DOM元素
isTabbable: (tabbableEntry) => (
  tabbableEntry.slateNode.type === CUSTOM_ELEMENT ||
  editor.api.isVoid(tabbableEntry.slateNode)
),

编辑器外部的DOM元素

某些情况下,可能需要允许用户从编辑器切换到外部渲染的DOM元素(如交互式弹出框)。

为此,覆盖insertTabbableEntries返回TabbableEntry对象数组,每个对象对应一个要包含在可聚焦列表中的外部DOM元素。TabbableEntryslateNodepath应引用当DOM元素可聚焦时用户光标所在的Slate节点。

globalEventListener选项设为true以确保Tabbable插件能将用户焦点返回到编辑器。

例如,如果DOM元素在选中链接时出现,slateNodepath应为该链接的节点。

// 将.my-popover内的按钮添加到可聚焦列表
globalEventListener: true,
insertTabbableEntries: (event) => {
  const [selectedNode, selectedNodePath] = editor.api.node(editor.selection);
 
  return [
    ...document.querySelectorAll('.my-popover > button'),
  ].map((domNode) => ({
    domNode,
    slateNode: selectedNode,
    path: selectedNodePath,
  }));
},

插件

TabbablePlugin

管理可聚焦元素间标签顺序的插件。

Options

Collapse all

    动态启用/禁用插件。

    • 默认值: () => true

    将事件监听器添加到document而非编辑器。

    • 默认值: false

    添加编辑器外的额外可聚焦entry。

    • 默认值: () => []

    判断元素是否应可聚焦。

    • 默认值: (tabbableEntry) => editor.api.isVoid(tabbableEntry.slateNode)

类型

TabbableEntry

定义可聚焦entry的属性。

Attributes

Collapse all

    表示可聚焦entry的HTML元素。

    对应的Slate节点。

    文档中Slate节点的路径。