# Custom plugins

Connect your own behavior to Core without a second editing state.

This page defines shared Core behavior. Classic and Block can install it through extensions, provided its commands exist and the selected View is validated. Product controls use their own adapter contracts; see [editor integration](/en/plugins/compatibility).

Suppose you want a keyboard shortcut that makes the current text bold. This adds behavior without introducing a document structure, so it uses `Extension`.

This chapter adds a custom shortcut to the editor from [Quick start](/en/guide/quick-start).

## 1. Define the shortcut

Create `shortcuts.ts`. Return a map of keys to handlers from `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` means Command on macOS and Ctrl elsewhere. The handler invokes the existing `toggleBold()` command. Its return value tells the editor whether the key was handled; `false` lets other shortcuts try it.

`name` identifies this extension within the editor; it is not an npm package name. StarterKit supplies bold. The shortcut only triggers that capability and shares its document and undo history.

## 2. Add it to the editor

Import the extension in the file that creates Editor and add it to the existing `extensions` list:

```ts [When creating the editor]
import { writingShortcuts } from './shortcuts'

const editor = new Editor({
  element,
  mode: 'dom',
  extensions: [StarterKit, DomViewExtension, writingShortcuts],
  content: '<p>Select text and try the new shortcut.</p>',
})
```

This continues to use the `Editor`, `StarterKit`, `DomViewExtension` imports and `element` from Quick start. Install the extension once; no separate keyboard listener is needed on the host.

## Live effect

Select text below and press Command+Shift+B on macOS or Ctrl+Shift+B elsewhere, then undo. The button invokes the same bold command.

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

```

Use Node or Mark for new persistent content, defining attributes, parsing and serialization. Add separate Canvas or DOCX contributions when needed; a behavior extension does not automatically support every backend.

## Complete files

In the example, `core/shortcuts.ts` defines the shortcut and `core/create.ts` creates and mounts the editor. Select a file when you need its complete imports or initial content:

### 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 };
  },
});

```

For persistent custom content and multiple backends, continue to [the complete content extension](/en/build/extension). For ribbon, panels and file services, see [product plugins](/en/build/product-plugin).
