Add product features to Classic
Distinguish content extensions from interface plugins, then add actions, sidebars and file services.
The note group changes document content. Toolbars, sidebars and file menus belong to product plugins. This chapter reuses Classic's interface and adds “My product” and a product inspector.
A custom workspace can compose its own components. Classic plugin declarations require a Classic instance; putting them into Core's extensions does not create a toolbar.
Try the feature
Find “My product” on the Home ribbon. Insert a note and observe the content change. Open the inspector to read product state; closing it does not change content.
1. Declare actions
plugin/notebook.ts defines the plugin, ribbon group and actions. The group's tabId chooses an existing tab; each action's groupId chooses its group.
Actions receive the current editor, product instance and ui. Use Core commands to change content and ui to open panels. The same action can be placed elsewhere without another content command system.
Availability, executability, active state and disabled reasons are separate queries. Classic uses them to refresh controls when selection or read-only state changes.
2. Create and update a sidebar
plugin/panel.ts connects Classic to Vue; plugin/panel.vue renders the inspector. Mounting returns element, update and destroy for DOM, context refresh and cleanup.
Create the Vue root on open, update it when locale or selection changes, and release it on close. Do not recreate subscriptions on every plugin update.
3. Register a file service
The example registers notebook-backup. The download action calls instance.exportDocument('notebook-backup'), then hands its data, filename and MIME to the browser.
Services own conversion. File-menu placement is declared separately, allowing the same service with different menu layouts.
File responsibilities
| File | Purpose |
|---|---|
plugin/notebook.ts | Plugin, groups, actions and file service |
plugin/panel.ts | Connect sidebar lifecycle to Vue |
plugin/panel.vue | Inspector interface |
plugin/editor.vue | Create Classic and install the plugin |
import { defineClassicEditorPlugin, createClassicEditorRibbonAction, type ClassicEditorActionDefinition } from "@quirekit/classic-editor/vue";
import { readBackup, createBackup } from "../files/backup";
import { renderNotebookPanel } from "./panel";
/**
* Builds a fresh product plugin with a ribbon group, typed action, panel and backup service.
* @returns Declarations consumed by Classic's registry; they do not replace Core mutation semantics.
*/
export function createNotebookPlugin() {
const open: ClassicEditorActionDefinition = {
id: "notebook.inspect", label: { "zh-CN": "产品检查器", en: "Product inspector" }, order: 10,
run(context) { context.ui.openSidebarPanel("right", "notebook.panel"); return true; },
isActive(context) { return context.ui.state.rightSidebar.visible && context.ui.state.rightSidebar.activePanelId === "notebook.panel"; },
};
const insert: ClassicEditorActionDefinition = {
id: "notebook.insert", label: { "zh-CN": "插入说明", en: "Insert note text" }, order: 20,
canExecute(context) { return context.editor.isEditable && context.editor.can().insertContent({ type: "paragraph" }); },
run(context) { return context.editor.chain().focus().insertContent({ type: "paragraph", content: [{ type: "text",
text: context.locale === "en" ? "Inserted by a product plugin." : "由产品插件插入的说明。" }] }).run(); },
};
return defineClassicEditorPlugin({ id: "notebook",
contributions: [
{ kind: "ribbon-group", id: "notebook.group", tabId: "home", label: { "zh-CN": "我的产品", en: "My product" }, order: 1 },
createClassicEditorRibbonAction("notebook.inspect.control", "notebook.group", open),
createClassicEditorRibbonAction("notebook.insert.control", "notebook.group", insert),
],
sidebarPanels: [{ kind: "sidebar-panel", id: "notebook.panel", side: "right", label: { "zh-CN": "产品检查器", en: "Product inspector" }, order: 10, render: renderNotebookPanel }],
services: [{ id: "notebook-backup", label: { "zh-CN": "产品备份", en: "Product backup" },
format: { id: "notebook-json", extensions: [".json"], mimeTypes: ["application/json"] },
export(context) { return { data: JSON.stringify(createBackup(context.editor, "word-v1")), fileName: "notebook.json", mimeType: "application/json" }; },
import(context, input) {
if (!context.editor.isEditable) throw new Error("The product is read only");
const source = typeof input.data === "string" ? input.data : new TextDecoder().decode(input.data);
const document = readBackup(source, "word-v1", context.editor.schema);
context.commit(() => context.editor.commands.replaceDocument(document, { errorOnInvalidContent: true }));
},
}],
});
}
See Classic plugins and Classic API. For failures and asynchronous loading, continue with files.