feat(editor): eslint/tsconfig migration + type fixes
@robonen/editor: migrate to eslint flat config + composite tsconfig; fix convergence test type annotations.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import type { Command } from '../state';
|
||||
import type { Platform } from '../view/config';
|
||||
import type { Keymap } from './types';
|
||||
import { normalizeCombo } from './normalize';
|
||||
|
||||
/**
|
||||
* Merge ordered keymaps into a single normalized lookup. Earlier keymaps win, so
|
||||
* pass user overrides before the defaults: `compileKeymaps([user, defaults], …)`.
|
||||
*/
|
||||
export function compileKeymaps(keymaps: readonly Keymap[], platform: Platform): Map<string, Command> {
|
||||
const compiled = new Map<string, Command>();
|
||||
|
||||
for (const keymap of keymaps) {
|
||||
for (const combo in keymap) {
|
||||
const normalized = normalizeCombo(combo, platform);
|
||||
if (!compiled.has(normalized))
|
||||
compiled.set(normalized, keymap[combo]!);
|
||||
}
|
||||
}
|
||||
|
||||
return compiled;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
chainCommands,
|
||||
deleteSelection,
|
||||
indentListItem,
|
||||
insertHardBreak,
|
||||
joinBackward,
|
||||
joinForward,
|
||||
moveBlockDown,
|
||||
moveBlockUp,
|
||||
outdentListItem,
|
||||
selectAll,
|
||||
setBlockType,
|
||||
splitBlock,
|
||||
toggleBlockType,
|
||||
toggleMark,
|
||||
} from '../commands';
|
||||
import type { Command, Editor } from '../state';
|
||||
import type { Keymap } from './types';
|
||||
|
||||
/**
|
||||
* The standard editor keymap. Mark/heading shortcuts are no-ops when the mark or
|
||||
* block type isn't registered. Enter/Backspace/Delete are no-ops except at block
|
||||
* boundaries, so ordinary intra-block editing stays native. Arrow navigation and
|
||||
* cross-block selection are fully native (one contenteditable spans the doc).
|
||||
*/
|
||||
export function defaultKeymap(editor: Editor): Keymap {
|
||||
const undo: Command = () => editor.undo();
|
||||
const redo: Command = () => editor.redo();
|
||||
|
||||
const keymap: Keymap = {
|
||||
'Mod-b': toggleMark('bold'),
|
||||
'Mod-i': toggleMark('italic'),
|
||||
'Mod-u': toggleMark('underline'),
|
||||
'Mod-Shift-s': toggleMark('strike'),
|
||||
'Mod-e': toggleMark('code'),
|
||||
'Mod-z': undo,
|
||||
'Mod-Shift-z': redo,
|
||||
'Mod-y': redo,
|
||||
Enter: splitBlock,
|
||||
'Shift-Enter': insertHardBreak,
|
||||
Backspace: chainCommands(deleteSelection, joinBackward),
|
||||
Delete: chainCommands(deleteSelection, joinForward),
|
||||
'Mod-a': selectAll,
|
||||
Tab: indentListItem,
|
||||
'Shift-Tab': outdentListItem,
|
||||
'Mod-Shift-ArrowUp': moveBlockUp,
|
||||
'Mod-Shift-ArrowDown': moveBlockDown,
|
||||
'Mod-Alt-0': setBlockType('paragraph'),
|
||||
};
|
||||
|
||||
for (let level = 1; level <= 6; level++)
|
||||
keymap[`Mod-Alt-${level}`] = toggleBlockType('heading', { level });
|
||||
|
||||
return keymap;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Command, CommandView, Dispatch, EditorState } from '../state';
|
||||
import { eventToCombo } from './normalize';
|
||||
|
||||
/**
|
||||
* Look up and run the command bound to a keydown event. Returns `true` when a
|
||||
* command handled it (the caller should then `preventDefault`).
|
||||
*/
|
||||
export function runKeydown(
|
||||
event: KeyboardEvent,
|
||||
compiled: Map<string, Command>,
|
||||
state: EditorState,
|
||||
dispatch: Dispatch,
|
||||
view: CommandView,
|
||||
): boolean {
|
||||
const command = compiled.get(eventToCombo(event));
|
||||
return command ? command(state, dispatch, view) : false;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './types';
|
||||
export * from './normalize';
|
||||
export * from './compile';
|
||||
export * from './defaults';
|
||||
export * from './dispatcher';
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Platform } from '../view/config';
|
||||
|
||||
const MOD_ORDER = ['Ctrl', 'Alt', 'Shift', 'Meta'] as const;
|
||||
|
||||
function modAlias(token: string, platform: Platform): string | null {
|
||||
switch (token.toLowerCase()) {
|
||||
case 'mod': return platform === 'mac' ? 'Meta' : 'Ctrl';
|
||||
case 'cmd':
|
||||
case 'command':
|
||||
case 'meta': return 'Meta';
|
||||
case 'ctrl':
|
||||
case 'control': return 'Ctrl';
|
||||
case 'alt':
|
||||
case 'option': return 'Alt';
|
||||
case 'shift': return 'Shift';
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a human combo (`'Mod-Shift-z'`) to a canonical, platform-resolved
|
||||
* form (`'Shift-Meta-z'` on mac). Modifiers are ordered deterministically so a
|
||||
* keydown event maps to the same string via {@link eventToCombo}.
|
||||
*/
|
||||
export function normalizeCombo(combo: string, platform: Platform): string {
|
||||
const parts = combo.split(/[-+]/).map(part => part.trim()).filter(Boolean);
|
||||
const key = parts.pop() ?? '';
|
||||
const mods = new Set<string>();
|
||||
|
||||
for (const part of parts) {
|
||||
const mod = modAlias(part, platform);
|
||||
if (mod)
|
||||
mods.add(mod);
|
||||
}
|
||||
|
||||
const order = MOD_ORDER.filter(mod => mods.has(mod));
|
||||
const normalizedKey = key.length === 1 ? key.toLowerCase() : key;
|
||||
return [...order, normalizedKey].join('-');
|
||||
}
|
||||
|
||||
/** Canonical combo string for a keydown event (matches {@link normalizeCombo}). */
|
||||
export function eventToCombo(event: KeyboardEvent): string {
|
||||
const mods: string[] = [];
|
||||
|
||||
if (event.ctrlKey) mods.push('Ctrl');
|
||||
if (event.altKey) mods.push('Alt');
|
||||
if (event.shiftKey) mods.push('Shift');
|
||||
if (event.metaKey) mods.push('Meta');
|
||||
|
||||
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
|
||||
return [...mods, key].join('-');
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { Command } from '../state';
|
||||
|
||||
/** A keymap: normalized (or human) key-combos mapped to commands. */
|
||||
export type Keymap = Record<string, Command>;
|
||||
Reference in New Issue
Block a user