Files
dees-catalog/ts_web/elements/wysiwyg/wysiwyg.blockoperations.ts
Juergen Kunz 1c76ade150 fix(wysiwyg): Implement programmatic rendering to eliminate focus loss during typing
- Convert parent component to use static rendering with programmatic DOM manipulation
- Remove all reactive state that could trigger re-renders during editing
- Delay content sync to avoid interference with typing (2s auto-save)
- Update all block operations to use manual DOM updates instead of Lit re-renders
- Fix specific issue where typing + arrow keys caused focus loss
- Add comprehensive focus management documentation
2025-06-24 11:06:02 +00:00

170 lines
4.9 KiB
TypeScript

import { type IBlock } from './wysiwyg.types.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
import { WysiwygBlocks } from './wysiwyg.blocks.js';
export class WysiwygBlockOperations {
private component: any; // Will be typed properly when imported
constructor(component: any) {
this.component = component;
}
/**
* Creates a new block with the specified parameters
*/
createBlock(type: IBlock['type'] = 'paragraph', content: string = '', metadata?: any): IBlock {
return {
id: WysiwygShortcuts.generateBlockId(),
type,
content,
...(metadata && { metadata })
};
}
/**
* Inserts a block after the specified block
*/
async insertBlockAfter(afterBlock: IBlock, newBlock: IBlock, focusNewBlock: boolean = true): Promise<void> {
const blocks = this.component.blocks;
const blockIndex = blocks.findIndex((b: IBlock) => b.id === afterBlock.id);
this.component.blocks = [
...blocks.slice(0, blockIndex + 1),
newBlock,
...blocks.slice(blockIndex + 1)
];
// Insert the new block element programmatically if we have the editor
if (this.component.editorContentRef) {
const afterWrapper = this.component.editorContentRef.querySelector(`[data-block-id="${afterBlock.id}"]`);
if (afterWrapper) {
const newWrapper = this.component.createBlockElement(newBlock);
afterWrapper.insertAdjacentElement('afterend', newWrapper);
}
}
this.component.updateValue();
if (focusNewBlock && newBlock.type !== 'divider') {
// Give DOM time to settle
await new Promise(resolve => setTimeout(resolve, 0));
// Focus the new block
await this.focusBlock(newBlock.id, 'start');
}
}
/**
* Removes a block by its ID
*/
removeBlock(blockId: string): void {
this.component.blocks = this.component.blocks.filter((b: IBlock) => b.id !== blockId);
// Remove the block element programmatically if we have the editor
if (this.component.editorContentRef) {
const wrapper = this.component.editorContentRef.querySelector(`[data-block-id="${blockId}"]`);
if (wrapper) {
wrapper.remove();
}
}
this.component.updateValue();
}
/**
* Finds a block by its ID
*/
findBlock(blockId: string): IBlock | undefined {
return this.component.blocks.find((b: IBlock) => b.id === blockId);
}
/**
* Gets the index of a block
*/
getBlockIndex(blockId: string): number {
return this.component.blocks.findIndex((b: IBlock) => b.id === blockId);
}
/**
* Focuses a specific block
*/
async focusBlock(blockId: string, cursorPosition: 'start' | 'end' | number = 'start'): Promise<void> {
const wrapperElement = this.component.shadowRoot!.querySelector(`[data-block-id="${blockId}"]`);
if (wrapperElement) {
const blockComponent = wrapperElement.querySelector('dees-wysiwyg-block') as any;
if (blockComponent) {
// Wait a frame to ensure the block is rendered
await new Promise(resolve => requestAnimationFrame(resolve));
// Now focus with cursor position
blockComponent.focusWithCursor(cursorPosition);
}
}
}
/**
* Updates the content of a block
*/
updateBlockContent(blockId: string, content: string): void {
const block = this.findBlock(blockId);
if (block) {
block.content = content;
this.component.updateValue();
}
}
/**
* Transforms a block to a different type
*/
transformBlock(blockId: string, newType: IBlock['type'], metadata?: any): void {
const block = this.findBlock(blockId);
if (block) {
block.type = newType;
block.content = '';
if (metadata) {
block.metadata = metadata;
}
// Update the block element programmatically if we have the editor
if (this.component.editorContentRef) {
this.component.updateBlockElement(blockId);
}
this.component.updateValue();
}
}
/**
* Moves a block to a new position
*/
moveBlock(blockId: string, targetIndex: number): void {
const blocks = [...this.component.blocks];
const currentIndex = this.getBlockIndex(blockId);
if (currentIndex === -1 || targetIndex < 0 || targetIndex >= blocks.length) {
return;
}
const [movedBlock] = blocks.splice(currentIndex, 1);
blocks.splice(targetIndex, 0, movedBlock);
this.component.blocks = blocks;
this.component.updateValue();
}
/**
* Gets the previous block
*/
getPreviousBlock(blockId: string): IBlock | null {
const index = this.getBlockIndex(blockId);
return index > 0 ? this.component.blocks[index - 1] : null;
}
/**
* Gets the next block
*/
getNextBlock(blockId: string): IBlock | null {
const index = this.getBlockIndex(blockId);
return index < this.component.blocks.length - 1 ? this.component.blocks[index + 1] : null;
}
}