Plugin API
Core plugin registration and management API.
Element Definition
dmn.plugin.defineElement(definition): void
Defines a draggable overlay element. See Declarative API for details.
dmn.plugin.defineElement({
name: "My Plugin",
maxInstances: 1,
settings: {
color: { type: "color", default: "#ffffff", label: "Color" },
},
messages: {
ko: { "menu.create": "생성", "menu.delete": "삭제" },
en: { "menu.create": "Create", "menu.delete": "Delete" },
},
contextMenu: {
create: "menu.create",
delete: "menu.delete",
},
template: (state, settings, { html, t }) => html` <div>Content</div> `,
onMount: ({ setState, getSettings, onHook }) => {
return () => {
/* cleanup */
};
},
});PluginDefinition
The definition object passed to defineElement.
interface PluginDefinition {
/** Plugin name (shown in the context menu) */
name: string;
/** Maximum instance count (0 = unlimited, default) */
maxInstances?: number;
/**
* Whether the element can be resized in the grid
* @default false
*/
resizable?: boolean;
/**
* Which size axis to preserve when settings change
* Only applies when resizable is true
* - 'width': preserve width, height follows content
* - 'height': preserve height, width follows content
* - 'both': preserve both (default)
* - 'none': both follow content
* @default 'both'
*/
preserveAxis?: "width" | "height" | "both" | "none";
/**
* Resize anchor (fixed point when the size changes)
* @default "top-left"
*/
resizeAnchor?: ElementResizeAnchor;
/** Context menu setup */
contextMenu?: {
create?: string; // Create menu label
delete?: string; // Delete menu label
items?: PluginDefinitionContextMenuItem[];
};
/**
* Overlay state keys mirrored to the main window for menu predicates
* (opt-in, low-frequency). Only declared keys are sent on setState changes
*/
contextMenuStateKeys?: string[];
/**
* Settings UI mode
* @default "panel"
*/
settingsUI?: "panel" | "modal";
/** Settings schema */
settings?: Record<string, PluginSettingSchema>;
/** i18n messages (nested objects allowed, PluginMessages) */
messages?: Record<string, Record<string, unknown>>;
/** Preview state (for the main window) */
previewState?: Record<string, any>;
/** Template function */
template: (
state: Record<string, any>,
settings: Record<string, any>,
helpers: DisplayElementTemplateHelpers
) => DisplayElementTemplateResult | string;
/** Mount logic (runs only in the overlay) */
onMount?: (context: PluginDefinitionHookContext) => void | (() => void);
}ElementResizeAnchor
The type that specifies the fixed point when an element’s size changes.
type ElementResizeAnchor =
| "top-left" // Top-left (default)
| "top-center" // Top center
| "top-right" // Top-right
| "center-left" // Center-left
| "center" // Center
| "center-right" // Center-right
| "bottom-left" // Bottom-left
| "bottom-center" // Bottom center
| "bottom-right"; // Bottom-rightAnchor Behavior
When the size changes, the specified anchor position stays fixed and the element expands or shrinks in the other directions.
| Anchor | Behavior |
|---|---|
top-left | Expands/shrinks right and down |
center | Expands/shrinks evenly in all directions |
bottom-right | Expands/shrinks left and up |
PluginDefinitionHookContext
The context object passed to the onMount callback.
interface PluginDefinitionHookContext {
/** Update state */
setState: (updates: Record<string, any>) => void;
/** Get current settings */
getSettings: () => Record<string, any>;
/** Set the resize anchor */
setAnchor: (anchor: ElementResizeAnchor) => void;
/** Get the current resize anchor */
getAnchor: () => ElementResizeAnchor;
/**
* Register event hooks
* - "key": mapped key events
* - "rawKey": all raw input events
*/
onHook: (event: "key" | "rawKey", callback: (...args: any[]) => void) => void;
/** Expose functions for the context menu */
expose: (actions: Record<string, (...args: any[]) => any>) => void;
/** Current locale code */
locale: string;
/** Translation function */
t: PluginTranslateFn;
/** Subscribe to locale changes */
onLocaleChange: (listener: (locale: string) => void) => Unsubscribe;
/** Subscribe to settings changes */
onSettingsChange: (
listener: (
newSettings: Record<string, any>,
oldSettings: Record<string, any>
) => void
) => void;
}setAnchor / getAnchor
Dynamically change or read the resize anchor per instance.
onMount: ({ setAnchor, getAnchor, getSettings }) => {
// Check the current anchor
console.log("Current anchor:", getAnchor()); // "top-left" (default)
// Change the anchor to center
setAnchor("center");
// Change the anchor based on settings
const settings = getSettings();
if (settings.expandFromCenter) {
setAnchor("center");
}
},Anchor priority:
- Per-instance anchor set via
setAnchor() PluginDefinition.resizeAnchor(definition default)"top-left"(system default)
Settings Definition
dmn.plugin.defineSettings(definition): PluginSettingsInstance
Defines plugin-global settings independent of elements (defineElement). The
returned instance exposes get/set/open/reset/subscribe. See
Settings System for details.
dmn.plugin.defineSettings(
definition: PluginSettingsDefinition
): PluginSettingsInstanceconst pluginSettings = dmn.plugin.defineSettings({
settings: {
enabled: { type: "boolean", default: true, label: "Enable" },
theme: {
type: "select",
default: "dark",
options: [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
],
label: "Theme",
},
},
});PluginSettingSchema
Settings use a discriminated union of value settings and sections. Layout entries never appear in stored settings, defaults, getters, or change callbacks.
type PluginSettingSchema =
| {
type: "boolean" | "color" | "number" | "string" | "select";
default: string | number | boolean;
label: string;
min?: number;
max?: number;
step?: number;
options?: { label: string; value: string | number | boolean }[];
placeholder?: string;
visible?: boolean | ((settings: Record<string, unknown>) => boolean);
}
| {
type: "section";
label?: string; // optional caption above the card
visible?: boolean | ((settings: Record<string, unknown>) => boolean);
};A section starts a card and controls the whole group through visible. Its key is
a layout identifier and never appears in stored values, defaults, getters, or
change callbacks. Panel and modal settings UIs follow the same rules, and
visibility exceptions hide the affected item or group (fail-closed). The legacy
divider type was removed, so divider entries in existing plugins are ignored
as an unsupported type.
Lifecycle
dmn.plugin.registerCleanup(callback): void
Registers cleanup function called when plugin is unloaded.
const interval = setInterval(() => {
console.log("tick");
}, 1000);
dmn.plugin.registerCleanup(() => {
clearInterval(interval);
});Always clean up timers, event listeners, and DOM elements.
Storage
dmn.plugin.storage
Plugin-specific persistent storage. See Storage API for details.
dmn.plugin.storage.get<T = any>(key: string): Promise<T | null>
dmn.plugin.storage.set(key: string, value: any): Promise<void>
dmn.plugin.storage.remove(key: string): Promise<void>
dmn.plugin.storage.clear(): Promise<void>
dmn.plugin.storage.keys(): Promise<string[]>
dmn.plugin.storage.hasData(prefix: string): Promise<boolean>
dmn.plugin.storage.clearByPrefix(prefix: string): Promise<number>// Save
await dmn.plugin.storage.set("myKey", { value: 42 });
// Load
const data = await dmn.plugin.storage.get("myKey");
// Delete
await dmn.plugin.storage.remove("myKey");
// List keys
const keys = await dmn.plugin.storage.keys();Example Usage
Basic Plugin
// @id simple-counter
dmn.plugin.defineElement({
name: "Simple Counter",
maxInstances: 3,
resizeAnchor: "top-left",
settings: {
textColor: {
type: "color",
default: "#FFFFFF",
label: "Text Color",
},
},
previewState: {
count: 42,
},
template: (state, settings, { html }) => html`
<div style="color: ${settings.textColor}; padding: 16px;">
Count: ${state.count ?? 0}
</div>
`,
onMount: ({ setState, onHook }) => {
let count = 0;
onHook("key", ({ state }) => {
if (state === "DOWN") {
count++;
setState({ count });
}
});
},
});Dynamic Anchor Change
// @id dynamic-anchor-panel
dmn.plugin.defineElement({
name: "Dynamic Anchor Panel",
resizeAnchor: "top-left", // Default anchor
settings: {
expandFromCenter: {
type: "boolean",
default: false,
label: "Expand From Center",
},
},
template: (state, settings, { html }) => html`
<div style="padding: 16px; background: rgba(0,0,0,0.8);">
Anchor: ${state.currentAnchor ?? "top-left"}
</div>
`,
onMount: ({
setState,
getSettings,
setAnchor,
getAnchor,
onSettingsChange,
}) => {
// Set the initial anchor
const settings = getSettings();
const anchor = settings.expandFromCenter ? "center" : "top-left";
setAnchor(anchor);
setState({ currentAnchor: anchor });
// Update the anchor when settings change
onSettingsChange((newSettings, oldSettings) => {
if (newSettings.expandFromCenter !== oldSettings.expandFromCenter) {
const newAnchor = newSettings.expandFromCenter ? "center" : "top-left";
setAnchor(newAnchor);
setState({ currentAnchor: newAnchor });
}
});
},
});Stats Tracker
// @id stats-tracker
dmn.plugin.defineElement({
name: "Stats Tracker",
maxInstances: 1,
template: (state, settings, { html }) => html`
<div style="background: #222; padding: 12px; border-radius: 8px;">
<div>Total Keys: ${state.total ?? 0}</div>
<div>Session: ${state.session ?? 0}</div>
</div>
`,
onMount: ({ setState, onHook }) => {
let session = 0;
// Load saved total
dmn.plugin.storage.get("total").then((total) => {
setState({ total: total ?? 0 });
});
onHook("key", ({ state }) => {
if (state !== "DOWN") return;
session++;
dmn.plugin.storage.get("total").then((total) => {
const newTotal = (total ?? 0) + 1;
dmn.plugin.storage.set("total", newTotal);
setState({ total: newTotal, session });
});
});
},
});