import { DeesInputBase } from '../dees-input-base.js'; import { demoFunc } from '../dees-input-wysiwyg.demo.js'; import { DeesModal } from '../dees-modal.js'; import { customElement, type TemplateResult, property, html, cssManager, state, } from '@design.estate/dees-element'; import { type IBlock, type OutputFormat, wysiwygStyles, WysiwygConverters, WysiwygShortcuts, WysiwygBlocks, type ISlashMenuItem, WysiwygFormatting } from './index.js'; declare global { interface HTMLElementTagNameMap { 'dees-input-wysiwyg': DeesInputWysiwyg; } } @customElement('dees-input-wysiwyg') export class DeesInputWysiwyg extends DeesInputBase { public static demo = demoFunc; @property({ type: String }) public value: string = ''; @property({ type: String }) public outputFormat: OutputFormat = 'html'; @state() private blocks: IBlock[] = [ { id: WysiwygShortcuts.generateBlockId(), type: 'paragraph', content: '', } ]; @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; @state() private draggedBlockId: string | null = null; @state() private dragOverBlockId: string | null = null; @state() private dragOverPosition: 'before' | 'after' | null = null; @state() private showFormattingMenu: boolean = false; @state() private formattingMenuPosition: { x: number; y: number } = { x: 0, y: 0 }; @state() private selectedText: string = ''; private editorContentRef: HTMLDivElement; private isComposing: boolean = false; private saveTimeout: any = null; private selectionChangeHandler = () => this.handleSelectionChange(); public static styles = [ ...DeesInputBase.baseStyles, cssManager.defaultStyles, wysiwygStyles ]; async connectedCallback() { await super.connectedCallback(); } async disconnectedCallback() { await super.disconnectedCallback(); // Remove selection listener document.removeEventListener('selectionchange', this.selectionChangeHandler); } async firstUpdated() { this.updateValue(); this.editorContentRef = this.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; // Add global selection listener console.log('Adding selectionchange listener'); document.addEventListener('selectionchange', this.selectionChangeHandler); } render(): TemplateResult { return html`
${this.blocks.map(block => this.renderBlock(block))}
${this.showSlashMenu ? this.renderSlashMenu() : ''} ${this.showFormattingMenu ? this.renderFormattingMenu() : ''}
`; } private renderBlock(block: IBlock): TemplateResult { const isSelected = this.selectedBlockId === block.id; const isDragging = this.draggedBlockId === block.id; const isDragOver = this.dragOverBlockId === block.id; return html`
${block.type !== 'divider' ? html`
` : ''} ${WysiwygBlocks.renderBlock(block, isSelected, { onInput: (e: InputEvent) => this.handleBlockInput(e, block), onKeyDown: (e: KeyboardEvent) => this.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), })} ${block.type !== 'divider' ? html`
` : ''}
`; } private 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`
${menuItems.map((item, index) => html`
${item.icon} ${item.label}
`)}
`; } 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}>
  • `; // 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 this.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 if (this.saveTimeout) { clearTimeout(this.saveTimeout); } this.saveTimeout = setTimeout(() => { this.updateValue(); }, 1000); // Save after 1 second of inactivity } 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; break; case 'ArrowUp': e.preventDefault(); this.slashMenuSelectedIndex = this.slashMenuSelectedIndex === 0 ? menuItems.length - 1 : this.slashMenuSelectedIndex - 1; break; case 'Enter': e.preventDefault(); if (menuItems[this.slashMenuSelectedIndex]) { this.insertBlock(menuItems[this.slashMenuSelectedIndex].type as IBlock['type']); } break; case 'Escape': e.preventDefault(); this.closeSlashMenu(); break; } } private closeSlashMenu() { if (this.showSlashMenu && 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(); } } } } this.showSlashMenu = false; this.slashMenuFilter = ''; this.slashMenuSelectedIndex = 0; } private detectBlockTypeIntent(content: string): { type: IBlock['type'], listType?: 'bullet' | 'ordered' } | null { // Check heading patterns const headingResult = WysiwygShortcuts.checkHeadingShortcut(content); if (headingResult) { return headingResult; } // Check list patterns const listResult = WysiwygShortcuts.checkListShortcut(content); if (listResult) { return listResult; } // Check quote pattern if (WysiwygShortcuts.checkQuoteShortcut(content)) { return { type: 'quote' }; } // Check code pattern if (WysiwygShortcuts.checkCodeShortcut(content)) { return { type: 'code' }; } // Check divider pattern if (WysiwygShortcuts.checkDividerShortcut(content)) { return { type: 'divider' }; } // Don't automatically revert to paragraph - blocks should keep their type // unless explicitly changed by the user return null; } private handleBlockFocus(block: IBlock) { if (block.type !== 'divider') { this.selectedBlockId = block.id; } } private handleBlockBlur(block: IBlock) { // 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); } private handleEditorClick(e: MouseEvent) { const target = e.target as HTMLElement; 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); } } } } private createNewBlock(type: IBlock['type'] = 'paragraph', content: string = '', metadata?: any): IBlock { return { id: WysiwygShortcuts.generateBlockId(), type, content, ...(metadata && { metadata }) }; } private insertBlockAfter(afterBlock: IBlock, newBlock: IBlock, focusNewBlock: boolean = true): 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(); 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); } } private async insertBlock(type: IBlock['type']) { const currentBlockIndex = this.blocks.findIndex(b => b.id === this.selectedBlockId); const currentBlock = this.blocks[currentBlockIndex]; if (currentBlock) { // If it's a code block, ask for language if (type === 'code') { const language = await this.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 = ''; 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(); } } }); } } this.closeSlashMenu(); this.updateValue(); } private updateValue() { if (this.outputFormat === 'html') { this.value = WysiwygConverters.getHtmlOutput(this.blocks); } else { this.value = WysiwygConverters.getMarkdownOutput(this.blocks); } this.changeSubject.next(this.value); } public getValue(): string { return this.value; } public setValue(value: string): void { this.value = value; if (this.outputFormat === 'html') { this.blocks = WysiwygConverters.parseHtmlToBlocks(value); } else { this.blocks = WysiwygConverters.parseMarkdownToBlocks(value); } if (this.blocks.length === 0) { this.blocks = [{ id: WysiwygShortcuts.generateBlockId(), type: 'paragraph', content: '', }]; } this.changeSubject.next(this.value); this.requestUpdate(); } /** * Export the editor content as raw blocks (lossless) */ public exportBlocks(): IBlock[] { return JSON.parse(JSON.stringify(this.blocks)); } /** * Import raw blocks (lossless) */ public importBlocks(blocks: IBlock[]): void { this.blocks = JSON.parse(JSON.stringify(blocks)); this.updateValue(); this.requestUpdate(); } /** * Export content as HTML regardless of outputFormat setting */ public exportAsHtml(): string { return WysiwygConverters.getHtmlOutput(this.blocks); } /** * Export content as Markdown regardless of outputFormat setting */ public exportAsMarkdown(): string { return WysiwygConverters.getMarkdownOutput(this.blocks); } /** * Get a JSON representation of the editor state (for saving) */ public exportState(): { blocks: IBlock[], outputFormat: OutputFormat } { return { blocks: this.exportBlocks(), outputFormat: this.outputFormat }; } /** * Restore editor state from JSON */ public importState(state: { blocks: IBlock[], outputFormat?: OutputFormat }): void { if (state.outputFormat) { this.outputFormat = state.outputFormat; } this.importBlocks(state.blocks); } // Drag and Drop Handlers private handleDragStart(e: DragEvent, block: IBlock): void { if (!e.dataTransfer) return; this.draggedBlockId = block.id; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', block.id); // Add a slight delay to show the dragging state setTimeout(() => { this.requestUpdate(); }, 10); } private handleDragEnd(): void { this.draggedBlockId = null; this.dragOverBlockId = null; this.dragOverPosition = null; this.requestUpdate(); } private handleDragOver(e: DragEvent, block: IBlock): void { e.preventDefault(); if (!e.dataTransfer || !this.draggedBlockId || this.draggedBlockId === block.id) return; e.dataTransfer.dropEffect = 'move'; const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); const midpoint = rect.top + rect.height / 2; this.dragOverBlockId = block.id; this.dragOverPosition = e.clientY < midpoint ? 'before' : 'after'; this.requestUpdate(); } private handleDragLeave(block: IBlock): void { if (this.dragOverBlockId === block.id) { this.dragOverBlockId = null; this.dragOverPosition = null; this.requestUpdate(); } } private handleDrop(e: DragEvent, targetBlock: IBlock): void { e.preventDefault(); if (!this.draggedBlockId || this.draggedBlockId === targetBlock.id) return; const draggedIndex = this.blocks.findIndex(b => b.id === this.draggedBlockId); const targetIndex = this.blocks.findIndex(b => b.id === targetBlock.id); if (draggedIndex === -1 || targetIndex === -1) return; // Remove the dragged block const [draggedBlock] = this.blocks.splice(draggedIndex, 1); // Calculate the new index let newIndex = targetIndex; if (this.dragOverPosition === 'after') { newIndex = draggedIndex < targetIndex ? targetIndex : targetIndex + 1; } else { newIndex = draggedIndex < targetIndex ? targetIndex - 1 : targetIndex; } // Insert at new position this.blocks.splice(newIndex, 0, draggedBlock); // Update state this.updateValue(); this.handleDragEnd(); // Focus the moved block setTimeout(() => { const movedBlockElement = this.shadowRoot!.querySelector(`[data-block-id="${draggedBlock.id}"] .block`) as HTMLDivElement; if (movedBlockElement && draggedBlock.type !== 'divider') { movedBlockElement.focus(); } }, 100); } private handleTextSelection(e: MouseEvent): void { // Stop event to prevent it from bubbling up e.stopPropagation(); 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); } private handleSelectionChange(): void { // Try to get selection from shadow root first, then fall back to window const shadowSelection = (this.shadowRoot as any).getSelection ? (this.shadowRoot as any).getSelection() : null; const windowSelection = window.getSelection(); const editorContent = this.shadowRoot?.querySelector('.editor-content') as HTMLElement; // Check both shadow and window selections let selection = shadowSelection; let selectedText = shadowSelection?.toString() || ''; // If no shadow selection, check window selection if (!selectedText && windowSelection) { selection = windowSelection; selectedText = windowSelection.toString() || ''; } console.log('Selection change:', { hasText: selectedText.length > 0, selectedText: selectedText.substring(0, 50), shadowSelection: !!shadowSelection, windowSelection: !!windowSelection, rangeCount: selection?.rangeCount, editorContent: !!editorContent }); if (!selection || selection.rangeCount === 0 || !editorContent) { console.log('No selection or editor content'); return; } // If we have selected text, show the formatting menu if (selectedText.length > 0) { console.log('✅ Text selected:', selectedText); if (selectedText !== this.selectedText) { this.selectedText = selectedText; this.updateFormattingMenuPosition(); } } else if (this.showFormattingMenu) { console.log('No text selected, hiding menu'); this.hideFormattingMenu(); } } private updateFormattingMenuPosition(): void { console.log('updateFormattingMenuPosition called'); const coords = WysiwygFormatting.getSelectionCoordinates(this.shadowRoot as ShadowRoot); console.log('Selection coordinates:', coords); if (coords) { const container = this.shadowRoot!.querySelector('.wysiwyg-container'); if (!container) { console.error('Container not found!'); return; } const containerRect = container.getBoundingClientRect(); this.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); } else { console.log('No coordinates found'); } } private hideFormattingMenu(): void { this.showFormattingMenu = false; this.selectedText = ''; } private applyFormat(command: string): 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 const anchorNode = selection.anchorNode; const blockElement = anchorNode?.nodeType === Node.TEXT_NODE ? anchorNode.parentElement?.closest('.block') : (anchorNode as Element)?.closest('.block'); if (!blockElement) return; const blockId = blockElement.closest('.block-wrapper')?.getAttribute('data-block-id'); const block = this.blocks.find(b => b.id === blockId); if (!block) return; // 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; } this.updateValue(); // Keep selection active if (command !== 'link') { this.updateFormattingMenuPosition(); } }, 10); } private async showLanguageSelectionModal(): Promise { return new Promise((resolve) => { let selectedLanguage: string | null = null; DeesModal.createAndShow({ heading: 'Select Programming Language', content: html`
    ${['JavaScript', 'TypeScript', 'Python', 'Java', 'C++', 'C#', 'Go', 'Rust', 'HTML', 'CSS', 'SQL', 'Shell', 'JSON', 'YAML', 'Markdown', 'Plain Text'].map(lang => html`
    ${lang}
    `)}
    `, menuOptions: [ { name: 'Cancel', action: async (modal) => { modal.destroy(); resolve(null); } }, { name: 'OK', action: async (modal) => { modal.destroy(); resolve(selectedLanguage); } } ] }); }); } private async showBlockSettingsModal(block: IBlock): Promise { let content: TemplateResult; if (block.type === 'code') { const currentLanguage = block.metadata?.language || 'plain text'; content = html`
    Programming Language
    ${['JavaScript', 'TypeScript', 'Python', 'Java', 'C++', 'C#', 'Go', 'Rust', 'HTML', 'CSS', 'SQL', 'Shell', 'JSON', 'YAML', 'Markdown', 'Plain Text'].map(lang => html`
    ${lang}
    `)}
    `; } else { content = html`
    No settings available for this block type.
    `; } DeesModal.createAndShow({ heading: 'Block Settings', content, menuOptions: [ { name: 'Close', action: async (modal) => { modal.destroy(); } } ] }); } }