Files
dees-catalog/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts
2025-06-24 08:19:53 +00:00

198 lines
5.3 KiB
TypeScript

import { type IBlock } from './wysiwyg.types.js';
import { WysiwygBlocks } from './wysiwyg.blocks.js';
import { WysiwygBlockOperations } from './wysiwyg.blockoperations.js';
export class WysiwygKeyboardHandler {
private component: any;
constructor(component: any) {
this.component = component;
}
/**
* Handles keyboard events for blocks
*/
handleBlockKeyDown(e: KeyboardEvent, block: IBlock): void {
// Handle slash menu navigation
if (this.component.showSlashMenu && this.isSlashMenuKey(e.key)) {
this.handleSlashMenuKeyboard(e);
return;
}
// Handle formatting shortcuts
if (this.handleFormattingShortcuts(e)) {
return;
}
// Handle special keys
switch (e.key) {
case 'Tab':
this.handleTab(e, block);
break;
case 'Enter':
this.handleEnter(e, block);
break;
case 'Backspace':
this.handleBackspace(e, block);
break;
}
}
/**
* Checks if key is for slash menu navigation
*/
private isSlashMenuKey(key: string): boolean {
return ['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(key);
}
/**
* Handles formatting keyboard shortcuts
*/
private handleFormattingShortcuts(e: KeyboardEvent): boolean {
if (!(e.metaKey || e.ctrlKey)) return false;
switch (e.key.toLowerCase()) {
case 'b':
e.preventDefault();
this.component.applyFormat('bold');
return true;
case 'i':
e.preventDefault();
this.component.applyFormat('italic');
return true;
case 'u':
e.preventDefault();
this.component.applyFormat('underline');
return true;
case 'k':
e.preventDefault();
this.component.applyFormat('link');
return true;
}
return false;
}
/**
* Handles Tab key
*/
private handleTab(e: KeyboardEvent, block: IBlock): void {
if (block.type === 'code') {
// Allow tab in code blocks
e.preventDefault();
document.execCommand('insertText', false, ' ');
} else if (block.type === 'list') {
// Future: implement list indentation
e.preventDefault();
}
}
/**
* Handles Enter key
*/
private handleEnter(e: KeyboardEvent, block: IBlock): void {
const blockOps = this.component.blockOperations;
if (block.type === 'code') {
if (e.shiftKey) {
// Shift+Enter in code blocks creates a new block
e.preventDefault();
const newBlock = blockOps.createBlock();
blockOps.insertBlockAfter(block, newBlock);
}
// Normal Enter in code blocks creates new line (let browser handle it)
return;
}
if (!e.shiftKey) {
if (block.type === 'list') {
this.handleEnterInList(e, block);
} else {
// Create new paragraph block
e.preventDefault();
const newBlock = blockOps.createBlock();
blockOps.insertBlockAfter(block, newBlock);
}
}
// Shift+Enter creates line break (let browser handle it)
}
/**
* Handles Enter key in list blocks
*/
private handleEnterInList(e: KeyboardEvent, block: IBlock): void {
const target = e.target as HTMLDivElement;
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const currentLi = range.startContainer.parentElement?.closest('li');
if (currentLi && currentLi.textContent === '') {
// Empty list item - exit list mode
e.preventDefault();
const blockOps = this.component.blockOperations;
const newBlock = blockOps.createBlock();
blockOps.insertBlockAfter(block, newBlock);
}
// Otherwise, let browser create new list item
}
}
/**
* Handles Backspace key
*/
private handleBackspace(e: KeyboardEvent, block: IBlock): void {
if (block.content === '' && this.component.blocks.length > 1) {
e.preventDefault();
const blockOps = this.component.blockOperations;
const prevBlock = blockOps.getPreviousBlock(block.id);
if (prevBlock) {
blockOps.removeBlock(block.id);
setTimeout(() => {
if (prevBlock.type !== 'divider') {
blockOps.focusBlock(prevBlock.id, 'end');
}
});
}
}
}
/**
* Handles slash menu keyboard navigation
*/
private handleSlashMenuKeyboard(e: KeyboardEvent): void {
const menuItems = this.component.getFilteredMenuItems();
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
this.component.slashMenuSelectedIndex =
(this.component.slashMenuSelectedIndex + 1) % menuItems.length;
break;
case 'ArrowUp':
e.preventDefault();
this.component.slashMenuSelectedIndex =
this.component.slashMenuSelectedIndex === 0
? menuItems.length - 1
: this.component.slashMenuSelectedIndex - 1;
break;
case 'Enter':
e.preventDefault();
if (menuItems[this.component.slashMenuSelectedIndex]) {
this.component.insertBlock(
menuItems[this.component.slashMenuSelectedIndex].type as IBlock['type']
);
}
break;
case 'Escape':
e.preventDefault();
this.component.closeSlashMenu();
break;
}
}
}