Improve Wysiwyg editor
This commit is contained in:
193
ts_web/elements/wysiwyg/wysiwyg.inputhandler.ts
Normal file
193
ts_web/elements/wysiwyg/wysiwyg.inputhandler.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user