# 自定义插件

把自己的行为接到 Core，不另建编辑状态。

适用范围：本页定义共享 Core 行为；Classic、Block 均可在 extensions 中接入，但需确保依赖命令已安装，并验证所选 View。产品控件使用各自的适配合同，见[编辑器接入方式](/plugins/compatibility)。

假设你希望用户按一个快捷键，就能给当前文字加粗。这里不需要新增文档结构，只需要把按键接到已有命令上。这样的功能使用 `Extension`。

本章沿用[快速开始](/guide/quick-start)的编辑器，在其中加入一个自己编写的快捷键扩展。

## 1. 定义快捷键

新建 `shortcuts.ts`，通过 `addKeyboardShortcuts()` 返回“按键 → 处理函数”的对应关系：

```ts [shortcuts.ts]
import { Extension } from '@quirekit/core'

export const writingShortcuts = Extension.create({
  name: 'writingShortcuts',
  addKeyboardShortcuts() {
    return {
      'Mod-Shift-b': () => this.editor?.commands.toggleBold() ?? false,
    }
  },
})
```

`Mod` 表示 macOS 的 Command，其他系统的 Ctrl。处理函数调用已有的 `toggleBold()`；返回值告诉编辑器这次按键是否已处理。`false` 表示没有处理，其他快捷键仍可继续尝试。

`name` 是这个扩展在编辑器中的名称，不是 npm 包名。粗体能力来自 StarterKit；快捷键只负责触发它，不另建一份文字或撤销历史。

## 2. 加到编辑器中

在创建 Editor 的文件中导入扩展，并加入原有的 `extensions`：

```ts [创建编辑器时]
import { writingShortcuts } from './shortcuts'

const editor = new Editor({
  element,
  mode: 'dom',
  extensions: [StarterKit, DomViewExtension, writingShortcuts],
  content: '<p>选择文字，试试新的快捷键。</p>',
})
```

这里继续使用快速开始中导入的 `Editor`、`StarterKit`、`DomViewExtension` 和挂载元素 `element`。只安装一次扩展即可；不需要再给宿主元素添加键盘监听。

## 实际效果

选中下方文字，按 macOS 的 Command+Shift+B，或其他系统的 Ctrl+Shift+B，然后撤销。按钮调用同一个粗体命令。

### core/editor.vue

```vue
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
import type { Editor } from "@quirekit/core";
import { QuireButton } from "@quirekit/ui/vue";
import { createExampleEditor } from "./create";
import { downloadJson } from "../shared/download";
import "@quirekit/pm-view/style/prosemirror.css";

const props = withDefaults(defineProps<{
  /** Initial copy and control language. */ locale?: "zh-CN" | "en";
  /** Visible controls for the capability being demonstrated. */ tools?: "formatting" | "structure";
}>(), { locale: "zh-CN" });
const host = ref<HTMLElement>();
const instance = shallowRef<Editor>();
const copy = computed(() => props.locale === "en"
  ? { bold: "Bold", undo: "Undo", save: "Download JSON", label: "Editable document" }
  : { bold: "加粗", undo: "撤销", save: "下载 JSON", label: "可编辑文档" });
onMounted(() => { if (host.value) instance.value = createExampleEditor(host.value, props.locale); });
onBeforeUnmount(() => instance.value?.destroy());
/** Downloads the actual editor state instead of a second copy maintained by the UI. */
function download(): void { if (instance.value) downloadJson(instance.value.getJSON(), "quirekit-document.json"); }
</script>
<template>
  <div class="core-example">
    <div class="core-example-tools">
      <QuireButton variant="outline" @click="instance?.chain().focus().toggleBold().run()">{{ copy.bold }}</QuireButton>
      <template v-if="tools === 'formatting'">
        <QuireButton variant="outline" @click="instance?.chain().focus().toggleItalic().run()">{{ locale === 'en' ? 'Italic' : '斜体' }}</QuireButton>
        <QuireButton variant="outline" @click="instance?.chain().focus().toggleUnderline().run()">{{ locale === 'en' ? 'Underline' : '下划线' }}</QuireButton>
      </template>
      <template v-if="tools === 'structure'">
        <QuireButton variant="outline" @click="instance?.chain().focus().toggleHeading({ level: 2 }).run()">{{ locale === 'en' ? 'Heading 2' : '二级标题' }}</QuireButton>
        <QuireButton variant="outline" @click="instance?.chain().focus().toggleBulletList().run()">{{ locale === 'en' ? 'Bullet list' : '项目列表' }}</QuireButton>
      </template>
      <QuireButton variant="outline" @click="instance?.chain().focus().undo().run()">{{ copy.undo }}</QuireButton>
      <QuireButton variant="outline" @click="download">{{ copy.save }}</QuireButton>
    </div>
    <div ref="host" class="core-example-document" role="group" :aria-label="copy.label" />
  </div>
</template>
<style scoped>
.core-example { padding: 20px; background: var(--qk-color-surface-page); }
.core-example-tools { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
.core-example-document { border: 1px solid var(--qk-color-border-default); border-radius: 6px; padding: 20px; text-align: start; }
.core-example-document :deep(.ProseMirror) { min-height: 180px; line-height: 1.8; }
/* A writing surface expresses focus through its caret and selection, without a second frame. */
.core-example-document :deep(.ProseMirror:focus), .core-example-document :deep(.ProseMirror:focus-visible) { outline: none; box-shadow: none; }
.core-example-document :deep(h2) { font-size: 22px; margin: 0 0 12px; }
.core-example-document :deep(p) { margin: 8px 0; }
</style>

```

### core/create.ts

```ts
import { Editor } from "@quirekit/core";
import { StarterKit } from "@quirekit/starter-kit";
import { DomViewExtension } from "@quirekit/dom-view";
import { writingShortcuts } from "./shortcuts";

/**
 * Mounts a minimal editor with the Tiptap-style Core API and an explicit native DOM view.
 * @param element - Browser-owned mount target.
 * @param locale - Language for initial sample copy only.
 * @returns The caller-owned editor; destroy it when its host unmounts.
 */
export function createExampleEditor(element: HTMLElement, locale: "zh-CN" | "en"): Editor {
  return new Editor({
    element,
    mode: "dom",
    extensions: [StarterKit, DomViewExtension, writingShortcuts],
    content: { type: "doc", content: [
      { type: "heading", attrs: { level: 2 }, content: [{ type: "text", text: locale === "en" ? "Start with your words." : "从你写下的文字开始。" }] },
      { type: "paragraph", content: [{ type: "text", text: locale === "en" ? "Select some text, make it bold, or add a new paragraph. This is a real QuireKit Core editor." : "选择一段文字，加粗，或另起一段。这里运行的是真实的 QuireKit Core 编辑器。" }] },
    ] },
  });
}

```

### core/shortcuts.ts

```ts
import { Extension } from "@quirekit/core";

/** A host-authored behavior extension using the same typed command registry as the toolbar. */
export const writingShortcuts = Extension.create({
  name: "writingShortcuts",
  /** @returns A keyboard binding that participates in the editor's existing history. */
  addKeyboardShortcuts() {
    return { "Mod-Shift-b": () => this.editor?.commands.toggleBold() ?? false };
  },
});

```

### shared/download.ts

```ts
/**
 * Downloads canonical JSON through a short-lived browser URL.
 * @param document - Serializable canonical content.
 * @param name - Download filename chosen by the host.
 */
export function downloadJson(document: unknown, name: string): void {
  const url = URL.createObjectURL(new Blob([JSON.stringify(document, null, 2)], { type: "application/json" }));
  const link = window.document.createElement("a");
  link.href = url;
  link.download = name;
  link.click();
  window.setTimeout(() => URL.revokeObjectURL(url), 1000);
}

```

新增持久内容时使用 Node 或 Mark，并定义属性、解析与序列化。若需要 Canvas 或 DOCX，再分别提供对应贡献；行为插件不会自动让新节点获得所有后端支持。

## 完整文件参考

示例中，`core/shortcuts.ts` 定义快捷键，`core/create.ts` 负责创建并挂载编辑器。需要对照完整导入和初始内容时，再选择相应文件：

### core/create.ts

```ts
import { Editor } from "@quirekit/core";
import { StarterKit } from "@quirekit/starter-kit";
import { DomViewExtension } from "@quirekit/dom-view";
import { writingShortcuts } from "./shortcuts";

/**
 * Mounts a minimal editor with the Tiptap-style Core API and an explicit native DOM view.
 * @param element - Browser-owned mount target.
 * @param locale - Language for initial sample copy only.
 * @returns The caller-owned editor; destroy it when its host unmounts.
 */
export function createExampleEditor(element: HTMLElement, locale: "zh-CN" | "en"): Editor {
  return new Editor({
    element,
    mode: "dom",
    extensions: [StarterKit, DomViewExtension, writingShortcuts],
    content: { type: "doc", content: [
      { type: "heading", attrs: { level: 2 }, content: [{ type: "text", text: locale === "en" ? "Start with your words." : "从你写下的文字开始。" }] },
      { type: "paragraph", content: [{ type: "text", text: locale === "en" ? "Select some text, make it bold, or add a new paragraph. This is a real QuireKit Core editor." : "选择一段文字，加粗，或另起一段。这里运行的是真实的 QuireKit Core 编辑器。" }] },
    ] },
  });
}

```

### core/shortcuts.ts

```ts
import { Extension } from "@quirekit/core";

/** A host-authored behavior extension using the same typed command registry as the toolbar. */
export const writingShortcuts = Extension.create({
  name: "writingShortcuts",
  /** @returns A keyboard binding that participates in the editor's existing history. */
  addKeyboardShortcuts() {
    return { "Mod-Shift-b": () => this.editor?.commands.toggleBold() ?? false };
  },
});

```

需要持久业务节点与多后端支持时，继续[完整内容扩展](/build/extension)；要增加功能区、侧栏和文件服务，继续[产品插件](/build/product-plugin)。
