Files
dees-catalog/ts_web/elements/00group-input/dees-input-code/dees-input-code.ts

721 lines
23 KiB
TypeScript
Raw Normal View History

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<string> {
public static demo = () => html`
<dees-input-code
label="TypeScript Code"
key="code"
language="typescript"
height="300px"
.value=${'const greeting: string = "Hello World";\nconsole.log(greeting);'}
></dees-input-code>
`;
// 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`
<style>
.editor-wrapper {
height: ${this.height};
}
</style>
<div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
<div class="code-container">
<div class="toolbar">
<div class="toolbar-left">
<div class="language-selector">
<button
class="language-button"
@click=${this.toggleLanguageDropdown}
@blur=${this.handleLanguageBlur}
>
${currentLanguage.label}
<dees-icon .icon=${'lucide:ChevronDown'} iconSize="14"></dees-icon>
</button>
${this.isLanguageDropdownOpen ? html`
<div class="language-dropdown">
${LANGUAGES.map(lang => html`
<div
class="language-option ${lang.key === this.language ? 'selected' : ''}"
@mousedown=${(e: Event) => this.selectLanguage(e, lang.key)}
>
${lang.label}
</div>
`)}
</div>
` : ''}
</div>
</div>
<div class="toolbar-right">
<button
class="toolbar-button ${this.wordWrap === 'on' ? 'active' : ''}"
title="Word Wrap"
@click=${this.toggleWordWrap}
>
<dees-icon .icon=${'lucide:WrapText'} iconSize="16"></dees-icon>
</button>
<button
class="toolbar-button ${this.showLineNumbers ? 'active' : ''}"
title="Line Numbers"
@click=${this.toggleLineNumbers}
>
<dees-icon .icon=${'lucide:Hash'} iconSize="16"></dees-icon>
</button>
<div class="toolbar-divider"></div>
<button
class="toolbar-button ${this.copySuccess ? 'success' : ''}"
title="Copy Code"
@click=${this.copyCode}
>
<dees-icon .icon=${this.copySuccess ? 'lucide:Check' : 'lucide:Copy'} iconSize="16"></dees-icon>
</button>
<button
class="toolbar-button"
title="Expand"
@click=${this.openFullscreen}
>
<dees-icon .icon=${'lucide:Maximize2'} iconSize="16"></dees-icon>
</button>
</div>
</div>
<div class="editor-wrapper">
<dees-workspace-monaco
.content=${this.value}
.language=${this.language}
.wordWrap=${this.wordWrap}
@content-change=${this.handleContentChange}
></dees-workspace-monaco>
</div>
</div>
</div>
`;
}
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`
<style>
.modal-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;
}
.modal-toolbar .toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.modal-toolbar .toolbar-right {
display: flex;
align-items: center;
gap: 4px;
}
.modal-toolbar .language-selector {
position: relative;
}
.modal-toolbar .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;
}
.modal-toolbar .language-button:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
}
.modal-toolbar .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;
display: none;
}
.modal-toolbar .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;
}
.modal-toolbar .language-option:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
}
.modal-toolbar .language-option.selected {
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 20%)')};
}
.modal-toolbar .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;
}
.modal-toolbar .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%)')};
}
.modal-toolbar .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%)')};
}
.modal-toolbar .toolbar-button.success {
color: hsl(142.1 76.2% 36.3%);
}
.modal-toolbar .toolbar-divider {
width: 1px;
height: 20px;
background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')};
margin: 0 4px;
}
.modal-editor-wrapper {
position: relative;
height: calc(100vh - 175px);
width: 100%;
}
</style>
<div class="modal-toolbar">
<div class="toolbar-left">
<div class="language-selector">
<button class="language-button">
<span>${getLanguageLabel()}</span>
<dees-icon .icon=${'lucide:ChevronDown'} iconSize="14"></dees-icon>
</button>
<div class="language-dropdown">
${LANGUAGES.map(lang => html`
<div
class="language-option ${lang.key === modalLanguage ? 'selected' : ''}"
data-lang="${lang.key}"
>
${lang.label}
</div>
`)}
</div>
</div>
</div>
<div class="toolbar-right">
<button class="toolbar-button wrap-btn ${modalWordWrap === 'on' ? 'active' : ''}" title="Word Wrap">
<dees-icon .icon=${'lucide:WrapText'} iconSize="16"></dees-icon>
</button>
<button class="toolbar-button lines-btn ${modalShowLineNumbers ? 'active' : ''}" title="Line Numbers">
<dees-icon .icon=${'lucide:Hash'} iconSize="16"></dees-icon>
</button>
<div class="toolbar-divider"></div>
<button class="toolbar-button copy-btn" title="Copy Code">
<dees-icon .icon=${'lucide:Copy'} iconSize="16"></dees-icon>
</button>
</div>
</div>
<div class="modal-editor-wrapper">
<dees-workspace-monaco
.content=${currentValue}
.language=${modalLanguage}
.wordWrap=${modalWordWrap}
></dees-workspace-monaco>
</div>
`,
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);
}
}