update
This commit is contained in:
@ -123,25 +123,12 @@ export class DeesFormattingMenu extends DeesElement {
|
||||
class="formatting-menu"
|
||||
style="left: ${this.position.x}px; top: ${this.position.y}px;"
|
||||
tabindex="-1"
|
||||
@mousedown="${(e: MouseEvent) => {
|
||||
// Prevent focus loss
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}"
|
||||
@click="${(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}"
|
||||
@focus="${(e: FocusEvent) => {
|
||||
// Prevent menu from taking focus
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}"
|
||||
data-menu-type="formatting"
|
||||
>
|
||||
${WysiwygFormatting.formatButtons.map(button => html`
|
||||
<button
|
||||
class="format-button ${button.command}"
|
||||
@click="${() => this.applyFormat(button.command)}"
|
||||
data-command="${button.command}"
|
||||
title="${button.label}${button.shortcut ? ` (${button.shortcut})` : ''}"
|
||||
>
|
||||
<span class="${button.command === 'code' ? 'code-icon' : ''}">${button.icon}</span>
|
||||
@ -177,4 +164,40 @@ export class DeesFormattingMenu extends DeesElement {
|
||||
public updatePosition(position: { x: number; y: number }): void {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public firstUpdated(): void {
|
||||
// Set up event delegation for the menu
|
||||
this.shadowRoot?.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
const menu = this.shadowRoot?.querySelector('.formatting-menu');
|
||||
if (menu && menu.contains(e.target as Node)) {
|
||||
// Prevent focus loss
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
this.shadowRoot?.addEventListener('click', (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const button = target.closest('.format-button') as HTMLElement;
|
||||
|
||||
if (button) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const command = button.getAttribute('data-command');
|
||||
if (command) {
|
||||
this.applyFormat(command);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.shadowRoot?.addEventListener('focus', (e: FocusEvent) => {
|
||||
const menu = this.shadowRoot?.querySelector('.formatting-menu');
|
||||
if (menu && menu.contains(e.target as Node)) {
|
||||
// Prevent menu from taking focus
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, true); // Use capture phase
|
||||
}
|
||||
}
|
@ -121,6 +121,11 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
this.updateValue();
|
||||
this.editorContentRef = this.shadowRoot!.querySelector('.editor-content') as HTMLDivElement;
|
||||
|
||||
// Add click handler to editor content
|
||||
if (this.editorContentRef) {
|
||||
this.editorContentRef.addEventListener('click', (e) => this.handleEditorClick(e));
|
||||
}
|
||||
|
||||
// We now rely on block-level selection detection
|
||||
// No global selection listener needed
|
||||
|
||||
@ -300,7 +305,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
<div class="wysiwyg-container">
|
||||
<div
|
||||
class="editor-content ${this.draggedBlockId ? 'dragging' : ''}"
|
||||
@click="${this.handleEditorClick}"
|
||||
id="editor-content"
|
||||
>
|
||||
<!-- Blocks will be rendered programmatically -->
|
||||
|
@ -125,26 +125,13 @@ export class DeesSlashMenu extends DeesElement {
|
||||
class="slash-menu"
|
||||
style="left: ${this.position.x}px; top: ${this.position.y}px;"
|
||||
tabindex="-1"
|
||||
@mousedown="${(e: MouseEvent) => {
|
||||
// Prevent focus loss
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}"
|
||||
@click="${(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}"
|
||||
@focus="${(e: FocusEvent) => {
|
||||
// Prevent menu from taking focus
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}"
|
||||
data-menu-type="slash"
|
||||
>
|
||||
${menuItems.map((item, index) => html`
|
||||
<div
|
||||
class="slash-menu-item ${index === this.selectedIndex ? 'selected' : ''}"
|
||||
@click="${() => this.selectItem(item.type)}"
|
||||
@mouseenter="${() => this.selectedIndex = index}"
|
||||
data-item-type="${item.type}"
|
||||
data-item-index="${index}"
|
||||
>
|
||||
<span class="icon">${item.icon}</span>
|
||||
<span>${item.label}</span>
|
||||
@ -206,4 +193,50 @@ export class DeesSlashMenu extends DeesElement {
|
||||
this.selectItem(items[this.selectedIndex].type);
|
||||
}
|
||||
}
|
||||
|
||||
public firstUpdated(): void {
|
||||
// Set up event delegation
|
||||
this.shadowRoot?.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
const menu = this.shadowRoot?.querySelector('.slash-menu');
|
||||
if (menu && menu.contains(e.target as Node)) {
|
||||
// Prevent focus loss
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
this.shadowRoot?.addEventListener('click', (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const menuItem = target.closest('.slash-menu-item') as HTMLElement;
|
||||
|
||||
if (menuItem) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const itemType = menuItem.getAttribute('data-item-type');
|
||||
if (itemType) {
|
||||
this.selectItem(itemType);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.shadowRoot?.addEventListener('mouseenter', (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const menuItem = target.closest('.slash-menu-item') as HTMLElement;
|
||||
|
||||
if (menuItem) {
|
||||
const index = parseInt(menuItem.getAttribute('data-item-index') || '0', 10);
|
||||
this.selectedIndex = index;
|
||||
}
|
||||
}, true); // Use capture phase
|
||||
|
||||
this.shadowRoot?.addEventListener('focus', (e: FocusEvent) => {
|
||||
const menu = this.shadowRoot?.querySelector('.slash-menu');
|
||||
if (menu && menu.contains(e.target as Node)) {
|
||||
// Prevent menu from taking focus
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, true); // Use capture phase
|
||||
}
|
||||
}
|
@ -20,6 +20,13 @@ declare global {
|
||||
|
||||
@customElement('dees-wysiwyg-block')
|
||||
export class DeesWysiwygBlock extends DeesElement {
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
// Clean up selection handler
|
||||
if ((this as any)._selectionHandler) {
|
||||
document.removeEventListener('selectionchange', (this as any)._selectionHandler);
|
||||
}
|
||||
}
|
||||
@property({ type: Object })
|
||||
public block: IBlock;
|
||||
|
||||
@ -45,6 +52,7 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
|
||||
// Track cursor position
|
||||
private lastKnownCursorPosition: number = 0;
|
||||
private lastSelectedText: string = '';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
@ -309,20 +317,6 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
this.handlers?.onKeyDown?.(e);
|
||||
});
|
||||
|
||||
editableBlock.addEventListener('keyup', (e) => {
|
||||
// Track cursor position after key release
|
||||
const pos = this.getCursorPosition(editableBlock);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
|
||||
// Check for selection after keyboard navigation
|
||||
if (e.shiftKey || ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
||||
setTimeout(() => {
|
||||
this.checkForTextSelection();
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
|
||||
editableBlock.addEventListener('focus', () => {
|
||||
this.handlers?.onFocus?.();
|
||||
@ -341,21 +335,13 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
});
|
||||
|
||||
editableBlock.addEventListener('mouseup', (e) => {
|
||||
// Small delay to let browser set cursor position
|
||||
setTimeout(() => {
|
||||
const pos = this.getCursorPosition(editableBlock);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('Cursor position after mouseup:', pos);
|
||||
}
|
||||
|
||||
// Check for text selection with a longer delay
|
||||
setTimeout(() => {
|
||||
this.checkForTextSelection();
|
||||
}, 50);
|
||||
}, 0);
|
||||
|
||||
this.handleMouseUp(e);
|
||||
// Selection will be handled by selectionchange event
|
||||
this.handlers?.onMouseUp?.(e);
|
||||
});
|
||||
|
||||
@ -370,28 +356,102 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Add select event listener
|
||||
editableBlock.addEventListener('selectstart', () => {
|
||||
console.log('Selection started in block');
|
||||
});
|
||||
// Add selection change handler
|
||||
const checkSelection = () => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
// Listen for selection changes with a mutation observer
|
||||
let selectionCheckTimeout: any = null;
|
||||
const checkSelectionDebounced = () => {
|
||||
if (selectionCheckTimeout) clearTimeout(selectionCheckTimeout);
|
||||
selectionCheckTimeout = setTimeout(() => {
|
||||
this.checkForTextSelection();
|
||||
}, 100);
|
||||
const selectedText = selection.toString();
|
||||
if (selectedText.length === 0) {
|
||||
// Clear selection if no text
|
||||
if (this.lastSelectedText) {
|
||||
this.lastSelectedText = '';
|
||||
this.dispatchEvent(new CustomEvent('block-text-selected', {
|
||||
detail: {
|
||||
text: '',
|
||||
blockId: this.block.id,
|
||||
hasSelection: false
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get fresh reference to the editable block
|
||||
const currentEditableBlock = this.block?.type === 'code'
|
||||
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
|
||||
: this.shadowRoot?.querySelector('.block') as HTMLDivElement;
|
||||
|
||||
if (!currentEditableBlock) return;
|
||||
|
||||
// Get parent wysiwyg component's shadow root
|
||||
const parentComponent = this.closest('dees-input-wysiwyg');
|
||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
||||
|
||||
// Use getComposedRanges with shadow roots as per MDN docs
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
||||
if (this.shadowRoot) shadowRoots.push(this.shadowRoot);
|
||||
|
||||
// Get selection info using our Shadow DOM-aware utility
|
||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
||||
if (!selectionInfo) return;
|
||||
|
||||
// Check if selection is within this block
|
||||
const startInBlock = currentEditableBlock.contains(selectionInfo.startContainer);
|
||||
const endInBlock = currentEditableBlock.contains(selectionInfo.endContainer);
|
||||
|
||||
if (startInBlock || endInBlock) {
|
||||
if (selectedText !== this.lastSelectedText) {
|
||||
this.lastSelectedText = selectedText;
|
||||
console.log('✅ Selection detected in block using getComposedRanges:', selectedText);
|
||||
|
||||
// Create range and get rect
|
||||
const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
// Dispatch event
|
||||
this.dispatchEvent(new CustomEvent('block-text-selected', {
|
||||
detail: {
|
||||
text: selectedText.trim(),
|
||||
blockId: this.block.id,
|
||||
range: range,
|
||||
rect: rect,
|
||||
hasSelection: true
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
} else if (this.lastSelectedText) {
|
||||
// Clear selection if no longer in this block
|
||||
this.lastSelectedText = '';
|
||||
this.dispatchEvent(new CustomEvent('block-text-selected', {
|
||||
detail: {
|
||||
text: '',
|
||||
blockId: this.block.id,
|
||||
hasSelection: false
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Check selection on various events
|
||||
editableBlock.addEventListener('mouseup', checkSelectionDebounced);
|
||||
editableBlock.addEventListener('keyup', checkSelectionDebounced);
|
||||
document.addEventListener('selectionchange', () => {
|
||||
// Check if this block has focus
|
||||
if (document.activeElement === editableBlock ||
|
||||
this.shadowRoot?.activeElement === editableBlock) {
|
||||
checkSelectionDebounced();
|
||||
// Listen for selection changes
|
||||
document.addEventListener('selectionchange', checkSelection);
|
||||
|
||||
// Store the handler for cleanup
|
||||
(this as any)._selectionHandler = checkSelection;
|
||||
|
||||
// Add keyup handler for cursor position tracking
|
||||
editableBlock.addEventListener('keyup', (e) => {
|
||||
// Track cursor position
|
||||
const pos = this.getCursorPosition(editableBlock);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
}
|
||||
});
|
||||
|
||||
@ -814,72 +874,4 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
};
|
||||
}
|
||||
|
||||
private handleMouseUp(_e: MouseEvent): void {
|
||||
// Selection check is now handled in the mouseup event listener
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's text selected within this block
|
||||
*/
|
||||
private checkForTextSelection(): void {
|
||||
const editableElement = this.block?.type === 'code'
|
||||
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
|
||||
: this.shadowRoot?.querySelector('.block') as HTMLDivElement;
|
||||
if (!editableElement) {
|
||||
console.log('checkForTextSelection: No editable element found');
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
console.log('checkForTextSelection: No selection or range count is 0');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedText = selection.toString();
|
||||
console.log('checkForTextSelection: Selected text raw:', selectedText, 'length:', selectedText.length);
|
||||
|
||||
// Only proceed if we have selected text
|
||||
if (selectedText.trim().length === 0) {
|
||||
console.log('checkForTextSelection: Selected text is empty after trim');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the selection is within this block
|
||||
const range = selection.getRangeAt(0);
|
||||
console.log('checkForTextSelection: Range:', {
|
||||
startContainer: range.startContainer,
|
||||
endContainer: range.endContainer,
|
||||
collapsed: range.collapsed
|
||||
});
|
||||
|
||||
// Check if both start and end are within our editable element
|
||||
const startInBlock = editableElement.contains(range.startContainer);
|
||||
const endInBlock = editableElement.contains(range.endContainer);
|
||||
|
||||
console.log('checkForTextSelection: Start in block:', startInBlock, 'End in block:', endInBlock);
|
||||
|
||||
if (startInBlock && endInBlock) {
|
||||
console.log('✅ Block detected text selection:', selectedText.trim());
|
||||
|
||||
// Get the bounding rect of the selection
|
||||
const rect = range.getBoundingClientRect();
|
||||
console.log('Selection rect:', rect);
|
||||
|
||||
// Dispatch event to parent with selection details
|
||||
this.dispatchEvent(new CustomEvent('block-text-selected', {
|
||||
detail: {
|
||||
text: selectedText.trim(),
|
||||
blockId: this.block.id,
|
||||
range: range,
|
||||
rect: rect,
|
||||
hasSelection: true
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
} else {
|
||||
console.log('checkForTextSelection: Selection not contained in block');
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user