/** * Contains keyboard and mouse events bound on each Block by Block Manager */ import { Module } from '../../__module'; import { Dom as $ } from '../../dom'; import { keyCodes } from '../../utils'; import { isInlineEmojiEnabled } from '../../utils/emoji/inline-emoji-config'; import { YjsManager } from '../yjs'; import { BlockSelectionKeys } from './composers/blockSelectionKeys'; import { EmojiTrigger } from './composers/emojiTrigger'; import { KeyboardNavigation } from './composers/keyboardNavigation'; import { MarkdownShortcuts } from './composers/markdownShortcuts'; import { NavigationMode } from './composers/navigationMode'; import { isPrintableKeyEvent, isInsideKeyboardOwner, keyCodeFromEvent } from './utils/keyboard'; import { isTextLikeBlock } from './utils/text-like-block'; /** * All keydowns on Block * @param {KeyboardEvent} event - keydown */ export class BlockEvents extends Module { /** * Lazy-loaded composer instances */ private _navigationMode?: NavigationMode; private _markdownShortcuts?: MarkdownShortcuts; private _blockSelectionKeys?: BlockSelectionKeys; private _keyboardNavigation?: KeyboardNavigation; private _emojiTrigger?: EmojiTrigger; /** * Get the NavigationMode composer instance */ private get navigationMode(): NavigationMode { if (!this._navigationMode) { this._navigationMode = new NavigationMode(this.Blok); } return this._navigationMode; } /** * Get the MarkdownShortcuts composer instance */ private get markdownShortcuts(): MarkdownShortcuts { if (!this._markdownShortcuts) { this._markdownShortcuts = new MarkdownShortcuts(this.Blok); } return this._markdownShortcuts; } /** * Get the EmojiTrigger composer instance. Public: KeyboardController's * capture-phase Escape handler reads `emojiTrigger.opened` and calls * `close()` directly (see keyboard.ts) — the same pattern it already uses * for `Toolbar.toolbox`. */ public get emojiTrigger(): EmojiTrigger { if (!this._emojiTrigger) { this._emojiTrigger = new EmojiTrigger(this.Blok); } return this._emojiTrigger; } /** * Get the BlockSelectionKeys composer instance */ private get blockSelectionKeys(): BlockSelectionKeys { if (!this._blockSelectionKeys) { this._blockSelectionKeys = new BlockSelectionKeys(this.Blok); } return this._blockSelectionKeys; } /** * Get the KeyboardNavigation composer instance */ private get keyboardNavigation(): KeyboardNavigation { if (!this._keyboardNavigation) { this._keyboardNavigation = new KeyboardNavigation( this.Blok, () => this.config.onEnter, () => this.config.onSubmit ); } return this._keyboardNavigation; } /** * All keydowns on Block * @param {KeyboardEvent} event - keydown */ public keydown(event: KeyboardEvent): void { /** * A Tool can claim the whole keyboard for a subtree it renders (a field with * its own Tab/Escape/arrow semantics) by marking it * `data-blok-keyboard-owner`. Blok's block-level pipeline stands down * entirely there — see the attribute's note in constants/data-attributes. * Checked before ANY handler so navigation mode, the structural keys, "/", * the block shortcuts and the caret navigation are all skipped in one place. */ if (isInsideKeyboardOwner(event.target)) { return; } /** * The emoji menu owns Escape/ArrowUp/ArrowDown/Home/End/Enter/Tab while it * is open. This must run before navigationMode.handleEscape below — that * handler has no knowledge of the emoji menu, so an unclaimed Escape would * enter block navigation mode instead of closing the menu. */ if (this.emojiTrigger.opened && this.emojiTrigger.handleKeydown(event)) { return; } /** * Handle navigation mode keys first */ if (this.navigationMode.handleKey(event)) { return; } /** * Handle Escape key to enable navigation mode */ if (this.navigationMode.handleEscape(event)) { return; } /** * A cross-block TEXT selection spans two editing hosts, which no engine will * edit natively — the keystroke has to replace the range itself. Checked * before beforeKeydownProcessing so the printable-key path here wins over the * block-selection one, which reads a different selection unit entirely. */ if (this.blockSelectionKeys.handleTextSelectionEditKey(event)) { return; } /** * Run common method for all keydown events */ this.beforeKeydownProcessing(event); if (this.blockSelectionKeys.handleDeletion(event)) { return; } /** * If event was already handled by something (e.g. tool), we should not handle it */ if (event.defaultPrevented) { return; } const keyCode = keyCodeFromEvent(event); /** * Eagerly resolve currentBlock from the event target for keys that mutate * block structure (Enter split, Backspace/Delete merge). The debounced * selectionchange handler (180ms) may not have fired yet when the caret was * set programmatically (e.g. inside a column), leaving currentBlock * undefined — the handlers would then bail before preventDefault and the * browser would perform a destructive native edit. Mirrors slashPressed. */ if ( (keyCode === keyCodes.ENTER || keyCode === keyCodes.BACKSPACE || keyCode === keyCodes.DELETE) && event.target instanceof Node ) { this.Blok.BlockManager.setCurrentBlockByChildNode(event.target); } /** * A native form control a tool renders (,