fix(dees-modal): theming
This commit is contained in:
@@ -24,7 +24,9 @@ import {
|
||||
WysiwygInputHandler,
|
||||
WysiwygKeyboardHandler,
|
||||
WysiwygDragDropHandler,
|
||||
WysiwygModalManager
|
||||
WysiwygModalManager,
|
||||
DeesSlashMenu,
|
||||
DeesFormattingMenu
|
||||
} from './index.js';
|
||||
|
||||
declare global {
|
||||
@@ -55,17 +57,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
@state()
|
||||
private selectedBlockId: string | null = null;
|
||||
|
||||
@state()
|
||||
private showSlashMenu: boolean = false;
|
||||
|
||||
@state()
|
||||
private slashMenuPosition: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
@state()
|
||||
private slashMenuFilter: string = '';
|
||||
|
||||
@state()
|
||||
private slashMenuSelectedIndex: number = 0;
|
||||
// Slash menu is now globally rendered
|
||||
private slashMenu = DeesSlashMenu.getInstance();
|
||||
|
||||
@state()
|
||||
private draggedBlockId: string | null = null;
|
||||
@@ -76,11 +69,8 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
@state()
|
||||
private dragOverPosition: 'before' | 'after' | null = null;
|
||||
|
||||
@state()
|
||||
private showFormattingMenu: boolean = false;
|
||||
|
||||
@state()
|
||||
private formattingMenuPosition: { x: number; y: number } = { x: 0, y: 0 };
|
||||
// Formatting menu is now globally rendered
|
||||
private formattingMenu = DeesFormattingMenu.getInstance();
|
||||
|
||||
@state()
|
||||
private selectedText: string = '';
|
||||
@@ -129,8 +119,17 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
// Add global selection listener
|
||||
console.log('Adding selectionchange listener');
|
||||
document.addEventListener('selectionchange', this.selectionChangeHandler);
|
||||
|
||||
// Listen for custom selection events from blocks
|
||||
this.addEventListener('block-text-selected', (e: CustomEvent) => {
|
||||
if (!this.slashMenu.visible) {
|
||||
this.selectedText = e.detail.text;
|
||||
this.updateFormattingMenuPosition();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
<dees-label
|
||||
@@ -145,8 +144,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
>
|
||||
${this.blocks.map(block => this.renderBlock(block))}
|
||||
</div>
|
||||
${this.showSlashMenu ? this.renderSlashMenu() : ''}
|
||||
${this.showFormattingMenu ? this.renderFormattingMenu() : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -173,15 +170,19 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
@dragend="${() => this.dragDropHandler.handleDragEnd()}"
|
||||
></div>
|
||||
` : ''}
|
||||
${WysiwygBlocks.renderBlock(block, isSelected, {
|
||||
onInput: (e: InputEvent) => this.inputHandler.handleBlockInput(e, block),
|
||||
onKeyDown: (e: KeyboardEvent) => this.keyboardHandler.handleBlockKeyDown(e, block),
|
||||
onFocus: () => this.handleBlockFocus(block),
|
||||
onBlur: () => this.handleBlockBlur(block),
|
||||
onCompositionStart: () => this.isComposing = true,
|
||||
onCompositionEnd: () => this.isComposing = false,
|
||||
onMouseUp: (e: MouseEvent) => this.handleTextSelection(e),
|
||||
})}
|
||||
<dees-wysiwyg-block
|
||||
.block="${block}"
|
||||
.isSelected="${isSelected}"
|
||||
.handlers="${{
|
||||
onInput: (e: InputEvent) => this.inputHandler.handleBlockInput(e, block),
|
||||
onKeyDown: (e: KeyboardEvent) => this.keyboardHandler.handleBlockKeyDown(e, block),
|
||||
onFocus: () => this.handleBlockFocus(block),
|
||||
onBlur: () => this.handleBlockBlur(block),
|
||||
onCompositionStart: () => this.isComposing = true,
|
||||
onCompositionEnd: () => this.isComposing = false,
|
||||
onMouseUp: (e: MouseEvent) => this.handleTextSelection(e),
|
||||
}}"
|
||||
></dees-wysiwyg-block>
|
||||
${block.type !== 'divider' ? html`
|
||||
<div
|
||||
class="block-settings"
|
||||
@@ -205,311 +206,56 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
`;
|
||||
}
|
||||
|
||||
public getFilteredMenuItems(): ISlashMenuItem[] {
|
||||
const allItems = WysiwygShortcuts.getSlashMenuItems();
|
||||
return allItems.filter(item =>
|
||||
this.slashMenuFilter === '' ||
|
||||
item.label.toLowerCase().includes(this.slashMenuFilter.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
private renderSlashMenu(): TemplateResult {
|
||||
const menuItems = this.getFilteredMenuItems();
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="slash-menu"
|
||||
style="top: ${this.slashMenuPosition.y}px; left: ${this.slashMenuPosition.x}px;"
|
||||
>
|
||||
${menuItems.map((item, index) => html`
|
||||
<div
|
||||
class="slash-menu-item ${index === this.slashMenuSelectedIndex ? 'selected' : ''}"
|
||||
@click="${() => this.insertBlock(item.type as IBlock['type'])}"
|
||||
@mouseenter="${() => this.slashMenuSelectedIndex = index}"
|
||||
>
|
||||
<span class="icon">${item.icon}</span>
|
||||
<span>${item.label}</span>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderFormattingMenu(): TemplateResult {
|
||||
return WysiwygFormatting.renderFormattingMenu(
|
||||
this.formattingMenuPosition,
|
||||
(command) => this.applyFormat(command)
|
||||
);
|
||||
}
|
||||
|
||||
private handleBlockInput(e: InputEvent, block: IBlock) {
|
||||
if (this.isComposing) return;
|
||||
|
||||
const target = e.target as HTMLDivElement;
|
||||
|
||||
if (block.type === 'list') {
|
||||
// Extract text from list items
|
||||
const listItems = target.querySelectorAll('li');
|
||||
block.content = Array.from(listItems).map(li => li.textContent || '').join('\n');
|
||||
// Preserve list type
|
||||
const listElement = target.querySelector('ol, ul');
|
||||
if (listElement) {
|
||||
block.metadata = { listType: listElement.tagName.toLowerCase() === 'ol' ? 'ordered' : 'bullet' };
|
||||
}
|
||||
} else if (block.type === 'code') {
|
||||
// For code blocks, preserve the exact text content
|
||||
block.content = target.textContent || '';
|
||||
} else {
|
||||
// For other blocks, preserve HTML formatting
|
||||
block.content = target.innerHTML || '';
|
||||
}
|
||||
|
||||
// Check for block type change intents (use text content for detection, not HTML)
|
||||
const textContent = target.textContent || '';
|
||||
const detectedType = this.detectBlockTypeIntent(textContent);
|
||||
|
||||
// Only process if the detected type is different from current type
|
||||
if (detectedType && detectedType.type !== block.type) {
|
||||
e.preventDefault();
|
||||
|
||||
// Handle special cases
|
||||
if (detectedType.type === 'list') {
|
||||
block.type = 'list';
|
||||
block.content = '';
|
||||
block.metadata = { listType: detectedType.listType };
|
||||
|
||||
// Update list structure immediately
|
||||
const listTag = detectedType.listType === 'ordered' ? 'ol' : 'ul';
|
||||
target.innerHTML = `<${listTag}><li></li></${listTag}>`;
|
||||
|
||||
// Force update and focus
|
||||
this.updateValue();
|
||||
this.requestUpdate();
|
||||
|
||||
setTimeout(() => {
|
||||
WysiwygBlocks.focusListItem(target);
|
||||
}, 0);
|
||||
|
||||
return;
|
||||
} else if (detectedType.type === 'divider') {
|
||||
block.type = 'divider';
|
||||
block.content = ' ';
|
||||
|
||||
// Create a new paragraph block after the divider
|
||||
const newBlock = this.createNewBlock();
|
||||
this.insertBlockAfter(block, newBlock);
|
||||
|
||||
this.updateValue();
|
||||
this.requestUpdate();
|
||||
return;
|
||||
} else if (detectedType.type === 'code') {
|
||||
// For code blocks, ask for language
|
||||
WysiwygModalManager.showLanguageSelectionModal().then(language => {
|
||||
if (language) {
|
||||
block.type = 'code';
|
||||
block.content = '';
|
||||
block.metadata = { language };
|
||||
|
||||
// Clear the DOM element immediately
|
||||
target.textContent = '';
|
||||
|
||||
// Force update
|
||||
this.updateValue();
|
||||
this.requestUpdate();
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
// For all other block types
|
||||
block.type = detectedType.type;
|
||||
block.content = '';
|
||||
|
||||
// Clear the DOM element immediately
|
||||
target.textContent = '';
|
||||
|
||||
// Force update
|
||||
this.updateValue();
|
||||
this.requestUpdate();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for slash commands at the beginning of any block
|
||||
if (textContent === '/' || (textContent.startsWith('/') && this.showSlashMenu)) {
|
||||
// Only show menu on initial '/', or update filter if already showing
|
||||
if (!this.showSlashMenu && textContent === '/') {
|
||||
this.showSlashMenu = true;
|
||||
this.slashMenuSelectedIndex = 0;
|
||||
const rect = target.getBoundingClientRect();
|
||||
const containerRect = this.shadowRoot!.querySelector('.wysiwyg-container')!.getBoundingClientRect();
|
||||
this.slashMenuPosition = {
|
||||
x: rect.left - containerRect.left,
|
||||
y: rect.bottom - containerRect.top + 4
|
||||
};
|
||||
}
|
||||
this.slashMenuFilter = textContent.slice(1);
|
||||
} else if (!textContent.startsWith('/')) {
|
||||
this.closeSlashMenu();
|
||||
}
|
||||
|
||||
// Don't update value on every input - let the browser handle typing normally
|
||||
// But schedule a save after a delay
|
||||
// Removed - now handled by inputHandler
|
||||
}
|
||||
|
||||
private handleBlockKeyDown(e: KeyboardEvent, block: IBlock) {
|
||||
if (this.showSlashMenu && ['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.key)) {
|
||||
this.handleSlashMenuKeyboard(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle formatting shortcuts
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
switch (e.key.toLowerCase()) {
|
||||
case 'b':
|
||||
e.preventDefault();
|
||||
this.applyFormat('bold');
|
||||
return;
|
||||
case 'i':
|
||||
e.preventDefault();
|
||||
this.applyFormat('italic');
|
||||
return;
|
||||
case 'u':
|
||||
e.preventDefault();
|
||||
this.applyFormat('underline');
|
||||
return;
|
||||
case 'k':
|
||||
e.preventDefault();
|
||||
this.applyFormat('link');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Tab key for indentation
|
||||
if (e.key === 'Tab') {
|
||||
if (block.type === 'code') {
|
||||
// Allow tab in code blocks
|
||||
e.preventDefault();
|
||||
document.execCommand('insertText', false, ' ');
|
||||
return;
|
||||
} else if (block.type === 'list') {
|
||||
// Future: implement list indentation
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
// Handle code blocks specially
|
||||
if (block.type === 'code') {
|
||||
if (e.shiftKey) {
|
||||
// Shift+Enter in code blocks creates a new block
|
||||
e.preventDefault();
|
||||
const newBlock = this.createNewBlock();
|
||||
this.insertBlockAfter(block, newBlock);
|
||||
}
|
||||
// For normal Enter in code blocks, let the browser handle it (creates new line)
|
||||
return;
|
||||
}
|
||||
|
||||
// For other block types, handle Enter normally (without shift)
|
||||
if (!e.shiftKey) {
|
||||
if (block.type === 'list') {
|
||||
// Handle Enter in lists differently
|
||||
const target = e.target as HTMLDivElement;
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
const currentLi = range.startContainer.parentElement?.closest('li');
|
||||
|
||||
if (currentLi && currentLi.textContent === '') {
|
||||
// Empty list item - exit list mode
|
||||
e.preventDefault();
|
||||
const newBlock = this.createNewBlock();
|
||||
this.insertBlockAfter(block, newBlock);
|
||||
}
|
||||
// Otherwise, let the browser handle creating new list items
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const newBlock = this.createNewBlock();
|
||||
this.insertBlockAfter(block, newBlock);
|
||||
}
|
||||
} else if (e.key === 'Backspace' && block.content === '' && this.blocks.length > 1) {
|
||||
e.preventDefault();
|
||||
const blockIndex = this.blocks.findIndex(b => b.id === block.id);
|
||||
if (blockIndex > 0) {
|
||||
const prevBlock = this.blocks[blockIndex - 1];
|
||||
this.blocks = this.blocks.filter(b => b.id !== block.id);
|
||||
|
||||
this.updateValue();
|
||||
|
||||
setTimeout(() => {
|
||||
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${prevBlock.id}"]`);
|
||||
if (wrapperElement && prevBlock.type !== 'divider') {
|
||||
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
|
||||
if (blockElement) {
|
||||
blockElement.focus();
|
||||
WysiwygBlocks.setCursorToEnd(blockElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleSlashMenuKeyboard(e: KeyboardEvent) {
|
||||
const menuItems = this.getFilteredMenuItems();
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.slashMenuSelectedIndex = (this.slashMenuSelectedIndex + 1) % menuItems.length;
|
||||
this.slashMenu.navigate('down');
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.slashMenuSelectedIndex = this.slashMenuSelectedIndex === 0
|
||||
? menuItems.length - 1
|
||||
: this.slashMenuSelectedIndex - 1;
|
||||
this.slashMenu.navigate('up');
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (menuItems[this.slashMenuSelectedIndex]) {
|
||||
this.insertBlock(menuItems[this.slashMenuSelectedIndex].type as IBlock['type']);
|
||||
}
|
||||
this.slashMenu.selectCurrent();
|
||||
break;
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
this.closeSlashMenu();
|
||||
this.closeSlashMenu(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public closeSlashMenu() {
|
||||
if (this.showSlashMenu && this.selectedBlockId) {
|
||||
public closeSlashMenu(clearSlash: boolean = false) {
|
||||
if (clearSlash && this.selectedBlockId) {
|
||||
// Clear the slash command from the content if menu is closing without selection
|
||||
const currentBlock = this.blocks.find(b => b.id === this.selectedBlockId);
|
||||
if (currentBlock) {
|
||||
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${currentBlock.id}"]`);
|
||||
if (wrapperElement) {
|
||||
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
|
||||
if (blockElement && (blockElement.textContent || '').startsWith('/')) {
|
||||
// Clear the slash command text
|
||||
blockElement.textContent = '';
|
||||
currentBlock.content = '';
|
||||
// Ensure cursor stays in the block
|
||||
blockElement.focus();
|
||||
const blockComponent = wrapperElement?.querySelector('dees-wysiwyg-block') as any;
|
||||
|
||||
if (blockComponent) {
|
||||
const content = blockComponent.getContent();
|
||||
if (content.startsWith('/')) {
|
||||
// Remove the entire slash command (slash + any filter text)
|
||||
const cleanContent = content.replace(/^\/[^\s]*\s*/, '').trim();
|
||||
blockComponent.setContent(cleanContent);
|
||||
currentBlock.content = cleanContent;
|
||||
|
||||
// Focus and set cursor at beginning
|
||||
requestAnimationFrame(() => {
|
||||
blockComponent.focusWithCursor(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.showSlashMenu = false;
|
||||
this.slashMenuFilter = '';
|
||||
this.slashMenuSelectedIndex = 0;
|
||||
this.slashMenu.hide();
|
||||
}
|
||||
|
||||
private detectBlockTypeIntent(content: string): { type: IBlock['type'], listType?: 'bullet' | 'ordered' } | null {
|
||||
@@ -552,33 +298,31 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
}
|
||||
|
||||
private handleBlockBlur(block: IBlock) {
|
||||
// Don't update value if slash menu is visible
|
||||
if (this.slashMenu.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update value on blur to ensure it's saved
|
||||
this.updateValue();
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.selectedBlockId === block.id) {
|
||||
this.selectedBlockId = null;
|
||||
}
|
||||
// Don't close slash menu on blur if clicking on menu item
|
||||
const activeElement = document.activeElement;
|
||||
const slashMenu = this.shadowRoot?.querySelector('.slash-menu');
|
||||
if (!slashMenu?.contains(activeElement as Node)) {
|
||||
this.closeSlashMenu();
|
||||
}
|
||||
}, 200);
|
||||
// Don't immediately clear selectedBlockId or close menus
|
||||
// Let click handlers decide what to do
|
||||
}
|
||||
|
||||
private handleEditorClick(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Close slash menu if clicking outside of it
|
||||
if (this.slashMenu.visible) {
|
||||
this.closeSlashMenu(true);
|
||||
}
|
||||
|
||||
// Focus last block if clicking on empty editor area
|
||||
if (target.classList.contains('editor-content')) {
|
||||
const lastBlock = this.blocks[this.blocks.length - 1];
|
||||
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${lastBlock.id}"]`);
|
||||
if (wrapperElement && lastBlock.type !== 'divider') {
|
||||
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
|
||||
if (blockElement) {
|
||||
blockElement.focus();
|
||||
WysiwygBlocks.setCursorToEnd(blockElement);
|
||||
}
|
||||
if (lastBlock.type !== 'divider') {
|
||||
this.blockOperations.focusBlock(lastBlock.id, 'end');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -592,79 +336,91 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
};
|
||||
}
|
||||
|
||||
private insertBlockAfter(afterBlock: IBlock, newBlock: IBlock, focusNewBlock: boolean = true): void {
|
||||
private async insertBlockAfter(afterBlock: IBlock, newBlock: IBlock, focusNewBlock: boolean = true): Promise<void> {
|
||||
const blockIndex = this.blocks.findIndex(b => b.id === afterBlock.id);
|
||||
this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)];
|
||||
|
||||
this.updateValue();
|
||||
this.requestUpdate();
|
||||
|
||||
if (focusNewBlock) {
|
||||
setTimeout(() => {
|
||||
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${newBlock.id}"]`);
|
||||
if (wrapperElement && newBlock.type !== 'divider') {
|
||||
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
|
||||
if (blockElement) {
|
||||
blockElement.focus();
|
||||
WysiwygBlocks.setCursorToStart(blockElement);
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
if (focusNewBlock && newBlock.type !== 'divider') {
|
||||
await this.blockOperations.focusBlock(newBlock.id, 'start');
|
||||
}
|
||||
}
|
||||
|
||||
public async insertBlock(type: IBlock['type']) {
|
||||
const currentBlockIndex = this.blocks.findIndex(b => b.id === this.selectedBlockId);
|
||||
const currentBlock = this.blocks[currentBlockIndex];
|
||||
const currentBlock = this.blocks.find(b => b.id === this.selectedBlockId);
|
||||
|
||||
if (currentBlock) {
|
||||
// If it's a code block, ask for language
|
||||
if (type === 'code') {
|
||||
const language = await WysiwygModalManager.showLanguageSelectionModal();
|
||||
if (!language) {
|
||||
// User cancelled
|
||||
this.closeSlashMenu();
|
||||
return;
|
||||
}
|
||||
currentBlock.metadata = { language };
|
||||
}
|
||||
|
||||
currentBlock.type = type;
|
||||
currentBlock.content = '';
|
||||
|
||||
if (type === 'divider') {
|
||||
currentBlock.content = ' ';
|
||||
const newBlock = this.createNewBlock();
|
||||
this.insertBlockAfter(currentBlock, newBlock);
|
||||
} else if (type === 'list') {
|
||||
// Handle list type specially
|
||||
currentBlock.metadata = { listType: 'bullet' }; // Default to bullet list
|
||||
setTimeout(() => {
|
||||
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${currentBlock.id}"]`);
|
||||
if (wrapperElement) {
|
||||
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
|
||||
if (blockElement) {
|
||||
blockElement.innerHTML = '<ul><li></li></ul>';
|
||||
WysiwygBlocks.focusListItem(blockElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Force update the contenteditable element
|
||||
setTimeout(() => {
|
||||
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${currentBlock.id}"]`);
|
||||
if (wrapperElement) {
|
||||
const blockElement = wrapperElement.querySelector('.block') as HTMLDivElement;
|
||||
if (blockElement) {
|
||||
blockElement.textContent = '';
|
||||
blockElement.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!currentBlock) {
|
||||
this.closeSlashMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the block component to extract clean content
|
||||
const wrapperElement = this.shadowRoot!.querySelector(`[data-block-id="${currentBlock.id}"]`);
|
||||
const blockComponent = wrapperElement?.querySelector('dees-wysiwyg-block') as any;
|
||||
|
||||
// Clear the slash command from the content before transforming
|
||||
if (blockComponent) {
|
||||
const content = blockComponent.getContent();
|
||||
if (content.startsWith('/')) {
|
||||
// Remove the slash and any filter text (including non-word characters)
|
||||
const cleanContent = content.replace(/^\/[^\s]*\s*/, '').trim();
|
||||
blockComponent.setContent(cleanContent);
|
||||
currentBlock.content = cleanContent;
|
||||
}
|
||||
}
|
||||
|
||||
this.closeSlashMenu();
|
||||
// Close menu
|
||||
this.closeSlashMenu(false);
|
||||
|
||||
// If it's a code block, ask for language
|
||||
if (type === 'code') {
|
||||
const language = await WysiwygModalManager.showLanguageSelectionModal();
|
||||
if (!language) {
|
||||
return; // User cancelled
|
||||
}
|
||||
currentBlock.metadata = { language };
|
||||
}
|
||||
|
||||
// Transform the current block
|
||||
currentBlock.type = type;
|
||||
currentBlock.content = currentBlock.content || '';
|
||||
|
||||
if (type === 'divider') {
|
||||
currentBlock.content = ' ';
|
||||
const newBlock = this.createNewBlock();
|
||||
this.insertBlockAfter(currentBlock, newBlock);
|
||||
} else if (type === 'list') {
|
||||
currentBlock.metadata = { listType: 'bullet' };
|
||||
// For lists, ensure we start with empty content
|
||||
currentBlock.content = '';
|
||||
} else {
|
||||
// For all other block types, ensure content is clean
|
||||
currentBlock.content = currentBlock.content || '';
|
||||
}
|
||||
|
||||
// Update and refocus
|
||||
this.updateValue();
|
||||
this.requestUpdate();
|
||||
|
||||
// Wait for update to complete before focusing
|
||||
await this.updateComplete;
|
||||
|
||||
// Focus the block after rendering
|
||||
if (type === 'list') {
|
||||
this.blockOperations.focusBlock(currentBlock.id, 'start');
|
||||
// Additional list-specific focus handling
|
||||
requestAnimationFrame(() => {
|
||||
const blockWrapper = this.shadowRoot?.querySelector(`[data-block-id="${currentBlock.id}"]`);
|
||||
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as any;
|
||||
if (blockComponent) {
|
||||
blockComponent.focusListItem();
|
||||
}
|
||||
});
|
||||
} else if (type !== 'divider') {
|
||||
this.blockOperations.focusBlock(currentBlock.id, 'start');
|
||||
}
|
||||
}
|
||||
|
||||
private updateValue() {
|
||||
@@ -833,35 +589,10 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
|
||||
|
||||
private handleTextSelection(e: MouseEvent): void {
|
||||
// Stop event to prevent it from bubbling up
|
||||
e.stopPropagation();
|
||||
// Don't interfere with slash menu
|
||||
if (this.slashMenu.visible) return;
|
||||
|
||||
console.log('handleTextSelection called from mouseup on contenteditable');
|
||||
|
||||
// Small delay to ensure selection is complete
|
||||
setTimeout(() => {
|
||||
// Alternative approach: check selection directly within the target element
|
||||
const target = e.target as HTMLElement;
|
||||
const selection = window.getSelection();
|
||||
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const selectedText = selection.toString();
|
||||
console.log('Direct selection check in handleTextSelection:', {
|
||||
selectedText: selectedText.substring(0, 50),
|
||||
hasText: selectedText.length > 0,
|
||||
target: target.tagName + '.' + target.className
|
||||
});
|
||||
|
||||
if (selectedText.length > 0) {
|
||||
// We know this came from a mouseup on our contenteditable, so it's definitely our selection
|
||||
console.log('✅ Text selected via mouseup:', selectedText);
|
||||
this.selectedText = selectedText;
|
||||
this.updateFormattingMenuPosition();
|
||||
} else if (this.showFormattingMenu) {
|
||||
this.hideFormattingMenu();
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
// Let the block component handle selection via custom event
|
||||
}
|
||||
|
||||
private handleSelectionChange(): void {
|
||||
@@ -902,7 +633,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
this.selectedText = selectedText;
|
||||
this.updateFormattingMenuPosition();
|
||||
}
|
||||
} else if (this.showFormattingMenu) {
|
||||
} else if (this.formattingMenu.visible) {
|
||||
console.log('No text selected, hiding menu');
|
||||
this.hideFormattingMenu();
|
||||
}
|
||||
@@ -922,42 +653,33 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
}
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
this.formattingMenuPosition = {
|
||||
const formattingMenuPosition = {
|
||||
x: coords.x - containerRect.left,
|
||||
y: coords.y - containerRect.top
|
||||
};
|
||||
|
||||
console.log('Setting menu position:', this.formattingMenuPosition);
|
||||
this.showFormattingMenu = true;
|
||||
console.log('showFormattingMenu set to:', this.showFormattingMenu);
|
||||
|
||||
// Force update
|
||||
this.requestUpdate();
|
||||
|
||||
// Check if menu exists in DOM after update
|
||||
setTimeout(() => {
|
||||
const menu = this.shadowRoot?.querySelector('.formatting-menu');
|
||||
console.log('Menu in DOM after update:', menu);
|
||||
if (menu) {
|
||||
console.log('Menu style:', menu.getAttribute('style'));
|
||||
}
|
||||
}, 100);
|
||||
console.log('Setting menu position:', formattingMenuPosition);
|
||||
// Show the global formatting menu
|
||||
this.formattingMenu.show(
|
||||
{ x: coords.x, y: coords.y }, // Use absolute coordinates
|
||||
async (command: string) => await this.applyFormat(command)
|
||||
);
|
||||
} else {
|
||||
console.log('No coordinates found');
|
||||
}
|
||||
}
|
||||
|
||||
private hideFormattingMenu(): void {
|
||||
this.showFormattingMenu = false;
|
||||
this.formattingMenu.hide();
|
||||
this.selectedText = '';
|
||||
}
|
||||
|
||||
public applyFormat(command: string): void {
|
||||
public async applyFormat(command: string): Promise<void> {
|
||||
// Save current selection before applying format
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
// Get the current block to update its content
|
||||
// Get the current block
|
||||
const anchorNode = selection.anchorNode;
|
||||
const blockElement = anchorNode?.nodeType === Node.TEXT_NODE
|
||||
? anchorNode.parentElement?.closest('.block')
|
||||
@@ -965,31 +687,120 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
|
||||
if (!blockElement) return;
|
||||
|
||||
const blockId = blockElement.closest('.block-wrapper')?.getAttribute('data-block-id');
|
||||
const blockWrapper = blockElement.closest('.block-wrapper');
|
||||
const blockId = blockWrapper?.getAttribute('data-block-id');
|
||||
const block = this.blocks.find(b => b.id === blockId);
|
||||
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as any;
|
||||
|
||||
if (!block) return;
|
||||
if (!block || !blockComponent) return;
|
||||
|
||||
// Apply the format
|
||||
WysiwygFormatting.applyFormat(command);
|
||||
// Handle link command specially
|
||||
if (command === 'link') {
|
||||
const url = await this.showLinkDialog();
|
||||
if (!url) {
|
||||
// User cancelled - restore focus to block
|
||||
blockComponent.focus();
|
||||
return;
|
||||
}
|
||||
WysiwygFormatting.applyFormat(command, url);
|
||||
} else {
|
||||
// Apply the format
|
||||
WysiwygFormatting.applyFormat(command);
|
||||
}
|
||||
|
||||
// Update block content after format is applied
|
||||
setTimeout(() => {
|
||||
if (block.type === 'list') {
|
||||
const listItems = blockElement.querySelectorAll('li');
|
||||
block.content = Array.from(listItems).map(li => li.textContent || '').join('\n');
|
||||
} else {
|
||||
// For other blocks, preserve HTML formatting
|
||||
block.content = blockElement.innerHTML;
|
||||
}
|
||||
// Update content after a microtask to ensure DOM is updated
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
// Force content update
|
||||
block.content = blockComponent.getContent();
|
||||
|
||||
// Update value to persist changes
|
||||
this.updateValue();
|
||||
|
||||
// For link command, close the formatting menu
|
||||
if (command === 'link') {
|
||||
this.hideFormattingMenu();
|
||||
} else if (this.formattingMenu.visible) {
|
||||
// Update menu position if still showing
|
||||
this.updateFormattingMenuPosition();
|
||||
}
|
||||
|
||||
// Ensure block still has focus
|
||||
if (document.activeElement !== blockElement) {
|
||||
blockComponent.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private async showLinkDialog(): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
let linkUrl: string | null = null;
|
||||
|
||||
this.updateValue();
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Add Link',
|
||||
content: html`
|
||||
<style>
|
||||
.link-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
border: 1px solid var(--dees-color-line-bright);
|
||||
border-radius: 4px;
|
||||
background: var(--dees-color-input);
|
||||
color: var(--dees-color-text);
|
||||
margin: 16px 0;
|
||||
}
|
||||
.link-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--dees-color-primary);
|
||||
}
|
||||
</style>
|
||||
<input
|
||||
class="link-input"
|
||||
type="url"
|
||||
placeholder="https://example.com"
|
||||
@keydown="${(e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
const input = e.target as HTMLInputElement;
|
||||
linkUrl = input.value;
|
||||
// Find and click the OK button
|
||||
const modal = input.closest('dees-modal');
|
||||
if (modal) {
|
||||
const okButton = modal.shadowRoot?.querySelector('.bottomButton:last-child') as HTMLElement;
|
||||
if (okButton) okButton.click();
|
||||
}
|
||||
}
|
||||
}}"
|
||||
@input="${(e: InputEvent) => {
|
||||
linkUrl = (e.target as HTMLInputElement).value;
|
||||
}}"
|
||||
/>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modal) => {
|
||||
modal.destroy();
|
||||
resolve(null);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Add Link',
|
||||
action: async (modal) => {
|
||||
modal.destroy();
|
||||
resolve(linkUrl);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Keep selection active
|
||||
if (command !== 'link') {
|
||||
this.updateFormattingMenuPosition();
|
||||
}
|
||||
}, 10);
|
||||
// Focus the input after modal is shown
|
||||
setTimeout(() => {
|
||||
const input = document.querySelector('dees-modal .link-input') as HTMLInputElement;
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
private async showLanguageSelectionModal(): Promise<string | null> {
|
||||
|
Reference in New Issue
Block a user