Compare commits

..

4 Commits

Author SHA1 Message Date
113c013ea9 1.9.2
Some checks failed
Default (tags) / security (push) Failing after 26s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-24 23:57:32 +00:00
0571d5bf4b fi(wysiwyg): fix navigation 2025-06-24 23:56:40 +00:00
5f86fdba72 update 2025-06-24 23:46:52 +00:00
474385a939 update 2025-06-24 23:15:56 +00:00
12 changed files with 404 additions and 30 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "1.9.1", "version": "1.9.2",
"private": false, "private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",

View File

@ -1,5 +1,43 @@
# WYSIWYG Editor Refactoring Progress Summary # WYSIWYG Editor Refactoring Progress Summary
## Latest Updates
### Selection Highlighting Fix ✅
- **Issue**: "Paragraphs are not highlighted consistently, headings are always highlighted"
- **Root Cause**: The `shouldUpdate` method in `dees-wysiwyg-block.ts` was using a generic `.block` selector that would match the first element with that class, not necessarily the correct block element
- **Solution**: Changed the selector to be more specific: `.block.${blockType}` which ensures the correct element is found for each block type
- **Result**: All block types now highlight consistently when selected
### Enter Key Block Creation Fix ✅
- **Issue**: "When pressing enter and jumping to new block then typing something: The cursor is not at the beginning of the new block and there is content"
- **Root Cause**: Block handlers were rendering content with template syntax `${block.content || ''}` in their render methods, which violates the static HTML principle
- **Solution**:
- Removed all `${block.content}` from render methods in paragraph, heading, quote, and code block handlers
- Content is now set programmatically in the setup() method only when needed
- Fixed `setCursorToStart` and `setCursorToEnd` to always find elements fresh instead of relying on cached `blockElement`
- **Result**: New empty blocks remain truly empty, cursor positioning works correctly
### Backspace Key Deletion Fix ✅
- **Issue**: "After typing in a new block, pressing backspace deletes the whole block instead of just the last character"
- **Root Cause**:
1. `getCursorPositionInElement` was using `element.contains()` which doesn't work across Shadow DOM boundaries
2. The backspace handler was checking `block.content === ''` which only contains the stored content, not the actual DOM content
- **Solution**:
1. Fixed `getCursorPositionInElement` to use `containsAcrossShadowDOM` for proper Shadow DOM support
2. Updated backspace handler to get actual content from DOM using `blockComponent.getContent()` instead of relying on stored `block.content`
3. Added debug logging to track cursor position and content state
- **Result**: Backspace now correctly deletes individual characters instead of the whole block
### Arrow Left Navigation Fix ✅
- **Issue**: "When jumping to the previous block from the beginning of a block with arrow left, the cursor should be at the end of the previous block, not at the start"
- **Root Cause**: Browser's default focus behavior places cursor at the beginning of contenteditable elements, overriding our cursor positioning
- **Solution**: For 'end' position, set up the selection range BEFORE focusing the element:
1. Create a range pointing to the end of content
2. Apply the selection
3. Then focus the element (which preserves the existing selection)
4. Only use setCursorToEnd for empty blocks
- **Result**: Arrow left navigation now correctly places cursor at the end of the previous block
## Completed Phases ## Completed Phases
### Phase 1: Infrastructure ✅ ### Phase 1: Infrastructure ✅

View File

@ -0,0 +1,9 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
tap.test('should create wysiwyg editor', async () => {
const editor = new DeesInputWysiwyg();
expect(editor).toBeInstanceOf(DeesInputWysiwyg);
});
export default tap.start();

View File

@ -0,0 +1,156 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
tap.test('Selection highlighting should work consistently for all block types', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import various block types
editor.importBlocks([
{ id: 'para-1', type: 'paragraph', content: 'This is a paragraph' },
{ id: 'heading-1', type: 'heading-1', content: 'This is a heading' },
{ id: 'quote-1', type: 'quote', content: 'This is a quote' },
{ id: 'code-1', type: 'code', content: 'const x = 42;' },
{ id: 'list-1', type: 'list', content: 'Item 1\nItem 2' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Test paragraph highlighting
console.log('Testing paragraph highlighting...');
const paraWrapper = editor.shadowRoot?.querySelector('[data-block-id="para-1"]');
const paraComponent = paraWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const paraContainer = paraComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const paraElement = paraContainer?.querySelector('.block.paragraph') as HTMLElement;
// Focus paragraph to select it
paraElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if paragraph has selected class
const paraHasSelected = paraElement.classList.contains('selected');
console.log('Paragraph has selected class:', paraHasSelected);
// Check computed styles
const paraStyle = window.getComputedStyle(paraElement);
console.log('Paragraph background:', paraStyle.background);
console.log('Paragraph box-shadow:', paraStyle.boxShadow);
// Test heading highlighting
console.log('\nTesting heading highlighting...');
const headingWrapper = editor.shadowRoot?.querySelector('[data-block-id="heading-1"]');
const headingComponent = headingWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const headingContainer = headingComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const headingElement = headingContainer?.querySelector('.block.heading-1') as HTMLElement;
// Focus heading to select it
headingElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if heading has selected class
const headingHasSelected = headingElement.classList.contains('selected');
console.log('Heading has selected class:', headingHasSelected);
// Check computed styles
const headingStyle = window.getComputedStyle(headingElement);
console.log('Heading background:', headingStyle.background);
console.log('Heading box-shadow:', headingStyle.boxShadow);
// Test quote highlighting
console.log('\nTesting quote highlighting...');
const quoteWrapper = editor.shadowRoot?.querySelector('[data-block-id="quote-1"]');
const quoteComponent = quoteWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const quoteContainer = quoteComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const quoteElement = quoteContainer?.querySelector('.block.quote') as HTMLElement;
// Focus quote to select it
quoteElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if quote has selected class
const quoteHasSelected = quoteElement.classList.contains('selected');
console.log('Quote has selected class:', quoteHasSelected);
// Test code highlighting
console.log('\nTesting code highlighting...');
const codeWrapper = editor.shadowRoot?.querySelector('[data-block-id="code-1"]');
const codeComponent = codeWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const codeContainer = codeComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const codeElement = codeContainer?.querySelector('.block.code') as HTMLElement;
// Focus code to select it
codeElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if code has selected class
const codeHasSelected = codeElement.classList.contains('selected');
console.log('Code has selected class:', codeHasSelected);
// Focus back on paragraph and check if others are deselected
console.log('\nFocusing back on paragraph...');
paraElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check that only paragraph is selected
expect(paraElement.classList.contains('selected')).toBeTrue();
expect(headingElement.classList.contains('selected')).toBeFalse();
expect(quoteElement.classList.contains('selected')).toBeFalse();
expect(codeElement.classList.contains('selected')).toBeFalse();
console.log('Selection highlighting test complete');
});
tap.test('Selected class should toggle correctly when clicking between blocks', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import two blocks
editor.importBlocks([
{ id: 'block-1', type: 'paragraph', content: 'First block' },
{ id: 'block-2', type: 'paragraph', content: 'Second block' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Get both blocks
const block1Wrapper = editor.shadowRoot?.querySelector('[data-block-id="block-1"]');
const block1Component = block1Wrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const block1Container = block1Component?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const block1Element = block1Container?.querySelector('.block.paragraph') as HTMLElement;
const block2Wrapper = editor.shadowRoot?.querySelector('[data-block-id="block-2"]');
const block2Component = block2Wrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const block2Container = block2Component?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const block2Element = block2Container?.querySelector('.block.paragraph') as HTMLElement;
// Initially neither should be selected
expect(block1Element.classList.contains('selected')).toBeFalse();
expect(block2Element.classList.contains('selected')).toBeFalse();
// Click on first block
block1Element.click();
block1Element.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// First block should be selected
expect(block1Element.classList.contains('selected')).toBeTrue();
expect(block2Element.classList.contains('selected')).toBeFalse();
// Click on second block
block2Element.click();
block2Element.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Second block should be selected, first should not
expect(block1Element.classList.contains('selected')).toBeFalse();
expect(block2Element.classList.contains('selected')).toBeTrue();
console.log('Toggle test complete');
});
export default tap.start();

View File

@ -0,0 +1,62 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
tap.test('Selection highlighting basic test', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import two blocks
editor.importBlocks([
{ id: 'para-1', type: 'paragraph', content: 'First paragraph' },
{ id: 'head-1', type: 'heading-1', content: 'First heading' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 500));
// Get paragraph element
const paraWrapper = editor.shadowRoot?.querySelector('[data-block-id="para-1"]');
const paraComponent = paraWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const paraBlock = paraComponent?.shadowRoot?.querySelector('.block.paragraph') as HTMLElement;
// Get heading element
const headWrapper = editor.shadowRoot?.querySelector('[data-block-id="head-1"]');
const headComponent = headWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const headBlock = headComponent?.shadowRoot?.querySelector('.block.heading-1') as HTMLElement;
console.log('Found elements:', {
paraBlock: !!paraBlock,
headBlock: !!headBlock
});
// Focus paragraph
paraBlock.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check classes
console.log('Paragraph classes:', paraBlock.className);
console.log('Heading classes:', headBlock.className);
// Check isSelected property
console.log('Paragraph component isSelected:', paraComponent.isSelected);
console.log('Heading component isSelected:', headComponent.isSelected);
// Focus heading
headBlock.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check classes again
console.log('\nAfter focusing heading:');
console.log('Paragraph classes:', paraBlock.className);
console.log('Heading classes:', headBlock.className);
console.log('Paragraph component isSelected:', paraComponent.isSelected);
console.log('Heading component isSelected:', headComponent.isSelected);
// Check that heading is selected
expect(headBlock.classList.contains('selected')).toBeTrue();
expect(paraBlock.classList.contains('selected')).toBeFalse();
});
export default tap.start();

View File

@ -25,7 +25,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
data-block-id="${block.id}" data-block-id="${block.id}"
data-block-type="${block.type}" data-block-type="${block.type}"
spellcheck="false" spellcheck="false"
>${block.content || ''}</div> ></div>
</div> </div>
`; `;
} }

View File

@ -32,7 +32,7 @@ export class HeadingBlockHandler extends BaseBlockHandler {
data-placeholder="${placeholder}" data-placeholder="${placeholder}"
data-block-id="${block.id}" data-block-id="${block.id}"
data-block-type="${block.type}" data-block-type="${block.type}"
>${block.content || ''}</div> ></div>
`; `;
} }
@ -280,6 +280,22 @@ export class HeadingBlockHandler extends BaseBlockHandler {
} }
} }
/**
* Helper to get the last text node in an element
*/
private getLastTextNode(element: Node): Text | null {
if (element.nodeType === Node.TEXT_NODE) {
return element as Text;
}
for (let i = element.childNodes.length - 1; i >= 0; i--) {
const lastText = this.getLastTextNode(element.childNodes[i]);
if (lastText) return lastText;
}
return null;
}
// Helper methods for heading functionality (mostly the same as paragraph) // Helper methods for heading functionality (mostly the same as paragraph)
getCursorPosition(element: HTMLElement, context?: any): number | null { getCursorPosition(element: HTMLElement, context?: any): number | null {
@ -404,19 +420,40 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement; const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
if (!headingBlock) return; if (!headingBlock) return;
// Ensure element is focusable first // Ensure element is focusable first
if (!headingBlock.hasAttribute('contenteditable')) { if (!headingBlock.hasAttribute('contenteditable')) {
headingBlock.setAttribute('contenteditable', 'true'); headingBlock.setAttribute('contenteditable', 'true');
} }
// Focus the element // For 'end' position, we need to set up selection before focus to prevent browser default
if (position === 'end' && headingBlock.textContent && headingBlock.textContent.length > 0) {
// Set up the selection first
const sel = window.getSelection();
if (sel) {
const range = document.createRange();
const lastNode = this.getLastTextNode(headingBlock) || headingBlock;
if (lastNode.nodeType === Node.TEXT_NODE) {
range.setStart(lastNode, lastNode.textContent?.length || 0);
range.setEnd(lastNode, lastNode.textContent?.length || 0);
} else {
range.selectNodeContents(lastNode);
range.collapse(false);
}
sel.removeAllRanges();
sel.addRange(range);
}
}
// Now focus the element
headingBlock.focus(); headingBlock.focus();
// Set cursor position after focus is established // Set cursor position after focus is established (for non-end positions)
const setCursor = () => { const setCursor = () => {
if (position === 'start') { if (position === 'start') {
this.setCursorToStart(element, context); this.setCursorToStart(element, context);
} else if (position === 'end') { } else if (position === 'end' && (!headingBlock.textContent || headingBlock.textContent.length === 0)) {
// Only call setCursorToEnd for empty blocks
this.setCursorToEnd(element, context); this.setCursorToEnd(element, context);
} else if (typeof position === 'number') { } else if (typeof position === 'number') {
// Use the selection utility to set cursor position // Use the selection utility to set cursor position
@ -432,6 +469,13 @@ export class HeadingBlockHandler extends BaseBlockHandler {
Promise.resolve().then(() => { Promise.resolve().then(() => {
if (document.activeElement === headingBlock || element.shadowRoot?.activeElement === headingBlock) { if (document.activeElement === headingBlock || element.shadowRoot?.activeElement === headingBlock) {
setCursor(); setCursor();
} else {
// Try again with a small delay - sometimes focus needs more time
setTimeout(() => {
if (document.activeElement === headingBlock || element.shadowRoot?.activeElement === headingBlock) {
setCursor();
}
}, 10);
} }
}); });
} }

View File

@ -25,7 +25,7 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
data-placeholder="${placeholder}" data-placeholder="${placeholder}"
data-block-id="${block.id}" data-block-id="${block.id}"
data-block-type="${block.type}" data-block-type="${block.type}"
>${block.content || ''}</div> ></div>
`; `;
} }
@ -246,6 +246,22 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
return "Type '/' for commands..."; return "Type '/' for commands...";
} }
/**
* Helper to get the last text node in an element
*/
private getLastTextNode(element: Node): Text | null {
if (element.nodeType === Node.TEXT_NODE) {
return element as Text;
}
for (let i = element.childNodes.length - 1; i >= 0; i--) {
const lastText = this.getLastTextNode(element.childNodes[i]);
if (lastText) return lastText;
}
return null;
}
// Helper methods for paragraph functionality // Helper methods for paragraph functionality
getCursorPosition(element: HTMLElement, context?: any): number | null { getCursorPosition(element: HTMLElement, context?: any): number | null {
@ -376,19 +392,40 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement; const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
if (!paragraphBlock) return; if (!paragraphBlock) return;
// Ensure element is focusable first // Ensure element is focusable first
if (!paragraphBlock.hasAttribute('contenteditable')) { if (!paragraphBlock.hasAttribute('contenteditable')) {
paragraphBlock.setAttribute('contenteditable', 'true'); paragraphBlock.setAttribute('contenteditable', 'true');
} }
// Focus the element // For 'end' position, we need to set up selection before focus to prevent browser default
if (position === 'end' && paragraphBlock.textContent && paragraphBlock.textContent.length > 0) {
// Set up the selection first
const sel = window.getSelection();
if (sel) {
const range = document.createRange();
const lastNode = this.getLastTextNode(paragraphBlock) || paragraphBlock;
if (lastNode.nodeType === Node.TEXT_NODE) {
range.setStart(lastNode, lastNode.textContent?.length || 0);
range.setEnd(lastNode, lastNode.textContent?.length || 0);
} else {
range.selectNodeContents(lastNode);
range.collapse(false);
}
sel.removeAllRanges();
sel.addRange(range);
}
}
// Now focus the element
paragraphBlock.focus(); paragraphBlock.focus();
// Set cursor position after focus is established // Set cursor position after focus is established (for non-end positions)
const setCursor = () => { const setCursor = () => {
if (position === 'start') { if (position === 'start') {
this.setCursorToStart(element, context); this.setCursorToStart(element, context);
} else if (position === 'end') { } else if (position === 'end' && (!paragraphBlock.textContent || paragraphBlock.textContent.length === 0)) {
// Only call setCursorToEnd for empty blocks
this.setCursorToEnd(element, context); this.setCursorToEnd(element, context);
} else if (typeof position === 'number') { } else if (typeof position === 'number') {
// Use the selection utility to set cursor position // Use the selection utility to set cursor position
@ -404,6 +441,13 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
Promise.resolve().then(() => { Promise.resolve().then(() => {
if (document.activeElement === paragraphBlock || element.shadowRoot?.activeElement === paragraphBlock) { if (document.activeElement === paragraphBlock || element.shadowRoot?.activeElement === paragraphBlock) {
setCursor(); setCursor();
} else {
// Try again with a small delay - sometimes focus needs more time
setTimeout(() => {
if (document.activeElement === paragraphBlock || element.shadowRoot?.activeElement === paragraphBlock) {
setCursor();
}
}, 10);
} }
}); });
} }

View File

@ -25,7 +25,7 @@ export class QuoteBlockHandler extends BaseBlockHandler {
data-placeholder="${placeholder}" data-placeholder="${placeholder}"
data-block-id="${block.id}" data-block-id="${block.id}"
data-block-type="${block.type}" data-block-type="${block.type}"
>${block.content || ''}</div> ></div>
`; `;
} }

View File

@ -699,11 +699,17 @@ 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 selection state changed, update the selected class without re-rendering
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; if (changedProperties.has('isSelected') && this.block) {
if (changedProperties.has('isSelected') && this.block && nonEditableTypes.includes(this.block.type)) { // Find the block element based on block type
// For non-editable blocks, we need to update the selected class let element: HTMLElement | null = null;
const element = this.shadowRoot?.querySelector('.block') as HTMLElement;
// Build the specific selector based on block type
const blockType = this.block.type;
const selector = `.block.${blockType}`;
element = this.shadowRoot?.querySelector(selector) as HTMLElement;
if (element) { if (element) {
if (this.isSelected) { if (this.isSelected) {
element.classList.add('selected'); element.classList.add('selected');
@ -1383,9 +1389,10 @@ export class DeesWysiwygBlock extends DeesElement {
return handler.setCursorToStart(container, context); return handler.setCursorToStart(container, context);
} }
// Always find the element fresh, don't rely on cached blockElement
const editableElement = this.block?.type === 'code' const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.blockElement; : this.shadowRoot?.querySelector('.block') as HTMLDivElement;
if (editableElement) { if (editableElement) {
WysiwygBlocks.setCursorToStart(editableElement); WysiwygBlocks.setCursorToStart(editableElement);
} }
@ -1400,9 +1407,10 @@ export class DeesWysiwygBlock extends DeesElement {
return handler.setCursorToEnd(container, context); return handler.setCursorToEnd(container, context);
} }
// Always find the element fresh, don't rely on cached blockElement
const editableElement = this.block?.type === 'code' const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.blockElement; : this.shadowRoot?.querySelector('.block') as HTMLDivElement;
if (editableElement) { if (editableElement) {
WysiwygBlocks.setCursorToEnd(editableElement); WysiwygBlocks.setCursorToEnd(editableElement);
} }

View File

@ -306,6 +306,8 @@ export class WysiwygKeyboardHandler {
const cursorPos = WysiwygSelection.getCursorPositionInElement(target, ...shadowRoots); const cursorPos = WysiwygSelection.getCursorPositionInElement(target, ...shadowRoots);
const actualContent = blockComponent.getContent ? blockComponent.getContent() : target.textContent;
// Check if cursor is at the beginning of the block // Check if cursor is at the beginning of the block
if (cursorPos === 0) { if (cursorPos === 0) {
e.preventDefault(); e.preventDefault();
@ -335,7 +337,8 @@ export class WysiwygKeyboardHandler {
if (block.type === 'code' && prevBlock.type !== 'code') { if (block.type === 'code' && prevBlock.type !== 'code') {
// Can't merge code into non-code block // Can't merge code into non-code block
if (block.content === '') { const actualContent = blockComponent.getContent ? blockComponent.getContent() : block.content;
if (actualContent === '' || actualContent.trim() === '') {
blockOps.removeBlock(block.id); blockOps.removeBlock(block.id);
await blockOps.focusBlock(prevBlock.id, 'end'); await blockOps.focusBlock(prevBlock.id, 'end');
} }
@ -376,7 +379,11 @@ export class WysiwygKeyboardHandler {
// Focus previous block at merge point // Focus previous block at merge point
await blockOps.focusBlock(prevBlock.id, mergePoint); await blockOps.focusBlock(prevBlock.id, mergePoint);
} }
} else if (block.content === '' && this.component.blocks.length > 1) { } else if (this.component.blocks.length > 1) {
// Check if block is actually empty by getting current content from DOM
const currentContent = blockComponent.getContent ? blockComponent.getContent() : block.content;
if (currentContent === '' || currentContent.trim() === '') {
// Empty block - just remove it // Empty block - just remove it
e.preventDefault(); e.preventDefault();
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
@ -389,6 +396,7 @@ export class WysiwygKeyboardHandler {
} }
} }
} }
}
// Otherwise, let browser handle normal backspace // Otherwise, let browser handle normal backspace
} }
@ -655,7 +663,8 @@ export class WysiwygKeyboardHandler {
if (prevBlock) { if (prevBlock) {
e.preventDefault(); e.preventDefault();
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'); const position = nonEditableTypes.includes(prevBlock.type) ? undefined : 'end';
await blockOps.focusBlock(prevBlock.id, position);
} }
} }
// Otherwise, let the browser handle normal left arrow navigation // Otherwise, let the browser handle normal left arrow navigation

View File

@ -122,9 +122,13 @@ export class WysiwygSelection {
range.selectNodeContents(element); range.selectNodeContents(element);
// Handle case where selection is in a text node that's a child of the element // Handle case where selection is in a text node that's a child of the element
if (element.contains(selectionInfo.startContainer)) { // Use our Shadow DOM-aware contains method
const isContained = this.containsAcrossShadowDOM(element, selectionInfo.startContainer);
if (isContained) {
range.setEnd(selectionInfo.startContainer, selectionInfo.startOffset); range.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
return range.toString().length; const position = range.toString().length;
return position;
} else { } else {
// Selection might be in shadow DOM or different context // Selection might be in shadow DOM or different context
// Try to find the equivalent position in the element // Try to find the equivalent position in the element