193 lines
5.6 KiB
TypeScript
193 lines
5.6 KiB
TypeScript
import { type IBlock } from './wysiwyg.types.js';
|
|
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
|
|
import { WysiwygBlocks } from './wysiwyg.blocks.js';
|
|
import { WysiwygBlockOperations } from './wysiwyg.blockoperations.js';
|
|
|
|
export class WysiwygInputHandler {
|
|
private component: any;
|
|
private saveTimeout: any = null;
|
|
|
|
constructor(component: any) {
|
|
this.component = component;
|
|
}
|
|
|
|
/**
|
|
* Handles input events for blocks
|
|
*/
|
|
handleBlockInput(e: InputEvent, block: IBlock): void {
|
|
if (this.component.isComposing) return;
|
|
|
|
const target = e.target as HTMLDivElement;
|
|
const textContent = target.textContent || '';
|
|
|
|
// Update block content based on type
|
|
this.updateBlockContent(block, target);
|
|
|
|
// Check for block type transformations
|
|
const detectedType = this.detectBlockTypeIntent(textContent);
|
|
if (detectedType && detectedType.type !== block.type) {
|
|
e.preventDefault();
|
|
this.handleBlockTransformation(block, detectedType, target);
|
|
return;
|
|
}
|
|
|
|
// Handle slash commands
|
|
this.handleSlashCommand(textContent, target);
|
|
|
|
// Schedule auto-save
|
|
this.scheduleAutoSave();
|
|
}
|
|
|
|
/**
|
|
* Updates block content based on its type
|
|
*/
|
|
private updateBlockContent(block: IBlock, target: HTMLDivElement): void {
|
|
if (block.type === 'list') {
|
|
const listItems = target.querySelectorAll('li');
|
|
block.content = Array.from(listItems).map(li => li.textContent || '').join('\n');
|
|
|
|
const listElement = target.querySelector('ol, ul');
|
|
if (listElement) {
|
|
block.metadata = {
|
|
listType: listElement.tagName.toLowerCase() === 'ol' ? 'ordered' : 'bullet'
|
|
};
|
|
}
|
|
} else if (block.type === 'code') {
|
|
block.content = target.textContent || '';
|
|
} else {
|
|
block.content = target.innerHTML || '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detects if the user is trying to create a specific block type
|
|
*/
|
|
private detectBlockTypeIntent(content: string): { type: IBlock['type'], listType?: 'bullet' | 'ordered' } | null {
|
|
// Check heading patterns
|
|
const headingResult = WysiwygShortcuts.checkHeadingShortcut(content);
|
|
if (headingResult) {
|
|
return headingResult;
|
|
}
|
|
|
|
// Check list patterns
|
|
const listResult = WysiwygShortcuts.checkListShortcut(content);
|
|
if (listResult) {
|
|
return listResult;
|
|
}
|
|
|
|
// Check quote pattern
|
|
if (WysiwygShortcuts.checkQuoteShortcut(content)) {
|
|
return { type: 'quote' };
|
|
}
|
|
|
|
// Check code pattern
|
|
if (WysiwygShortcuts.checkCodeShortcut(content)) {
|
|
return { type: 'code' };
|
|
}
|
|
|
|
// Check divider pattern
|
|
if (WysiwygShortcuts.checkDividerShortcut(content)) {
|
|
return { type: 'divider' };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Handles block type transformation
|
|
*/
|
|
private async handleBlockTransformation(
|
|
block: IBlock,
|
|
detectedType: { type: IBlock['type'], listType?: 'bullet' | 'ordered' },
|
|
target: HTMLDivElement
|
|
): Promise<void> {
|
|
const blockOps = this.component.blockOperations;
|
|
|
|
if (detectedType.type === 'list') {
|
|
block.type = 'list';
|
|
block.content = '';
|
|
block.metadata = { listType: detectedType.listType };
|
|
|
|
const listTag = detectedType.listType === 'ordered' ? 'ol' : 'ul';
|
|
target.innerHTML = `<${listTag}><li></li></${listTag}>`;
|
|
|
|
this.component.updateValue();
|
|
this.component.requestUpdate();
|
|
|
|
setTimeout(() => {
|
|
WysiwygBlocks.focusListItem(target);
|
|
}, 0);
|
|
} else if (detectedType.type === 'divider') {
|
|
block.type = 'divider';
|
|
block.content = ' ';
|
|
|
|
const newBlock = blockOps.createBlock();
|
|
blockOps.insertBlockAfter(block, newBlock);
|
|
|
|
this.component.updateValue();
|
|
this.component.requestUpdate();
|
|
} else if (detectedType.type === 'code') {
|
|
const language = await this.component.showLanguageSelectionModal();
|
|
if (language) {
|
|
block.type = 'code';
|
|
block.content = '';
|
|
block.metadata = { language };
|
|
target.textContent = '';
|
|
|
|
this.component.updateValue();
|
|
this.component.requestUpdate();
|
|
}
|
|
} else {
|
|
block.type = detectedType.type;
|
|
block.content = '';
|
|
target.textContent = '';
|
|
|
|
this.component.updateValue();
|
|
this.component.requestUpdate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles slash command detection and menu display
|
|
*/
|
|
private handleSlashCommand(textContent: string, target: HTMLDivElement): void {
|
|
if (textContent === '/' || (textContent.startsWith('/') && this.component.showSlashMenu)) {
|
|
if (!this.component.showSlashMenu && textContent === '/') {
|
|
this.component.showSlashMenu = true;
|
|
this.component.slashMenuSelectedIndex = 0;
|
|
|
|
const rect = target.getBoundingClientRect();
|
|
const containerRect = this.component.shadowRoot!.querySelector('.wysiwyg-container')!.getBoundingClientRect();
|
|
|
|
this.component.slashMenuPosition = {
|
|
x: rect.left - containerRect.left,
|
|
y: rect.bottom - containerRect.top + 4
|
|
};
|
|
}
|
|
this.component.slashMenuFilter = textContent.slice(1);
|
|
} else if (!textContent.startsWith('/')) {
|
|
this.component.closeSlashMenu();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedules auto-save after a delay
|
|
*/
|
|
private scheduleAutoSave(): void {
|
|
if (this.saveTimeout) {
|
|
clearTimeout(this.saveTimeout);
|
|
}
|
|
this.saveTimeout = setTimeout(() => {
|
|
this.component.updateValue();
|
|
}, 1000);
|
|
}
|
|
|
|
/**
|
|
* Cleans up resources
|
|
*/
|
|
destroy(): void {
|
|
if (this.saveTimeout) {
|
|
clearTimeout(this.saveTimeout);
|
|
}
|
|
}
|
|
} |