Host contributions: your app is an extension
Everything your app plugs into the editor (keys, / and @ palettes, type-to-format rules, actions) is a contribution registered into the editor's ElementRegistry through DocumentExtension.register, exactly as the starter kit registers its own. There are no per-feature host callbacks: your app is just another extension.
DocumentHostExtension is an extension that owns no document elements. Give it an id and a register callback, and pass it after the defaults:
dart
import 'package:flutter/services.dart';
import 'package:vyuh_document/engine.dart';
import 'package:vyuh_document/paragraph_extension.dart';
import 'package:vyuh_document_editor/document_editor_extensions.dart';
import 'package:vyuh_document_editor/vyuh_document_editor.dart';
import 'package:vyuh_document_extension_divider/vyuh_document_extension_divider.dart';
// Your app's own action ids, declared like any extension's.
enum MentionId with EditorActionKey implements EditorActionId {
alice('mention-alice'),
bob('mention-bob');
const MentionId(this.key);
@override
final String key;
}
/// Everything this app plugs into the editor, registered the way the
/// starter kit registers its own: a key handler, a trigger, an input rule.
DocumentHostExtension notesHost({required VoidCallback closePanel}) =>
DocumentHostExtension(
id: 'notes-app',
register: (registry) => registry
// Escape closes the app's panel before the editor sees the key.
// Return ignored to let the editor run its own Escape steps.
..registerKeyHandler(
DocumentKeyHandler(
id: 'notes-app.close-panel',
handle: (key) {
if (key.key != LogicalKeyboardKey.escape) {
return DocumentKeyResult.ignored;
}
closePanel();
return DocumentKeyResult.handled;
},
),
)
// `@` after a space opens a people palette; the word typed after it
// is the query. Choosing a person removes `@` and the query.
..registerTrigger(
DocumentTriggerContribution(
id: 'notes-app.mention',
trigger: '@',
position: DocumentTriggerPosition.wordBoundary,
title: 'People',
items: (query, context) => rankDocumentTriggerItems([
for (final (id, name) in [
(MentionId.alice, 'Alice'),
(MentionId.bob, 'Bob'),
])
EditorAction(
id: id,
label: name,
group: ToolbarGroup.insert,
execute: (controller) => controller.insertText('@$name'),
),
], query),
),
)
// A line reading *** becomes a scene break when Enter would split
// it: the rule takes that Enter, as one undo step.
..registerInputRule(
InputRule(
id: 'notes-app.scene-break',
trigger: InputRuleTrigger.onSplit,
pattern: RegExp(r'^\*\*\*$'),
apply: (controller, match) {
final line = controller.textRange.extent.blockId;
controller.transaction(
(tx) => tx
..deleteText(line, start: match.start, end: match.end)
..insert(
DividerBlock(id: BlockId('break-${line.value}')),
at: BeforeBlockAnchor(line),
),
);
},
),
),
);Both editors install it the same way, from the extension list:
dart
// The runtime editor (package:vyuh_document_editor/vyuh_document_editor.dart).
final controller = DocumentEditorController(
extensions: [
...DocumentExtension.defaults,
notesHost(closePanel: panel.close),
],
);
DocumentEditor(controller: controller);
// The typed Document API editor
// (package:vyuh_document_editor/document_editor.dart).
DocumentEditor(
controller: documentController,
extensions: [
...DocumentExtension.defaults,
notesHost(closePanel: panel.close),
],
);This is the one way in: there is no configuration-level host list, and the extension list is fixed for a controller's lifetime, so the host's contributions install in the same batch as the starter kit's, after them. Registering a contribution with a starter-kit id replaces it, and each registry has an unregister… call to remove one. A host turns a built-in off by id, never with a global switch.
This page covers keys, triggers and input rules. Actions, the right-click menu, inline interactions and the action catalog are on Configuration.
Keys
Every key press reaches one key path: the registered DocumentKeyHandlers, highest priority first, until one returns DocumentKeyResult.handled. A handler that returns ignored lets the key fall through to the next one and, past the last, to the platform (text input, focus traversal).
The editor's own key behaviour is registered on the same path, in bands:
Band (DocumentKeyPriority) | Value | Built-in handlers |
|---|---|---|
host | 1000 | The default for your handlers: before every built-in |
triggerSession | 900 | An open / or @ palette: arrows, Enter, Tab, Escape |
camera | 700 | Zoom chords; view-mode reading keys |
gesture | 600 | Escape cancels an inline drag or pointer gesture |
selection | 500 | Escape out of a frame, rectangle, node, header or note, or the highlighter; Enter into a frame; Enter or Space on a form field; Enter on a one-line surface |
platform | 400 | The web's vertical arrows; Command+Left/Right |
bindings | 300 | Every EditorAction.activators chord |
fallback | 100 | For your handlers that should act only when nothing built in did |
Handlers at or above DocumentKeyPriority.admitted (650) run before the editor admits the current layout, so they must not need document geometry; the rest run as one admitted keyboard interaction. While the platform IME is composing, it owns plain keys: no handler sees them. Built-in handler ids are on DocumentKeyHandlerId; registering one replaces that step.
Override a key with an action
A chord belongs to the last registered action whose activators match. To give a key a new meaning, register an action with that activator. An action that is hidden or disabled declines its key, and the key falls through:
dart
registry.registerAction(
EditorAction(
id: AppActionId.palette,
label: 'Command palette',
group: ToolbarGroup.review,
activators: const [
SingleActivator(LogicalKeyboardKey.keyK, meta: true),
SingleActivator(LogicalKeyboardKey.keyK, control: true),
],
transactionKind: DocumentCanonicalTransactionKind.none,
execute: (_) => openPalette(),
),
);Find is an action too. SearchActionId.find (Command/Control+F) opens the editor's search surface when one is mounted; register your own SearchActionId.find to open your find instead; the key, the Edit menu and the ribbon's Find all run that one action. Without a search surface the action is disabled and the chord falls through to the browser.
To remove a chord but keep its action in menus and toolbars, register the action again without it: registry.requireAction(id).copyWith(activators: []).
Triggers
A DocumentTriggerContribution is a typed trigger: its trigger text at a position (lineStart, wordBoundary or anywhere), followed by word characters up to the caret. Those characters are the query, so a fast typist's @al is one trigger with the query al; a space ends it. Offsets are document text units, where an inline atom is one unit.
The editor detects triggers itself: no host polling. It opens them at a collapsed caret in any editable text story, table cells included, but never in code or other literal source. Its items are EditorActions from items(query, context); rankDocumentTriggerItems ranks them by label. Choosing one removes the trigger and its query, then runs the action.
The default presenter is the editor's command palette. While you type it takes no focus; Enter or Tab chooses the first item, an arrow key moves into the list, and Escape dismisses it until you type a new trigger. Pass presenter to build your own list; the editor still places it under the trigger and owns dismissal.
The starter kit registers / at the start of a line as DocumentTriggerId.slash, the insert palette. Replace it by registering a trigger with that id, or remove it with unregisterTrigger.
Input rules
An InputRule matches the text before the caret, in document text units. Its trigger says when it runs:
InputRuleTrigger.afterText(the default) runs after typed text, as the starter kit's#,>,-,1.,[ ],```and URL rules do.triggerCharactersskips the match for other keystrokes.InputRuleTrigger.onSplitruns when Enter would split the block. A matching rule takes that Enter instead of the split, so its edit (onecontroller.transaction, say) is the Enter's one undo step.
The first registered rule that matches wins. The starter kit's rule ids are on DocumentInputRuleId: register a rule with one of them to replace it, or unregisterInputRule it.
Try them live: host key handler and host triggers and input rules.