import { DeesInputBase } from '../dees-input-base/dees-input-base.js'; import { customElement, type TemplateResult, property, html, cssManager, css, state, } from '@design.estate/dees-element'; import { themeDefaultStyles } from '../../00theme.js'; import { DeesModal } from '../../dees-modal/dees-modal.js'; import '../../dees-icon/dees-icon.js'; import '../../dees-label/dees-label.js'; import '../../00group-workspace/dees-workspace-monaco/dees-workspace-monaco.js'; import { DeesWorkspaceMonaco } from '../../00group-workspace/dees-workspace-monaco/dees-workspace-monaco.js'; declare global { interface HTMLElementTagNameMap { 'dees-input-code': DeesInputCode; } } // Common programming languages for the language selector const LANGUAGES = [ { key: 'typescript', label: 'TypeScript' }, { key: 'javascript', label: 'JavaScript' }, { key: 'json', label: 'JSON' }, { key: 'html', label: 'HTML' }, { key: 'css', label: 'CSS' }, { key: 'scss', label: 'SCSS' }, { key: 'markdown', label: 'Markdown' }, { key: 'yaml', label: 'YAML' }, { key: 'xml', label: 'XML' }, { key: 'sql', label: 'SQL' }, { key: 'python', label: 'Python' }, { key: 'java', label: 'Java' }, { key: 'csharp', label: 'C#' }, { key: 'cpp', label: 'C++' }, { key: 'go', label: 'Go' }, { key: 'rust', label: 'Rust' }, { key: 'shell', label: 'Shell' }, { key: 'plaintext', label: 'Plain Text' }, ]; @customElement('dees-input-code') export class DeesInputCode extends DeesInputBase { public static demo = () => html` `; // INSTANCE @property({ type: String }) accessor value: string = ''; @property({ type: String }) accessor language: string = 'typescript'; @property({ type: String }) accessor height: string = '200px'; @property({ type: String }) accessor wordWrap: 'on' | 'off' = 'off'; @property({ type: Boolean }) accessor showLineNumbers: boolean = true; @state() accessor isLanguageDropdownOpen: boolean = false; @state() accessor copySuccess: boolean = false; private editorElement: DeesWorkspaceMonaco | null = null; public static styles = [ themeDefaultStyles, ...DeesInputBase.baseStyles, cssManager.defaultStyles, css` * { box-sizing: border-box; } :host { display: block; } .code-container { border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; border-radius: 6px; overflow: hidden; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; } .toolbar { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')}; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; gap: 8px; } .toolbar-left { display: flex; align-items: center; gap: 8px; } .toolbar-right { display: flex; align-items: center; gap: 4px; } .language-selector { position: relative; } .language-button { display: flex; align-items: center; gap: 6px; padding: 4px 10px; font-size: 12px; font-weight: 500; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 12%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 20%)')}; border-radius: 4px; cursor: pointer; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; transition: all 0.15s ease; } .language-button:hover { background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')}; } .language-dropdown { position: absolute; top: 100%; left: 0; margin-top: 4px; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 20%)')}; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 100; max-height: 250px; overflow-y: auto; min-width: 140px; } .language-option { padding: 8px 12px; font-size: 12px; cursor: pointer; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; transition: background 0.15s ease; } .language-option:hover { background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')}; } .language-option.selected { background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 20%)')}; } .toolbar-button { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: transparent; border: none; border-radius: 4px; cursor: pointer; color: ${cssManager.bdTheme('hsl(0 0% 45%)', 'hsl(0 0% 60%)')}; transition: all 0.15s ease; } .toolbar-button:hover { background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 15%)')}; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; } .toolbar-button.active { background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')}; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; } .toolbar-button.success { color: hsl(142.1 76.2% 36.3%); } .editor-wrapper { position: relative; } dees-workspace-monaco { display: block; } .toolbar-divider { width: 1px; height: 20px; background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')}; margin: 0 4px; } :host([disabled]) .code-container { opacity: 0.5; pointer-events: none; } `, ]; public render(): TemplateResult { const currentLanguage = LANGUAGES.find(l => l.key === this.language) || LANGUAGES[0]; return html`
${this.isLanguageDropdownOpen ? html`
${LANGUAGES.map(lang => html`
this.selectLanguage(e, lang.key)} > ${lang.label}
`)}
` : ''}
`; } async firstUpdated() { this.editorElement = this.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco; if (this.editorElement) { // Subscribe to content changes from the editor this.editorElement.contentSubject.subscribe((newContent: string) => { if (this.value !== newContent) { this.value = newContent; this.changeSubject.next(this as any); } }); } } private toggleLanguageDropdown() { this.isLanguageDropdownOpen = !this.isLanguageDropdownOpen; } private handleLanguageBlur() { // Small delay to allow click events on dropdown items setTimeout(() => { this.isLanguageDropdownOpen = false; }, 150); } private async selectLanguage(e: Event, languageKey: string) { e.preventDefault(); this.language = languageKey; this.isLanguageDropdownOpen = false; // Update the editor language if (this.editorElement) { this.editorElement.language = languageKey; const editor = await this.editorElement.editorDeferred.promise; const model = editor.getModel(); if (model) { (window as any).monaco.editor.setModelLanguage(model, languageKey); } } } private toggleWordWrap() { this.wordWrap = this.wordWrap === 'on' ? 'off' : 'on'; this.updateEditorOption('wordWrap', this.wordWrap); } private toggleLineNumbers() { this.showLineNumbers = !this.showLineNumbers; this.updateEditorOption('lineNumbers', this.showLineNumbers ? 'on' : 'off'); } private async updateEditorOption(option: string, value: any) { if (this.editorElement) { const editor = await this.editorElement.editorDeferred.promise; editor.updateOptions({ [option]: value }); } } private async copyCode() { try { await navigator.clipboard.writeText(this.value); this.copySuccess = true; setTimeout(() => { this.copySuccess = false; }, 2000); } catch (err) { console.error('Failed to copy code:', err); } } private handleContentChange(e: CustomEvent) { const newContent = e.detail; if (this.value !== newContent) { this.value = newContent; this.changeSubject.next(this as any); } } public async openFullscreen() { const currentValue = this.value; let modalEditorElement: DeesWorkspaceMonaco | null = null; // Modal-specific state let modalLanguage = this.language; let modalWordWrap = this.wordWrap; let modalShowLineNumbers = this.showLineNumbers; let modalLanguageDropdownOpen = false; let modalCopySuccess = false; // Helper to get current language label const getLanguageLabel = () => { const lang = LANGUAGES.find(l => l.key === modalLanguage); return lang ? lang.label : 'TypeScript'; }; // Helper to update toolbar UI const updateToolbarUI = (modal: DeesModal) => { const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar'); if (!toolbar) return; // Update language button text const langBtn = toolbar.querySelector('.language-button span'); if (langBtn) langBtn.textContent = getLanguageLabel(); // Update word wrap button const wrapBtn = toolbar.querySelector('.wrap-btn') as HTMLElement; if (wrapBtn) { wrapBtn.classList.toggle('active', modalWordWrap === 'on'); } // Update line numbers button const linesBtn = toolbar.querySelector('.lines-btn') as HTMLElement; if (linesBtn) { linesBtn.classList.toggle('active', modalShowLineNumbers); } // Update copy button const copyBtn = toolbar.querySelector('.copy-btn') as HTMLElement; const copyIcon = copyBtn?.querySelector('dees-icon') as any; if (copyBtn && copyIcon) { copyBtn.classList.toggle('success', modalCopySuccess); copyIcon.icon = modalCopySuccess ? 'lucide:Check' : 'lucide:Copy'; } // Update dropdown visibility const dropdown = toolbar.querySelector('.language-dropdown') as HTMLElement; if (dropdown) { dropdown.style.display = modalLanguageDropdownOpen ? 'block' : 'none'; } }; const modal = await DeesModal.createAndShow({ heading: this.label || 'Code Editor', width: 'fullscreen', contentPadding: 0, content: html` `, menuOptions: [ { name: 'Cancel', action: async (modalRef) => { await modalRef.destroy(); }, }, { name: 'Save & Close', action: async (modalRef) => { // Get the editor content from the modal modalEditorElement = modalRef.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco; if (modalEditorElement) { const editor = await modalEditorElement.editorDeferred.promise; const newValue = editor.getValue(); this.setValue(newValue); } await modalRef.destroy(); }, }, ], }); // Wait for modal to render await new Promise(resolve => setTimeout(resolve, 100)); modalEditorElement = modal.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco; // Wire up toolbar event handlers const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar'); if (toolbar) { // Language button click const langBtn = toolbar.querySelector('.language-button'); langBtn?.addEventListener('click', () => { modalLanguageDropdownOpen = !modalLanguageDropdownOpen; updateToolbarUI(modal); }); // Language option clicks const langOptions = toolbar.querySelectorAll('.language-option'); langOptions.forEach((option) => { option.addEventListener('click', async () => { const newLang = (option as HTMLElement).dataset.lang; if (newLang && modalEditorElement) { modalLanguage = newLang; modalLanguageDropdownOpen = false; // Update editor language const editor = await modalEditorElement.editorDeferred.promise; const model = editor.getModel(); if (model) { (window as any).monaco.editor.setModelLanguage(model, newLang); } // Update selected state langOptions.forEach(opt => opt.classList.remove('selected')); option.classList.add('selected'); updateToolbarUI(modal); } }); }); // Word wrap button const wrapBtn = toolbar.querySelector('.wrap-btn'); wrapBtn?.addEventListener('click', async () => { modalWordWrap = modalWordWrap === 'on' ? 'off' : 'on'; if (modalEditorElement) { const editor = await modalEditorElement.editorDeferred.promise; editor.updateOptions({ wordWrap: modalWordWrap }); } updateToolbarUI(modal); }); // Line numbers button const linesBtn = toolbar.querySelector('.lines-btn'); linesBtn?.addEventListener('click', async () => { modalShowLineNumbers = !modalShowLineNumbers; if (modalEditorElement) { const editor = await modalEditorElement.editorDeferred.promise; editor.updateOptions({ lineNumbers: modalShowLineNumbers ? 'on' : 'off' }); } updateToolbarUI(modal); }); // Copy button const copyBtn = toolbar.querySelector('.copy-btn'); copyBtn?.addEventListener('click', async () => { if (modalEditorElement) { const editor = await modalEditorElement.editorDeferred.promise; const content = editor.getValue(); try { await navigator.clipboard.writeText(content); modalCopySuccess = true; updateToolbarUI(modal); setTimeout(() => { modalCopySuccess = false; updateToolbarUI(modal); }, 2000); } catch (err) { console.error('Failed to copy code:', err); } } }); // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (modalLanguageDropdownOpen && !langBtn?.contains(e.target as Node)) { modalLanguageDropdownOpen = false; updateToolbarUI(modal); } }, { once: true }); } } public getValue(): string { return this.value; } public setValue(value: string): void { this.value = value; if (this.editorElement) { this.editorElement.content = value; // Also update the Monaco editor directly if it's already loaded this.editorElement.editorDeferred.promise.then(editor => { if (editor.getValue() !== value) { editor.setValue(value); } }); } this.changeSubject.next(this as any); } }