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, WysiwygBlockOperations, WysiwygInputHandler, WysiwygKeyboardHandler, WysiwygDragDropHandler, WysiwygModalManager, DeesSlashMenu, DeesFormattingMenu } 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: '', } ]; // Not using @state to avoid re-renders when selection changes private selectedBlockId: string | null = null; // Slash menu is now globally rendered private slashMenu = DeesSlashMenu.getInstance(); @state() private draggedBlockId: string | null = null; @state() private dragOverBlockId: string | null = null; @state() private dragOverPosition: 'before' | 'after' | null = null; // Formatting menu is now globally rendered private formattingMenu = DeesFormattingMenu.getInstance(); @state() private selectedText: string = ''; private editorContentRef: HTMLDivElement; private isComposing: boolean = false; private selectionChangeHandler = () => this.handleSelectionChange(); // Handler instances private blockOperations: WysiwygBlockOperations; private inputHandler: WysiwygInputHandler; private keyboardHandler: WysiwygKeyboardHandler; private dragDropHandler: WysiwygDragDropHandler; // Content cache to avoid triggering re-renders during typing private contentCache: Map = new Map(); public static styles = [ ...DeesInputBase.baseStyles, cssManager.defaultStyles, wysiwygStyles ]; constructor() { super(); // Initialize handlers this.blockOperations = new WysiwygBlockOperations(this); this.inputHandler = new WysiwygInputHandler(this); this.keyboardHandler = new WysiwygKeyboardHandler(this); this.dragDropHandler = new WysiwygDragDropHandler(this); } async connectedCallback() { await super.connectedCallback(); } async disconnectedCallback() { await super.disconnectedCallback(); // Remove selection listener document.removeEventListener('selectionchange', this.selectionChangeHandler); // Clean up handlers this.inputHandler?.destroy(); // Clean up blur timeout if (this.blurTimeout) { clearTimeout(this.blurTimeout); this.blurTimeout = null; } } 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); // 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 blocks programmatically this.renderBlocksProgrammatically(); } /** * Renders all blocks programmatically without triggering re-renders */ private renderBlocksProgrammatically() { if (!this.editorContentRef) return; // Clear existing blocks this.editorContentRef.innerHTML = ''; // Create and append block elements this.blocks.forEach(block => { const blockWrapper = this.createBlockElement(block); this.editorContentRef.appendChild(blockWrapper); }); } /** * Creates a block element programmatically */ private createBlockElement(block: IBlock): HTMLElement { const wrapper = document.createElement('div'); wrapper.className = 'block-wrapper'; wrapper.setAttribute('data-block-id', block.id); // Add drag handle for non-divider blocks if (block.type !== 'divider') { const dragHandle = document.createElement('div'); dragHandle.className = 'drag-handle'; dragHandle.draggable = true; dragHandle.addEventListener('dragstart', (e) => this.dragDropHandler.handleDragStart(e, block)); dragHandle.addEventListener('dragend', () => this.dragDropHandler.handleDragEnd()); wrapper.appendChild(dragHandle); } // Create the block component const blockComponent = document.createElement('dees-wysiwyg-block') as any; blockComponent.block = block; blockComponent.isSelected = this.selectedBlockId === block.id; blockComponent.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), }; wrapper.appendChild(blockComponent); // Add settings button for non-divider blocks if (block.type !== 'divider') { const settings = document.createElement('div'); settings.className = 'block-settings'; settings.innerHTML = ` `; settings.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); WysiwygModalManager.showBlockSettingsModal(block, (updatedBlock) => { this.updateValue(); // Re-render only the updated block this.updateBlockElement(block.id); }); }); wrapper.appendChild(settings); } // Add drag event listeners wrapper.addEventListener('dragover', (e) => this.dragDropHandler.handleDragOver(e, block)); wrapper.addEventListener('drop', (e) => this.dragDropHandler.handleDrop(e, block)); wrapper.addEventListener('dragleave', () => this.dragDropHandler.handleDragLeave(block)); return wrapper; } /** * Updates a specific block element */ private updateBlockElement(blockId: string) { const block = this.blocks.find(b => b.id === blockId); if (!block) return; const wrapper = this.editorContentRef.querySelector(`[data-block-id="${blockId}"]`); if (!wrapper) return; // Replace with new element const newWrapper = this.createBlockElement(block); wrapper.replaceWith(newWrapper); } render(): TemplateResult { return html`
`; } // Old renderBlock method removed - using programmatic rendering instead private handleSlashMenuKeyboard(e: KeyboardEvent) { switch(e.key) { case 'ArrowDown': e.preventDefault(); this.slashMenu.navigate('down'); break; case 'ArrowUp': e.preventDefault(); this.slashMenu.navigate('up'); break; case 'Enter': e.preventDefault(); this.slashMenu.selectCurrent(); break; case 'Escape': e.preventDefault(); this.closeSlashMenu(true); break; } } 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}"]`); 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.slashMenu.hide(); } 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) { // Clear any pending blur timeout when focusing if (this.blurTimeout) { clearTimeout(this.blurTimeout); this.blurTimeout = null; } if (block.type !== 'divider') { const prevSelectedId = this.selectedBlockId; this.selectedBlockId = block.id; // Only update selection UI if it changed if (prevSelectedId !== block.id) { // Update the previous block's selection state if (prevSelectedId) { const prevWrapper = this.shadowRoot?.querySelector(`[data-block-id="${prevSelectedId}"]`); const prevBlockComponent = prevWrapper?.querySelector('dees-wysiwyg-block') as any; if (prevBlockComponent) { prevBlockComponent.isSelected = false; } } // Update the new block's selection state const wrapper = this.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockComponent = wrapper?.querySelector('dees-wysiwyg-block') as any; if (blockComponent) { blockComponent.isSelected = true; } } } } private blurTimeout: any = null; private handleBlockBlur(block: IBlock) { // Clear any existing blur timeout if (this.blurTimeout) { clearTimeout(this.blurTimeout); } // Don't update value if slash menu is visible if (this.slashMenu.visible) { return; } // Sync content from the block that's losing focus const wrapperElement = this.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockComponent = wrapperElement?.querySelector('dees-wysiwyg-block') as any; if (blockComponent && blockComponent.getContent) { const newContent = blockComponent.getContent(); // Only update if content actually changed if (block.content !== newContent) { block.content = newContent; } } // Delay the blur handling to avoid interfering with typing this.blurTimeout = setTimeout(() => { // Check if we've refocused on another block const activeElement = this.shadowRoot?.activeElement; const isBlockFocused = activeElement?.classList.contains('block'); if (!isBlockFocused) { // Only update value if we're truly blurring away from all blocks this.updateValue(); } }, 100); // 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]; if (lastBlock.type !== 'divider') { this.blockOperations.focusBlock(lastBlock.id, 'end'); } } } private createNewBlock(type: IBlock['type'] = 'paragraph', content: string = '', metadata?: any): IBlock { return { id: WysiwygShortcuts.generateBlockId(), type, content, ...(metadata && { metadata }) }; } private async insertBlockAfter(afterBlock: IBlock, newBlock: IBlock, focusNewBlock: boolean = true): Promise { const blockIndex = this.blocks.findIndex(b => b.id === afterBlock.id); this.blocks = [...this.blocks.slice(0, blockIndex + 1), newBlock, ...this.blocks.slice(blockIndex + 1)]; // Insert the new block element programmatically const afterWrapper = this.editorContentRef.querySelector(`[data-block-id="${afterBlock.id}"]`); if (afterWrapper) { const newWrapper = this.createBlockElement(newBlock); afterWrapper.insertAdjacentElement('afterend', newWrapper); } this.updateValue(); if (focusNewBlock && newBlock.type !== 'divider') { // Give DOM time to settle await new Promise(resolve => setTimeout(resolve, 0)); await this.blockOperations.focusBlock(newBlock.id, 'start'); } } public async insertBlock(type: IBlock['type']) { const currentBlock = this.blocks.find(b => b.id === this.selectedBlockId); 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; } } // 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 the block element programmatically this.updateBlockElement(currentBlock.id); this.updateValue(); // Give DOM time to settle await new Promise(resolve => setTimeout(resolve, 0)); // 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() { 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); // Re-render blocks programmatically if we have the editor if (this.editorContentRef) { this.renderBlocksProgrammatically(); } } /** * 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(); // Re-render blocks programmatically if we have the editor if (this.editorContentRef) { this.renderBlocksProgrammatically(); } } /** * 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 dragging class to the wrapper setTimeout(() => { const wrapper = this.editorContentRef.querySelector(`[data-block-id="${block.id}"]`); if (wrapper) { wrapper.classList.add('dragging'); } // Add dragging class to editor content this.editorContentRef.classList.add('dragging'); }, 10); } private handleDragEnd(): void { // Remove all drag-related classes if (this.draggedBlockId) { const wrapper = this.editorContentRef.querySelector(`[data-block-id="${this.draggedBlockId}"]`); if (wrapper) { wrapper.classList.remove('dragging'); } } // Remove all drag-over classes const allWrappers = this.editorContentRef.querySelectorAll('.block-wrapper'); allWrappers.forEach(wrapper => { wrapper.classList.remove('drag-over-before', 'drag-over-after'); }); // Remove dragging class from editor content this.editorContentRef.classList.remove('dragging'); this.draggedBlockId = null; this.dragOverBlockId = null; this.dragOverPosition = null; } 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; // Remove previous drag-over classes if (this.dragOverBlockId) { const prevWrapper = this.editorContentRef.querySelector(`[data-block-id="${this.dragOverBlockId}"]`); if (prevWrapper) { prevWrapper.classList.remove('drag-over-before', 'drag-over-after'); } } this.dragOverBlockId = block.id; this.dragOverPosition = e.clientY < midpoint ? 'before' : 'after'; // Add new drag-over class const wrapper = e.currentTarget as HTMLElement; wrapper.classList.add(`drag-over-${this.dragOverPosition}`); } private handleDragLeave(block: IBlock): void { if (this.dragOverBlockId === block.id) { const wrapper = this.editorContentRef.querySelector(`[data-block-id="${block.id}"]`); if (wrapper) { wrapper.classList.remove('drag-over-before', 'drag-over-after'); } this.dragOverBlockId = null; this.dragOverPosition = null; } } 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); // Re-render blocks programmatically to reflect the new order this.renderBlocksProgrammatically(); // 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 { // Don't interfere with slash menu if (this.slashMenu.visible) return; // Let the block component handle selection via custom event } 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.formattingMenu.visible) { 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(); const formattingMenuPosition = { x: coords.x - containerRect.left, y: coords.y - containerRect.top }; 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.formattingMenu.hide(); this.selectedText = ''; } public async applyFormat(command: string): Promise { // Save current selection before applying format const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return; // Get the current block const anchorNode = selection.anchorNode; const blockElement = anchorNode?.nodeType === Node.TEXT_NODE ? anchorNode.parentElement?.closest('.block') : (anchorNode as Element)?.closest('.block'); if (!blockElement) return; 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 || !blockComponent) return; // 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 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 { return new Promise((resolve) => { let linkUrl: string | null = null; DeesModal.createAndShow({ heading: 'Add Link', content: html` `, menuOptions: [ { name: 'Cancel', action: async (modal) => { modal.destroy(); resolve(null); } }, { name: 'Add Link', action: async (modal) => { modal.destroy(); resolve(linkUrl); } } ] }); // Focus the input after modal is shown setTimeout(() => { const input = document.querySelector('dees-modal .link-input') as HTMLInputElement; if (input) { input.focus(); } }, 100); }); } // Modal methods moved to WysiwygModalManager 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(); } } ] }); } }