Plugin System

@tangramino/base-editor 提供了一套完整的插件系统,用于扩展编辑器功能。

definePlugin

创建类型安全的编辑态插件的工厂函数。

import { definePlugin } from '@tangramino/base-editor';

// 无参数插件
const myPlugin = definePlugin(() => ({
  id: 'my-plugin',
  onInit(ctx) {
    console.log('Plugin initialized');
  },
}));

// 带参数插件
const configPlugin = definePlugin<EditorPlugin, { debug: boolean }>(
  (options) => ({
    id: 'config-plugin',
    onInit(ctx) {
      if (options.debug) console.log('Debug mode');
    },
  }),
);

EditorPlugin 接口

编辑态插件的完整接口定义。

interface EditorPlugin
  extends PluginMeta,
    PluginLifecycle,
    MaterialHooks,
    SchemaHooks,
    EditorHooks {}

PluginMeta - 插件元数据

interface PluginMeta {
  // 唯一标识
  id: string;
  // 依赖的其他插件 ID
  dependencies?: string[];
  // 优先级,数字越小越先执行,默认 100
  priority?: number;
}

PluginLifecycle - 生命周期钩子

interface PluginLifecycle {
  // 插件初始化,在 EditorProvider 挂载时调用
  // 可返回清理函数
  onInit?: (ctx: PluginContext) => (() => void) | void;

  // 插件销毁,在 EditorProvider 卸载时调用
  onDispose?: (ctx: PluginContext) => void;
}

MaterialHooks - 物料转换钩子

interface MaterialHooks {
  // 转换 materials,必须返回新数组
  // 用于添加默认配置、包装组件等
  transformMaterials?: (materials: Material[]) => Material[];
}

SchemaHooks - Schema 操作钩子

Schema 操作钩子接收操作级别的详细信息,而不仅是最终的 schema 状态。这使得插件能够获取操作的具体细节(如元素 ID、父元素、索引等),实现增量同步等高级功能。

// 插入操作详情
interface InsertOperation {
  elementId: string;
  parentId: string;
  index: number;
  element: { type: string; props: Record<string, unknown>; hidden?: boolean };
}

// 移动操作详情
interface MoveOperation {
  elementId: string;
  oldParentId: string;
  oldIndex: number;
  newParentId: string;
  newIndex: number;
  mode: 'same-level' | 'cross-level';
}

// 删除操作详情
interface RemoveOperation {
  elementId: string;
  parentId: string;
  index: number;
  element: Element; // 被删除元素的完整快照(包含子元素)
}

// 属性更新操作详情
interface UpdatePropsOperation {
  elementId: string;
  props: Record<string, unknown>; // 新属性
  oldProps: Record<string, unknown>; // 旧属性
}

interface SchemaHooks {
  // 插入前,返回 false 可取消操作
  onBeforeInsert?: (schema: Schema, operation: InsertOperation) => boolean | void;
  // 插入后
  onAfterInsert?: (schema: Schema, operation: InsertOperation) => void;

  // 移动前,返回 false 可取消操作
  onBeforeMove?: (schema: Schema, operation: MoveOperation) => boolean | void;
  // 移动后
  onAfterMove?: (schema: Schema, operation: MoveOperation) => void;

  // 删除前,返回 false 可取消操作
  onBeforeRemove?: (schema: Schema, operation: RemoveOperation) => boolean | void;
  // 删除后
  onAfterRemove?: (schema: Schema, operation: RemoveOperation) => void;

  // 属性更新前,返回 false 可取消操作
  onBeforeUpdateProps?: (schema: Schema, operation: UpdatePropsOperation) => boolean | void;
  // 属性更新后
  onAfterUpdateProps?: (schema: Schema, operation: UpdatePropsOperation) => void;
}

使用示例

const myPlugin = definePlugin(() => ({
  id: 'my-plugin',
  
  onAfterInsert(schema, operation) {
    console.log('元素已插入:', {
      elementId: operation.elementId,
      parentId: operation.parentId,
      index: operation.index,
      elementType: operation.element.type,
    });
  },
  
  onAfterMove(schema, operation) {
    console.log('元素已移动:', {
      elementId: operation.elementId,
      from: `${operation.oldParentId}[${operation.oldIndex}]`,
      to: `${operation.newParentId}[${operation.newIndex}]`,
    });
  },
  
  onAfterRemove(schema, operation) {
    console.log('元素已删除:', {
      elementId: operation.elementId,
      parentId: operation.parentId,
      removedElement: operation.element,
    });
  },
  
  onAfterUpdateProps(schema, operation) {
    console.log('属性已更新:', {
      elementId: operation.elementId,
      changedProps: operation.props,
      oldProps: operation.oldProps,
    });
  },
}));

EditorHooks - 编辑器交互钩子

interface EditorHooks {
  // 元素被激活/选中时
  onElementActivate?: (
    element: Element & { material: Material },
    parentChain: (Element & { material: Material })[],
  ) => void;

  // 元素取消激活时
  onElementDeactivate?: (element: Element) => void;

  // 画布更新完成时
  onCanvasUpdated?: (ctx: PluginContext) => void;
}

PluginContext - 插件上下文

插件可以通过上下文访问编辑器能力。

interface PluginContext {
  // 编辑器引擎实例
  engine: Engine;
  // 获取当前 schema
  getSchema: () => Schema;
  // 更新 schema
  setSchema: (schema: Schema) => void;
  // 获取 materials
  getMaterials: () => Material[];
  // 获取其他已加载的插件实例
  getPlugin: <T extends EditorPlugin>(id: string) => T | undefined;
}

内置插件

historyPlugin - 历史记录插件

提供撤销/重做功能。

import { historyPlugin, type HistoryPlugin } from '@tangramino/base-editor';

// 使用
const plugins = [
  historyPlugin({ limit: 50 })
];

// 在组件中使用
function ToolBar() {
  const { getPlugin } = usePluginCore();
  const history = getPlugin<HistoryPlugin>('history');

  return (
    <div>
      <button
        onClick={() => history?.undo()}
        disabled={!history?.canUndo()}
      >
        撤销
      </button>
      <button
        onClick={() => history?.redo()}
        disabled={!history?.canRedo()}
      >
        重做
      </button>
    </div>
  );
}

配置选项

选项类型默认值说明
limitnumber100历史记录最大数量

扩展方法

方法类型说明
undo() => void撤销操作
redo() => void重做操作
canUndo() => boolean是否可以撤销
canRedo() => boolean是否可以重做
clear() => void清空历史记录

modePlugin - 模式插件

设置所有元素的编辑/渲染模式。

import { modePlugin } from '@tangramino/base-editor';

// 编辑模式
const plugins = [modePlugin('edit')];

// 渲染模式
const plugins = [modePlugin('render')];

参数

参数类型说明
mode'edit' | 'render'模式类型

创建自定义插件

基础插件

import { definePlugin } from '@tangramino/base-editor';

const logPlugin = definePlugin(() => ({
  id: 'log-plugin',
  priority: 10,

  onInit(ctx) {
    console.log('Editor initialized');

    // 返回清理函数
    return () => {
      console.log('Cleanup');
    };
  },

  onDispose(ctx) {
    console.log('Editor disposed');
  },

  onAfterInsert(schema, insertedId) {
    console.log('Element inserted:', insertedId);
  },

  onCanvasUpdated(ctx) {
    console.log('Canvas updated');
  },
}));

带状态的插件

const counterPlugin = definePlugin(() => {
  // 插件内部状态
  let insertCount = 0;
  let removeCount = 0;

  return {
    id: 'counter-plugin',

    onAfterInsert() {
      insertCount++;
      console.log('Total inserts:', insertCount);
    },

    onAfterRemove() {
      removeCount++;
      console.log('Total removes:', removeCount);
    },

    // 对外暴露方法
    getStats: () => ({ insertCount, removeCount }),
  };
});

依赖其他插件

const dependentPlugin = definePlugin(() => ({
  id: 'dependent-plugin',
  dependencies: ['history'],

  onInit(ctx) {
    // 获取依赖的插件
    const history = ctx.getPlugin<HistoryPlugin>('history');

    if (history) {
      // 使用 history 插件的功能
    }
  },
}));

转换物料

const materialEnhancePlugin = definePlugin(() => ({
  id: 'material-enhance',

  transformMaterials(materials) {
    return materials.map((material) => ({
      ...material,
      // 为所有物料添加默认的 className
      defaultProps: {
        ...material.defaultProps,
        className: `tg-${material.type.toLowerCase()}`,
      },
    }));
  },
}));

拦截操作

const validationPlugin = definePlugin(() => ({
  id: 'validation-plugin',

  // 拦截插入操作
  onBeforeInsert(schema, targetId, element) {
    // 禁止在特定容器中添加元素
    if (targetId === 'locked-container') {
      console.warn('Cannot insert into locked container');
      return false;
    }
    return true;
  },

  // 拦截删除操作
  onBeforeRemove(schema, targetId) {
    // 禁止删除根元素
    if (targetId === schema.layout.root) {
      console.warn('Cannot remove root element');
      return false;
    }
    return true;
  },

  // 拦截属性更新
  onBeforeUpdateProps(schema, targetId, props) {
    // 验证属性值
    if (props.width && typeof props.width !== 'number') {
      console.warn('Width must be a number');
      return false;
    }
    return true;
  },
}));

插件注册顺序

插件按以下规则排序执行:

  1. 依赖关系: 被依赖的插件先执行
  2. 优先级: priority 值小的先执行(默认 100)
  3. 注册顺序: 同优先级按注册顺序执行
const plugins = [
  pluginA(), // priority: 100
  pluginB(), // priority: 50, 先于 A 执行
  pluginC(), // priority: 100, dependencies: ['pluginB'], 在 B 之后执行
];