Skip to content

Input System

The input system translates browser events into editor transactions. It consists of several registries and handlers that plugins use to register keymaps, input rules, and file handlers.

Facade that coordinates all input-related handlers. Created internally by the editor.

import { InputManager } from '@notectl/core';
import type { InputManagerDeps } from '@notectl/core';
interface InputManagerDeps {
readonly getState: () => EditorState;
readonly dispatch: (tr: Transaction) => void;
readonly syncSelection: () => void;
readonly undo: () => void;
readonly redo: () => void;
readonly schemaRegistry?: SchemaRegistry;
readonly keymapRegistry?: KeymapRegistry;
readonly inputRuleRegistry?: InputRuleRegistry;
readonly fileHandlerRegistry?: FileHandlerRegistry;
readonly isReadOnly: () => boolean;
readonly getPasteInterceptors?: () => readonly PasteInterceptorEntry[];
readonly getTextDirection?: (element: HTMLElement) => 'ltr' | 'rtl';
readonly navigateFromGapCursor?: (
state: EditorState,
direction: 'left' | 'right' | 'up' | 'down',
container?: HTMLElement,
) => Transaction | null;
}
const manager = new InputManager(contentElement, deps);
PropertyTypeDescription
compositionTrackerCompositionTrackerTracks IME composition state
MethodDescription
destroy()Removes all event listeners and cleans up resources

Stores keymaps registered by plugins, organized by priority level.

import { KeymapRegistry } from '@notectl/core';
const registry = new KeymapRegistry();
MethodSignatureDescription
registerKeymap(keymap: Keymap, options?: KeymapOptions) => voidRegister a keymap at a priority level
getKeymaps() => readonly Keymap[]Get all keymaps (flat list)
getKeymapsByPriority() => { context, navigation, default }Get keymaps grouped by priority
removeKeymap(keymap: Keymap) => voidRemove a specific keymap
clear() => voidRemove all keymaps

Keymaps are dispatched in priority order: context > navigation > default.

type KeymapPriority = 'context' | 'navigation' | 'default';
interface KeymapOptions {
readonly priority?: KeymapPriority;
}
PriorityUse Case
contextContext-sensitive shortcuts (e.g. code block Tab handling)
navigationCaret/selection movement (e.g. arrow keys, Home/End)
defaultGeneral commands (e.g. Ctrl+B for bold) — this is the default
type KeymapHandler = () => boolean;
type Keymap = Readonly<Record<string, KeymapHandler>>;

A Keymap maps key descriptors to handlers. Handlers return true if they consumed the event.

Key descriptors use the format Mod-B, Shift-Enter, Alt-ArrowUp, etc. Mod maps to Cmd on macOS and Ctrl on other platforms.

Normalizes a KeyboardEvent into a consistent key descriptor string. Format: "Mod-Shift-Alt-Key" where Mod = Ctrl/Cmd.

import { normalizeKeyDescriptor } from '@notectl/core';
// Takes a KeyboardEvent, not a string
element.addEventListener('keydown', (e) => {
const descriptor = normalizeKeyDescriptor(e);
// e.g. 'Mod-B', 'Mod-Shift-1', 'Enter', 'Space'
});
function normalizeKeyDescriptor(e: KeyboardEvent): string

Stores pattern-based text transform rules registered by plugins.

import { InputRuleRegistry } from '@notectl/core';
const registry = new InputRuleRegistry();
MethodSignatureDescription
registerInputRule(rule: InputRule) => voidRegister an input rule
getInputRules() => readonly InputRule[]Get all registered rules
removeInputRule(rule: InputRule) => voidRemove a specific rule
clear() => voidRemove all rules
interface InputRule {
readonly pattern: RegExp;
handler(
state: EditorState,
match: RegExpMatchArray,
start: number,
end: number,
): Transaction | null;
}

Input rules are matched against text as the user types. When a pattern matches, the handler is called with the match and can return a transaction to apply.

Example — auto-convert # to heading:

const headingRule: InputRule = {
pattern: /^(#{1,6})\s$/,
handler(state, match, start, end) {
const level = match[1].length;
return state.transaction('input')
.deleteTextAt(blockId, start, end)
.setBlockType(blockId, nodeType('heading'), { level })
.build();
},
};

Manages handlers for dropped or pasted files, matched by MIME type patterns.

import { FileHandlerRegistry } from '@notectl/core';
const registry = new FileHandlerRegistry();
MethodSignatureDescription
registerFileHandler(pattern: string, handler: FileHandler) => voidRegister a handler for a MIME pattern
getFileHandlers() => readonly FileHandlerEntry[]Get all registered handlers
matchFileHandlers(mimeType: string) => FileHandler[]Find handlers matching a MIME type
removeFileHandler(handler: FileHandler) => voidRemove a specific handler
clear() => voidRemove all handlers
type FileHandler = (
file: File,
position: Position | null,
) => boolean | Promise<boolean>;
interface FileHandlerEntry {
readonly pattern: string;
readonly handler: FileHandler;
}

Patterns support wildcards:

PatternMatches
image/*image/png, image/jpeg, etc.
text/plainOnly text/plain
*/*Any MIME type

Example:

registry.registerFileHandler('image/*', async (file, position) => {
const url = URL.createObjectURL(file);
// Insert image block at position
return true; // handled
});

Handles copy and cut operations, serializing the selection to text/plain and text/html clipboard formats.

import { ClipboardHandler } from '@notectl/core';
const handler = new ClipboardHandler(element, {
getState: () => editor.getState(),
dispatch: (tr) => editor.dispatch(tr),
schemaRegistry,
syncSelection: () => { /* ... */ },
isReadOnly: () => false,
});

The constructor accepts an options object with the following shape (the type itself is not exported):

{
readonly getState: () => EditorState;
readonly dispatch: (tr: Transaction) => void;
readonly schemaRegistry?: SchemaRegistry;
readonly syncSelection?: () => void;
readonly isReadOnly?: () => boolean;
}
MethodDescription
destroy()Removes clipboard event listeners

The ClipboardHandler automatically listens for copy and cut events on the provided element. For cut operations, it dispatches a deleteSelectionCommand after writing to the clipboard.


Tracks IME (Input Method Editor) composition state for languages like Chinese, Japanese, and Korean.

import { CompositionTracker } from '@notectl/core';
const tracker = new CompositionTracker();
PropertyTypeDescription
isComposingbooleanWhether an IME composition is active
activeBlockIdBlockId | nullThe block where composition is happening
MethodSignatureDescription
start(blockId: BlockId) => voidBegin composition tracking
end() => voidEnd composition tracking
interface CompositionState {
readonly isComposing: boolean;
readonly activeBlockId: BlockId | null;
}

The CompositionTracker implements this interface. During composition, the editor defers DOM reconciliation to avoid interfering with the IME.