diff --git a/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts b/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts index eb7d81a..09e1a8b 100644 --- a/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts +++ b/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts @@ -374,27 +374,25 @@ export class DeesInputWysiwyg extends DeesInputBase { this.blurTimeout = null; } - if (block.type !== 'divider') { - const prevSelectedId = this.selectedBlockId; - this.selectedBlockId = block.id; + const prevSelectedId = this.selectedBlockId; + this.selectedBlockId = block.id; + + // Only update selection UI if it changed + if (prevSelectedId !== block.id) { + // Update the previous block's selection state + if (prevSelectedId) { + const prevWrapper = this.shadowRoot?.querySelector(`[data-block-id="${prevSelectedId}"]`); + const prevBlockComponent = prevWrapper?.querySelector('dees-wysiwyg-block') as any; + if (prevBlockComponent) { + prevBlockComponent.isSelected = false; + } + } - // Only update selection UI if it changed - if (prevSelectedId !== block.id) { - // Update the previous block's selection state - if (prevSelectedId) { - const prevWrapper = this.shadowRoot?.querySelector(`[data-block-id="${prevSelectedId}"]`); - const prevBlockComponent = prevWrapper?.querySelector('dees-wysiwyg-block') as any; - if (prevBlockComponent) { - prevBlockComponent.isSelected = false; - } - } - - // Update the new block's selection state - const wrapper = this.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); - const blockComponent = wrapper?.querySelector('dees-wysiwyg-block') as any; - if (blockComponent) { - blockComponent.isSelected = true; - } + // Update the new block's selection state + const wrapper = this.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); + const blockComponent = wrapper?.querySelector('dees-wysiwyg-block') as any; + if (blockComponent) { + blockComponent.isSelected = true; } } } @@ -451,9 +449,7 @@ export class DeesInputWysiwyg extends DeesInputBase { // Focus last block if clicking on empty editor area if (target.classList.contains('editor-content')) { const lastBlock = this.blocks[this.blocks.length - 1]; - if (lastBlock.type !== 'divider') { - this.blockOperations.focusBlock(lastBlock.id, 'end'); - } + this.blockOperations.focusBlock(lastBlock.id, lastBlock.type === 'divider' || lastBlock.type === 'image' ? undefined : 'end'); } } diff --git a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts index 2a9751d..c395143 100644 --- a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts +++ b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts @@ -142,15 +142,28 @@ export class DeesWysiwygBlock extends DeesElement { } .block.divider { - padding: 0; + padding: 8px 0; margin: 16px 0; - pointer-events: none; + cursor: pointer; + position: relative; + border-radius: 4px; + transition: all 0.15s ease; + } + + .block.divider:focus { + outline: none; + } + + .block.divider.selected { + background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.05)', 'rgba(77, 148, 255, 0.08)')}; + box-shadow: inset 0 0 0 2px ${cssManager.bdTheme('rgba(0, 102, 204, 0.2)', 'rgba(77, 148, 255, 0.2)')}; } .block.divider hr { border: none; border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; margin: 0; + pointer-events: none; } /* Formatting styles */ @@ -271,6 +284,16 @@ export class DeesWysiwygBlock extends DeesElement { display: flex; align-items: center; justify-content: center; + cursor: pointer; + transition: all 0.15s ease; + } + + .block.image:focus { + outline: none; + } + + .block.image.selected { + box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')}; } .image-upload-placeholder { @@ -349,6 +372,20 @@ export class DeesWysiwygBlock extends DeesElement { ]; protected shouldUpdate(changedProperties: Map): boolean { + // If selection state changed, we need to update for non-editable blocks + if (changedProperties.has('isSelected') && (this.block?.type === 'divider' || this.block?.type === 'image')) { + // For non-editable blocks, we need to update the selected class + const element = this.shadowRoot?.querySelector('.block') as HTMLElement; + if (element) { + if (this.isSelected) { + element.classList.add('selected'); + } else { + element.classList.remove('selected'); + } + } + return false; // Don't re-render, just update the class + } + // Never update if only the block content changed if (changedProperties.has('block') && this.block) { const oldBlock = changedProperties.get('block'); @@ -372,10 +409,13 @@ export class DeesWysiwygBlock extends DeesElement { container.innerHTML = this.renderBlockContent(); } - // Handle image block setup + // Handle special block types if (this.block.type === 'image') { this.setupImageBlock(); return; // Image blocks don't need the standard editable setup + } else if (this.block.type === 'divider') { + this.setupDividerBlock(); + return; // Divider blocks don't need the standard editable setup } // Now find the actual editable block element @@ -575,8 +615,9 @@ export class DeesWysiwygBlock extends DeesElement { if (!this.block) return ''; if (this.block.type === 'divider') { + const selectedClass = this.isSelected ? ' selected' : ''; return ` -
+

`; @@ -603,7 +644,7 @@ export class DeesWysiwygBlock extends DeesElement { const isLoading = this.block.metadata?.loading || false; return ` -
+
${isLoading ? `
Uploading image...
` : ''} @@ -655,13 +696,19 @@ export class DeesWysiwygBlock extends DeesElement { public focus(): void { - // Image blocks don't focus in the traditional way + // Handle non-editable blocks if (this.block?.type === 'image') { const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement; if (imageBlock) { imageBlock.focus(); } return; + } else if (this.block?.type === 'divider') { + const dividerBlock = this.shadowRoot?.querySelector('.block.divider') as HTMLDivElement; + if (dividerBlock) { + dividerBlock.focus(); + } + return; } // Get the actual editable element (might be nested for code blocks) @@ -687,8 +734,8 @@ export class DeesWysiwygBlock extends DeesElement { } public focusWithCursor(position: 'start' | 'end' | number = 'end'): void { - // Image blocks don't support cursor positioning - if (this.block?.type === 'image') { + // Non-editable blocks don't support cursor positioning + if (this.block?.type === 'image' || this.block?.type === 'divider') { this.focus(); return; } @@ -866,6 +913,42 @@ export class DeesWysiwygBlock extends DeesElement { } } + /** + * Setup divider block functionality + */ + private setupDividerBlock(): void { + const dividerBlock = this.shadowRoot?.querySelector('.block.divider') as HTMLDivElement; + if (!dividerBlock) return; + + // Handle click to select + dividerBlock.addEventListener('click', (e) => { + e.stopPropagation(); + // Focus will trigger the selection + dividerBlock.focus(); + }); + + // Handle focus/blur + dividerBlock.addEventListener('focus', () => { + this.handlers?.onFocus?.(); + }); + + dividerBlock.addEventListener('blur', () => { + this.handlers?.onBlur?.(); + }); + + // Handle keyboard events + dividerBlock.addEventListener('keydown', (e) => { + if (e.key === 'Backspace' || e.key === 'Delete') { + e.preventDefault(); + // Let the keyboard handler in the parent component handle the deletion + this.handlers?.onKeyDown?.(e); + } else { + // Handle navigation keys + this.handlers?.onKeyDown?.(e); + } + }); + } + /** * Setup image block functionality */ @@ -873,8 +956,17 @@ export class DeesWysiwygBlock extends DeesElement { const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement; if (!imageBlock) return; - // Make the image block focusable - imageBlock.setAttribute('tabindex', '0'); + // Note: tabindex is already set in the HTML + + // Handle click to select the block + imageBlock.addEventListener('click', (e) => { + // Don't stop propagation for file input clicks + if ((e.target as HTMLElement).tagName !== 'INPUT') { + e.stopPropagation(); + // Focus will trigger the selection + imageBlock.focus(); + } + }); // Handle click on upload placeholder const uploadPlaceholder = imageBlock.querySelector('.image-upload-placeholder'); @@ -931,7 +1023,14 @@ export class DeesWysiwygBlock extends DeesElement { // Handle keyboard events imageBlock.addEventListener('keydown', (e) => { - this.handlers?.onKeyDown?.(e); + if (e.key === 'Backspace' || e.key === 'Delete') { + e.preventDefault(); + // Let the keyboard handler in the parent component handle the deletion + this.handlers?.onKeyDown?.(e); + } else { + // Handle navigation keys + this.handlers?.onKeyDown?.(e); + } }); } diff --git a/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts b/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts index a83af96..633a697 100644 --- a/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts +++ b/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts @@ -35,6 +35,9 @@ export class WysiwygKeyboardHandler { case 'Backspace': await this.handleBackspace(e, block); break; + case 'Delete': + await this.handleDelete(e, block); + break; case 'ArrowUp': await this.handleArrowUp(e, block); break; @@ -116,6 +119,14 @@ export class WysiwygKeyboardHandler { private async handleEnter(e: KeyboardEvent, block: IBlock): Promise { const blockOps = this.component.blockOperations; + // For non-editable blocks, create a new paragraph after + if (block.type === 'divider' || block.type === 'image') { + e.preventDefault(); + const newBlock = blockOps.createBlock(); + await blockOps.insertBlockAfter(block, newBlock); + return; + } + if (block.type === 'code') { if (e.shiftKey) { // Shift+Enter in code blocks creates a new block @@ -222,6 +233,41 @@ export class WysiwygKeyboardHandler { private async handleBackspace(e: KeyboardEvent, block: IBlock): Promise { const blockOps = this.component.blockOperations; + // Handle non-editable blocks (divider, image) + if (block.type === 'divider' || block.type === 'image') { + e.preventDefault(); + + // Don't delete if it's the only block + if (this.component.blocks.length === 1) { + return; + } + + // Save state for undo + this.component.saveToHistory(false); + + // Find the previous block to focus + const prevBlock = blockOps.getPreviousBlock(block.id); + const nextBlock = blockOps.getNextBlock(block.id); + + // Remove the block + blockOps.removeBlock(block.id); + + // Focus the appropriate block + if (prevBlock && prevBlock.type !== 'divider' && prevBlock.type !== 'image') { + await blockOps.focusBlock(prevBlock.id, 'end'); + } else if (nextBlock && nextBlock.type !== 'divider' && nextBlock.type !== 'image') { + await blockOps.focusBlock(nextBlock.id, 'start'); + } else if (prevBlock) { + // If previous block is also non-editable, just select it + await blockOps.focusBlock(prevBlock.id); + } else if (nextBlock) { + // If next block is also non-editable, just select it + await blockOps.focusBlock(nextBlock.id); + } + + return; + } + // Get the block component to check cursor position const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as any; @@ -321,10 +367,66 @@ export class WysiwygKeyboardHandler { // Otherwise, let browser handle normal backspace } + /** + * Handles Delete key + */ + private async handleDelete(e: KeyboardEvent, block: IBlock): Promise { + const blockOps = this.component.blockOperations; + + // Handle non-editable blocks (divider, image) - same as backspace + if (block.type === 'divider' || block.type === 'image') { + e.preventDefault(); + + // Don't delete if it's the only block + if (this.component.blocks.length === 1) { + return; + } + + // Save state for undo + this.component.saveToHistory(false); + + // Find the previous block to focus + const prevBlock = blockOps.getPreviousBlock(block.id); + const nextBlock = blockOps.getNextBlock(block.id); + + // Remove the block + blockOps.removeBlock(block.id); + + // Focus the appropriate block + if (nextBlock && nextBlock.type !== 'divider' && nextBlock.type !== 'image') { + await blockOps.focusBlock(nextBlock.id, 'start'); + } else if (prevBlock && prevBlock.type !== 'divider' && prevBlock.type !== 'image') { + await blockOps.focusBlock(prevBlock.id, 'end'); + } else if (nextBlock) { + // If next block is also non-editable, just select it + await blockOps.focusBlock(nextBlock.id); + } else if (prevBlock) { + // If previous block is also non-editable, just select it + await blockOps.focusBlock(prevBlock.id); + } + + return; + } + + // For editable blocks, let browser handle normal delete + } + /** * Handles ArrowUp key - navigate to previous block if at beginning or first line */ private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise { + // For non-editable blocks, always navigate to previous block + if (block.type === 'divider' || block.type === 'image') { + e.preventDefault(); + const blockOps = this.component.blockOperations; + const prevBlock = blockOps.getPreviousBlock(block.id); + + if (prevBlock) { + await blockOps.focusBlock(prevBlock.id, prevBlock.type === 'divider' || prevBlock.type === 'image' ? undefined : 'end'); + } + return; + } + // Get the block component from the wysiwyg component's shadow DOM const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); @@ -352,9 +454,9 @@ export class WysiwygKeyboardHandler { const blockOps = this.component.blockOperations; const prevBlock = blockOps.getPreviousBlock(block.id); - if (prevBlock && prevBlock.type !== 'divider') { + if (prevBlock) { console.log('ArrowUp: Focusing previous block:', prevBlock.id); - await blockOps.focusBlock(prevBlock.id, 'end'); + await blockOps.focusBlock(prevBlock.id, prevBlock.type === 'divider' || prevBlock.type === 'image' ? undefined : 'end'); } } // Otherwise, let browser handle normal navigation @@ -364,6 +466,18 @@ export class WysiwygKeyboardHandler { * Handles ArrowDown key - navigate to next block if at end or last line */ private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise { + // For non-editable blocks, always navigate to next block + if (block.type === 'divider' || block.type === 'image') { + e.preventDefault(); + const blockOps = this.component.blockOperations; + const nextBlock = blockOps.getNextBlock(block.id); + + if (nextBlock) { + await blockOps.focusBlock(nextBlock.id, nextBlock.type === 'divider' || nextBlock.type === 'image' ? undefined : 'start'); + } + return; + } + // Get the block component from the wysiwyg component's shadow DOM const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); @@ -391,9 +505,9 @@ export class WysiwygKeyboardHandler { const blockOps = this.component.blockOperations; const nextBlock = blockOps.getNextBlock(block.id); - if (nextBlock && nextBlock.type !== 'divider') { + if (nextBlock) { console.log('ArrowDown: Focusing next block:', nextBlock.id); - await blockOps.focusBlock(nextBlock.id, 'start'); + await blockOps.focusBlock(nextBlock.id, nextBlock.type === 'divider' || nextBlock.type === 'image' ? undefined : 'start'); } } // Otherwise, let browser handle normal navigation @@ -419,6 +533,18 @@ export class WysiwygKeyboardHandler { * Handles ArrowLeft key - navigate to previous block if at beginning */ private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise { + // For non-editable blocks, navigate to previous block + if (block.type === 'divider' || block.type === 'image') { + e.preventDefault(); + const blockOps = this.component.blockOperations; + const prevBlock = blockOps.getPreviousBlock(block.id); + + if (prevBlock) { + await blockOps.focusBlock(prevBlock.id, prevBlock.type === 'divider' || prevBlock.type === 'image' ? undefined : 'end'); + } + return; + } + // Get the block component from the wysiwyg component's shadow DOM const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); @@ -448,9 +574,9 @@ export class WysiwygKeyboardHandler { const prevBlock = blockOps.getPreviousBlock(block.id); console.log('ArrowLeft: At start, previous block:', prevBlock?.id); - if (prevBlock && prevBlock.type !== 'divider') { + if (prevBlock) { e.preventDefault(); - await blockOps.focusBlock(prevBlock.id, 'end'); + await blockOps.focusBlock(prevBlock.id, prevBlock.type === 'divider' || prevBlock.type === 'image' ? undefined : 'end'); } } // Otherwise, let the browser handle normal left arrow navigation @@ -460,6 +586,18 @@ export class WysiwygKeyboardHandler { * Handles ArrowRight key - navigate to next block if at end */ private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise { + // For non-editable blocks, navigate to next block + if (block.type === 'divider' || block.type === 'image') { + e.preventDefault(); + const blockOps = this.component.blockOperations; + const nextBlock = blockOps.getNextBlock(block.id); + + if (nextBlock) { + await blockOps.focusBlock(nextBlock.id, nextBlock.type === 'divider' || nextBlock.type === 'image' ? undefined : 'start'); + } + return; + } + // Get the block component from the wysiwyg component's shadow DOM const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); @@ -488,9 +626,9 @@ export class WysiwygKeyboardHandler { const blockOps = this.component.blockOperations; const nextBlock = blockOps.getNextBlock(block.id); - if (nextBlock && nextBlock.type !== 'divider') { + if (nextBlock) { e.preventDefault(); - await blockOps.focusBlock(nextBlock.id, 'start'); + await blockOps.focusBlock(nextBlock.id, nextBlock.type === 'divider' || nextBlock.type === 'image' ? undefined : 'start'); } } // Otherwise, let the browser handle normal right arrow navigation