import { customElement, html, css, cssManager, property, state, type TemplateResult, } from '@design.estate/dees-element'; import { DeesInputBase } from './dees-input-base.js'; import './dees-icon.js'; import './dees-button.js'; import { demoFunc } from './dees-input-list.demo.js'; declare global { interface HTMLElementTagNameMap { 'dees-input-list': DeesInputList; } } @customElement('dees-input-list') export class DeesInputList extends DeesInputBase { // STATIC public static demo = demoFunc; // INSTANCE @property({ type: Array }) public value: string[] = []; @property({ type: String }) public placeholder: string = 'Add new item...'; @property({ type: Number }) public maxItems: number = 0; // 0 means unlimited @property({ type: Number }) public minItems: number = 0; @property({ type: Boolean }) public allowDuplicates: boolean = false; @property({ type: Boolean }) public sortable: boolean = false; @property({ type: Boolean }) public confirmDelete: boolean = false; @property({ type: String }) public validationText: string = ''; @state() private inputValue: string = ''; @state() private editingIndex: number = -1; @state() private editingValue: string = ''; @state() private draggedIndex: number = -1; @state() private dragOverIndex: number = -1; public static styles = [ ...DeesInputBase.baseStyles, cssManager.defaultStyles, css` :host { display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; } .input-wrapper { width: 100%; } .list-container { background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; border-radius: 6px; overflow: hidden; transition: all 0.15s ease; } .list-container:hover:not(.disabled) { border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; } .list-container:focus-within { border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')}; } .list-container.disabled { opacity: 0.6; cursor: not-allowed; } .list-items { max-height: 400px; overflow-y: auto; } .list-item { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; transition: all 0.15s ease; position: relative; overflow: hidden; /* Prevent animation from affecting scroll bounds */ } .list-item:last-of-type { border-bottom: none; } .list-item:hover:not(.disabled) { background: ${cssManager.bdTheme('hsl(0 0% 97.5%)', 'hsl(0 0% 6.9%)')}; } .list-item.dragging { opacity: 0.4; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 10.8%)')}; } .list-item.drag-over { background: ${cssManager.bdTheme('hsl(210 40% 93.1%)', 'hsl(215 20.2% 13.8%)')}; border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; } .drag-handle { display: flex; align-items: center; cursor: move; color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')}; transition: color 0.15s ease; } .drag-handle:hover { color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; } .drag-handle dees-icon { width: 16px; height: 16px; } .item-content { flex: 1; display: flex; align-items: center; min-width: 0; } .item-text { flex: 1; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; font-size: 14px; line-height: 20px; word-break: break-word; } .item-edit-input { flex: 1; padding: 4px 8px; font-size: 14px; font-family: inherit; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; border: 1px solid ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; border-radius: 4px; outline: none; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; } .item-actions { display: flex; gap: 4px; align-items: center; } .action-button { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 4px; background: transparent; border: none; cursor: pointer; transition: all 0.15s ease; color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; } .action-button:hover { background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; } .action-button.save { color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')}; } .action-button.save:hover { background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.1)')}; } .action-button.cancel { color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 50.6%)')}; } .action-button.cancel:hover { background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 50.6% / 0.1)')}; } .action-button.delete { color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 50.6%)')}; } .action-button.delete:hover { background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 50.6% / 0.1)')}; } .action-button dees-icon { width: 14px; height: 14px; } .add-item-container { display: flex; gap: 8px; padding: 12px 16px; background: ${cssManager.bdTheme('hsl(0 0% 97.5%)', 'hsl(0 0% 6.9%)')}; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; } .add-input { flex: 1; padding: 8px 12px; font-size: 14px; font-family: inherit; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; border-radius: 4px; outline: none; color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; transition: all 0.15s ease; } .add-input:focus { border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')}; } .add-input::placeholder { color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')}; } .add-input:disabled { cursor: not-allowed; opacity: 0.5; } .add-button { padding: 8px 16px; } .empty-state { padding: 32px 16px; text-align: center; color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')}; font-size: 14px; font-style: italic; } .validation-message { color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')}; font-size: 13px; margin-top: 6px; line-height: 1.5; } .description { color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; font-size: 13px; margin-top: 6px; line-height: 1.5; } /* Scrollbar styling */ .list-items::-webkit-scrollbar { width: 8px; } .list-items::-webkit-scrollbar-track { background: transparent; } .list-items::-webkit-scrollbar-thumb { background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 24.9%)')}; border-radius: 4px; } .list-items::-webkit-scrollbar-thumb:hover { background: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 34.9%)')}; } /* Animation for adding/removing items */ @keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .list-item { animation: slideIn 0.2s ease; } /* Override any inherited contain/content-visibility that might cause scrolling issues */ .list-items, .list-item { content-visibility: visible !important; contain: none !important; contain-intrinsic-size: auto !important; } `, ]; public render(): TemplateResult { return html`
${this.label ? html`` : ''}
${this.value.length > 0 ? this.value.map((item, index) => html`
this.handleDragStart(e, index)} @dragend=${this.handleDragEnd} @dragover=${(e: DragEvent) => this.handleDragOver(e, index)} @dragleave=${this.handleDragLeave} @drop=${(e: DragEvent) => this.handleDrop(e, index)} > ${this.sortable && !this.disabled ? html`
` : ''}
${this.editingIndex === index ? html` this.editingValue = (e.target as HTMLInputElement).value} @keydown=${(e: KeyboardEvent) => this.handleEditKeyDown(e, index)} @blur=${() => this.saveEdit(index)} /> ` : html`
!this.disabled && this.startEdit(index)}> ${item}
`}
${this.editingIndex === index ? html` ` : html` ${!this.disabled ? html` ` : ''} `}
`) : html`
No items added yet
`}
${!this.disabled && (!this.maxItems || this.value.length < this.maxItems) ? html`
Add
` : ''}
${this.validationText ? html`
${this.validationText}
` : ''} ${this.description ? html`
${this.description}
` : ''}
`; } private handleInput(e: InputEvent) { this.inputValue = (e.target as HTMLInputElement).value; } private handleAddKeyDown(e: KeyboardEvent) { if (e.key === 'Enter' && this.inputValue.trim()) { e.preventDefault(); this.addItem(); } } private handleEditKeyDown(e: KeyboardEvent, index: number) { if (e.key === 'Enter') { e.preventDefault(); this.saveEdit(index); } else if (e.key === 'Escape') { e.preventDefault(); this.cancelEdit(); } } private addItem() { const trimmedValue = this.inputValue.trim(); if (!trimmedValue) return; if (!this.allowDuplicates && this.value.includes(trimmedValue)) { this.validationText = 'This item already exists in the list'; setTimeout(() => this.validationText = '', 3000); return; } if (this.maxItems && this.value.length >= this.maxItems) { this.validationText = `Maximum ${this.maxItems} items allowed`; setTimeout(() => this.validationText = '', 3000); return; } this.value = [...this.value, trimmedValue]; this.inputValue = ''; this.validationText = ''; // Clear the input const input = this.shadowRoot?.querySelector('.add-input') as HTMLInputElement; if (input) { input.value = ''; input.focus(); } this.emitChange(); } private startEdit(index: number) { this.editingIndex = index; this.editingValue = this.value[index]; // Focus the input after render this.updateComplete.then(() => { const input = this.shadowRoot?.querySelector('.item-edit-input') as HTMLInputElement; if (input) { input.focus(); input.select(); } }); } private saveEdit(index: number) { const trimmedValue = this.editingValue.trim(); if (!trimmedValue) { this.cancelEdit(); return; } if (!this.allowDuplicates && trimmedValue !== this.value[index] && this.value.includes(trimmedValue)) { this.validationText = 'This item already exists in the list'; setTimeout(() => this.validationText = '', 3000); return; } const newValue = [...this.value]; newValue[index] = trimmedValue; this.value = newValue; this.editingIndex = -1; this.editingValue = ''; this.validationText = ''; this.emitChange(); } private cancelEdit() { this.editingIndex = -1; this.editingValue = ''; } private async removeItem(index: number) { if (this.confirmDelete) { const confirmed = await this.showConfirmDialog(`Delete "${this.value[index]}"?`); if (!confirmed) return; } this.value = this.value.filter((_, i) => i !== index); this.emitChange(); } private async showConfirmDialog(message: string): Promise { // For now, use native confirm. In production, this should use a proper modal return confirm(message); } // Drag and drop handlers private handleDragStart(e: DragEvent, index: number) { if (!this.sortable || this.disabled) return; this.draggedIndex = index; e.dataTransfer!.effectAllowed = 'move'; e.dataTransfer!.setData('text/plain', index.toString()); } private handleDragEnd() { this.draggedIndex = -1; this.dragOverIndex = -1; } private handleDragOver(e: DragEvent, index: number) { if (!this.sortable || this.disabled) return; e.preventDefault(); e.dataTransfer!.dropEffect = 'move'; this.dragOverIndex = index; } private handleDragLeave() { this.dragOverIndex = -1; } private handleDrop(e: DragEvent, dropIndex: number) { if (!this.sortable || this.disabled) return; e.preventDefault(); const draggedIndex = parseInt(e.dataTransfer!.getData('text/plain')); if (draggedIndex !== dropIndex) { const newValue = [...this.value]; const [draggedItem] = newValue.splice(draggedIndex, 1); newValue.splice(dropIndex, 0, draggedItem); this.value = newValue; this.emitChange(); } this.draggedIndex = -1; this.dragOverIndex = -1; } private emitChange() { this.dispatchEvent(new CustomEvent('change', { detail: { value: this.value }, bubbles: true, composed: true })); this.changeSubject.next(this); } public getValue(): string[] { return this.value; } public setValue(value: string[]): void { this.value = value || []; } public async validate(): Promise { if (this.required && (!this.value || this.value.length === 0)) { this.validationText = 'At least one item is required'; return false; } if (this.minItems && this.value.length < this.minItems) { this.validationText = `At least ${this.minItems} items required`; return false; } this.validationText = ''; return true; } }