- 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
170 lines
4.9 KiB
TypeScript
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;
|
|
}
|
|
} |