/**
* 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 (,