Menus, toolbars and command placements
Menus, toolbars and command placements
Section intitulée « Menus, toolbars and command placements »Commands are declared once and placed anywhere. The flow does not change: a menu entry or toolbar button emits the command’s event, a handler updates the store, and a state source derives what the entry should look like.
import { fromSlice } from "@/kernel"; // bundled plugins; runtime plugins receive nothing extra
// 1. Declare the command, with an icon and (optionally) a default state.ctx.registerCommand({ id: "notes.save", title: ctx.i18n.key("command.save"), // shown in the palette, menus, tooltips shortcut: "mod+s", event: "notes/document.save", icon: Save, state: fromSlice(slice, (state, context) => ({ enabled: Boolean(targetDoc(context, state)?.dirty), icon: targetDoc(context, state)?.dirty ? Save : CheckCircle2, })),});
// 2. Place it.ctx.place({ commandId: "notes.save", menu: "file", group: "save", order: 30 });ctx.place({ commandId: "notes.save", toolbar: "view:notes:editor", payload: docPayload });ctx.place({ commandId: "notes.save", menu: "notes.document", payload: docPayload });
// 3. Menus: top-level (menubar), submenu, or standalone (context / dropdown).ctx.registerMenu({ id: "notes", title: ctx.i18n.key("menu.notes"), order: 20 });ctx.registerMenu({ id: "notes.recent", parent: "notes", title: ctx.i18n.key("menu.recent") });ctx.registerMenu({ id: "notes.document", title: ctx.i18n.key("menu.document"), standalone: true });| Target | Rendered by |
|---|---|
menu: "file" / "view" |
Shell menubar (File, View are built in) |
menu: "<your-menu>" |
Your own menubar menu, submenu or context menu |
toolbar: "shell:main" |
Global toolbar under the top bar |
toolbar: "view:<pluginId>:<viewId>" |
Above the active editor tab of that view |
toolbar: "panel:<pluginId>:<contributionId>" |
Header of that side/bottom panel |
toolbar: "<anything>" |
Wherever you render <ctx.ui.Toolbar id=… /> |
place({ …, dropdown: "menuId" }) |
Toolbar button that opens a menu instead |
Ids are namespaced: a view registered as editor by plugin notes is
notes:editor, a contribution documents is notes:documents.
State sources
Section intitulée « State sources »A state source is { subscribe(listener), get(context) } returning any of
{ enabled, checked, icon, title }:
enabled: falsegreys the entry, disables the button, and blocks the shortcut and the palette.checkedturns the entry into a checkbox item and the button into a pressed toggle (the shell’s panel toggles work this way).icon/titleswap the visual per state (e.g. a check mark once saved).
Helpers: fromSlice(slice, derive), fromObservable(observable, derive),
combineStates(a, b). A state on a placement overrides the command’s own
state for that placement only.
Every placement receives a context: the editor toolbar passes
{ tabId, params } of the active tab, ctx.ui.ContextMenu passes whatever
you give it. Both payload and state.get may be functions of that context,
so one command can target “the active document” from the File menu and “the
document under the pointer” from a context menu.
const { ContextMenu, Toolbar } = ctx.ui;
<ContextMenu menuId="notes.document" context={{ docId: doc.id }}> <li>{doc.title}</li></ContextMenu>
<Toolbar id="notes:inline" context={{ docId }} />Hovering a toolbar button shows the command title and its shortcut. Adjacent
entries with a different group are separated. Everything a plugin registers
is removed when it is disabled.
Documentation sous licence CC BY 4.0 · © les contributeurs de SCFlow ·Retour à supplychainflow.org