This commit is contained in:
Juergen Kunz
2025-06-24 18:43:51 +00:00
parent fca3638f7f
commit 89a4a15e78
3 changed files with 275 additions and 42 deletions

View File

@ -374,27 +374,25 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
this.blurTimeout = null; this.blurTimeout = null;
} }
if (block.type !== 'divider') { const prevSelectedId = this.selectedBlockId;
const prevSelectedId = this.selectedBlockId; this.selectedBlockId = block.id;
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 // Update the new block's selection state
if (prevSelectedId !== block.id) { const wrapper = this.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`);
// Update the previous block's selection state const blockComponent = wrapper?.querySelector('dees-wysiwyg-block') as any;
if (prevSelectedId) { if (blockComponent) {
const prevWrapper = this.shadowRoot?.querySelector(`[data-block-id="${prevSelectedId}"]`); blockComponent.isSelected = true;
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;
}
} }
} }
} }
@ -451,9 +449,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
// Focus last block if clicking on empty editor area // Focus last block if clicking on empty editor area
if (target.classList.contains('editor-content')) { if (target.classList.contains('editor-content')) {
const lastBlock = this.blocks[this.blocks.length - 1]; const lastBlock = this.blocks[this.blocks.length - 1];
if (lastBlock.type !== 'divider') { this.blockOperations.focusBlock(lastBlock.id, lastBlock.type === 'divider' || lastBlock.type === 'image' ? undefined : 'end');
this.blockOperations.focusBlock(lastBlock.id, 'end');
}
} }
} }

View File

@ -142,15 +142,28 @@ export class DeesWysiwygBlock extends DeesElement {
} }
.block.divider { .block.divider {
padding: 0; padding: 8px 0;
margin: 16px 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 { .block.divider hr {
border: none; border: none;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
margin: 0; margin: 0;
pointer-events: none;
} }
/* Formatting styles */ /* Formatting styles */
@ -271,6 +284,16 @@ export class DeesWysiwygBlock extends DeesElement {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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 { .image-upload-placeholder {
@ -349,6 +372,20 @@ export class DeesWysiwygBlock extends DeesElement {
]; ];
protected shouldUpdate(changedProperties: Map<string, any>): boolean { protected shouldUpdate(changedProperties: Map<string, any>): 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 // Never update if only the block content changed
if (changedProperties.has('block') && this.block) { if (changedProperties.has('block') && this.block) {
const oldBlock = changedProperties.get('block'); const oldBlock = changedProperties.get('block');
@ -372,10 +409,13 @@ export class DeesWysiwygBlock extends DeesElement {
container.innerHTML = this.renderBlockContent(); container.innerHTML = this.renderBlockContent();
} }
// Handle image block setup // Handle special block types
if (this.block.type === 'image') { if (this.block.type === 'image') {
this.setupImageBlock(); this.setupImageBlock();
return; // Image blocks don't need the standard editable setup 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 // Now find the actual editable block element
@ -575,8 +615,9 @@ export class DeesWysiwygBlock extends DeesElement {
if (!this.block) return ''; if (!this.block) return '';
if (this.block.type === 'divider') { if (this.block.type === 'divider') {
const selectedClass = this.isSelected ? ' selected' : '';
return ` return `
<div class="block divider" data-block-id="${this.block.id}" data-block-type="${this.block.type}"> <div class="block divider${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}" tabindex="0">
<hr> <hr>
</div> </div>
`; `;
@ -603,7 +644,7 @@ export class DeesWysiwygBlock extends DeesElement {
const isLoading = this.block.metadata?.loading || false; const isLoading = this.block.metadata?.loading || false;
return ` return `
<div class="block image${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}"> <div class="block image${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}" tabindex="0">
${isLoading ? ` ${isLoading ? `
<div class="image-loading">Uploading image...</div> <div class="image-loading">Uploading image...</div>
` : ''} ` : ''}
@ -655,13 +696,19 @@ export class DeesWysiwygBlock extends DeesElement {
public focus(): void { public focus(): void {
// Image blocks don't focus in the traditional way // Handle non-editable blocks
if (this.block?.type === 'image') { if (this.block?.type === 'image') {
const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement; const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement;
if (imageBlock) { if (imageBlock) {
imageBlock.focus(); imageBlock.focus();
} }
return; 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) // 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 { public focusWithCursor(position: 'start' | 'end' | number = 'end'): void {
// Image blocks don't support cursor positioning // Non-editable blocks don't support cursor positioning
if (this.block?.type === 'image') { if (this.block?.type === 'image' || this.block?.type === 'divider') {
this.focus(); this.focus();
return; 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 * Setup image block functionality
*/ */
@ -873,8 +956,17 @@ export class DeesWysiwygBlock extends DeesElement {
const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement; const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement;
if (!imageBlock) return; if (!imageBlock) return;
// Make the image block focusable // Note: tabindex is already set in the HTML
imageBlock.setAttribute('tabindex', '0');
// 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 // Handle click on upload placeholder
const uploadPlaceholder = imageBlock.querySelector('.image-upload-placeholder'); const uploadPlaceholder = imageBlock.querySelector('.image-upload-placeholder');
@ -931,7 +1023,14 @@ export class DeesWysiwygBlock extends DeesElement {
// Handle keyboard events // Handle keyboard events
imageBlock.addEventListener('keydown', (e) => { 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);
}
}); });
} }

View File

@ -35,6 +35,9 @@ export class WysiwygKeyboardHandler {
case 'Backspace': case 'Backspace':
await this.handleBackspace(e, block); await this.handleBackspace(e, block);
break; break;
case 'Delete':
await this.handleDelete(e, block);
break;
case 'ArrowUp': case 'ArrowUp':
await this.handleArrowUp(e, block); await this.handleArrowUp(e, block);
break; break;
@ -116,6 +119,14 @@ export class WysiwygKeyboardHandler {
private async handleEnter(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleEnter(e: KeyboardEvent, block: IBlock): Promise<void> {
const blockOps = this.component.blockOperations; 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 (block.type === 'code') {
if (e.shiftKey) { if (e.shiftKey) {
// Shift+Enter in code blocks creates a new block // Shift+Enter in code blocks creates a new block
@ -222,6 +233,41 @@ export class WysiwygKeyboardHandler {
private async handleBackspace(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleBackspace(e: KeyboardEvent, block: IBlock): Promise<void> {
const blockOps = this.component.blockOperations; 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 // Get the block component to check cursor position
const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`);
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as any; const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as any;
@ -321,10 +367,66 @@ export class WysiwygKeyboardHandler {
// Otherwise, let browser handle normal backspace // Otherwise, let browser handle normal backspace
} }
/**
* Handles Delete key
*/
private async handleDelete(e: KeyboardEvent, block: IBlock): Promise<void> {
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 * Handles ArrowUp key - navigate to previous block if at beginning or first line
*/ */
private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise<void> {
// 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 // Get the block component from the wysiwyg component's shadow DOM
const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`);
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
@ -352,9 +454,9 @@ export class WysiwygKeyboardHandler {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
if (prevBlock && prevBlock.type !== 'divider') { if (prevBlock) {
console.log('ArrowUp: Focusing previous block:', prevBlock.id); 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 // 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 * Handles ArrowDown key - navigate to next block if at end or last line
*/ */
private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise<void> {
// 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 // Get the block component from the wysiwyg component's shadow DOM
const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`);
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
@ -391,9 +505,9 @@ export class WysiwygKeyboardHandler {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock && nextBlock.type !== 'divider') { if (nextBlock) {
console.log('ArrowDown: Focusing next block:', nextBlock.id); 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 // Otherwise, let browser handle normal navigation
@ -419,6 +533,18 @@ export class WysiwygKeyboardHandler {
* Handles ArrowLeft key - navigate to previous block if at beginning * Handles ArrowLeft key - navigate to previous block if at beginning
*/ */
private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise<void> {
// 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 // Get the block component from the wysiwyg component's shadow DOM
const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`);
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
@ -448,9 +574,9 @@ export class WysiwygKeyboardHandler {
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
console.log('ArrowLeft: At start, previous block:', prevBlock?.id); console.log('ArrowLeft: At start, previous block:', prevBlock?.id);
if (prevBlock && prevBlock.type !== 'divider') { if (prevBlock) {
e.preventDefault(); 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 // 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 * Handles ArrowRight key - navigate to next block if at end
*/ */
private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise<void> {
// 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 // Get the block component from the wysiwyg component's shadow DOM
const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`);
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
@ -488,9 +626,9 @@ export class WysiwygKeyboardHandler {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock && nextBlock.type !== 'divider') { if (nextBlock) {
e.preventDefault(); 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 // Otherwise, let the browser handle normal right arrow navigation