622 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			622 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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<DeesInputList> {
 | |
|   // 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`
 | |
|       <div class="input-wrapper">
 | |
|         ${this.label ? html`<dees-label .label=${this.label} .required=${this.required}></dees-label>` : ''}
 | |
|         
 | |
|         <div class="list-container ${this.disabled ? 'disabled' : ''}">
 | |
|           <div class="list-items">
 | |
|             ${this.value.length > 0 ? this.value.map((item, index) => html`
 | |
|               <div
 | |
|                 class="list-item ${this.draggedIndex === index ? 'dragging' : ''} ${this.dragOverIndex === index ? 'drag-over' : ''}"
 | |
|                 draggable="${this.sortable && !this.disabled}"
 | |
|                 @dragstart=${(e: DragEvent) => 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`
 | |
|                   <div class="drag-handle">
 | |
|                     <dees-icon .icon=${'lucide:gripVertical'}></dees-icon>
 | |
|                   </div>
 | |
|                 ` : ''}
 | |
|                 
 | |
|                 <div class="item-content">
 | |
|                   ${this.editingIndex === index ? html`
 | |
|                     <input
 | |
|                       type="text"
 | |
|                       class="item-edit-input"
 | |
|                       .value=${this.editingValue}
 | |
|                       @input=${(e: InputEvent) => this.editingValue = (e.target as HTMLInputElement).value}
 | |
|                       @keydown=${(e: KeyboardEvent) => this.handleEditKeyDown(e, index)}
 | |
|                       @blur=${() => this.saveEdit(index)}
 | |
|                     />
 | |
|                   ` : html`
 | |
|                     <div class="item-text" @dblclick=${() => !this.disabled && this.startEdit(index)}>
 | |
|                       ${item}
 | |
|                     </div>
 | |
|                   `}
 | |
|                 </div>
 | |
|                 
 | |
|                 <div class="item-actions">
 | |
|                   ${this.editingIndex === index ? html`
 | |
|                     <button class="action-button save" @click=${() => this.saveEdit(index)}>
 | |
|                       <dees-icon .icon=${'lucide:check'}></dees-icon>
 | |
|                     </button>
 | |
|                     <button class="action-button cancel" @click=${() => this.cancelEdit()}>
 | |
|                       <dees-icon .icon=${'lucide:x'}></dees-icon>
 | |
|                     </button>
 | |
|                   ` : html`
 | |
|                     ${!this.disabled ? html`
 | |
|                       <button class="action-button" @click=${() => this.startEdit(index)}>
 | |
|                         <dees-icon .icon=${'lucide:pencil'}></dees-icon>
 | |
|                       </button>
 | |
|                       <button class="action-button delete" @click=${() => this.removeItem(index)}>
 | |
|                         <dees-icon .icon=${'lucide:trash2'}></dees-icon>
 | |
|                       </button>
 | |
|                     ` : ''}
 | |
|                   `}
 | |
|                 </div>
 | |
|               </div>
 | |
|             `) : html`
 | |
|               <div class="empty-state">
 | |
|                 No items added yet
 | |
|               </div>
 | |
|             `}
 | |
|           </div>
 | |
|           
 | |
|           ${!this.disabled && (!this.maxItems || this.value.length < this.maxItems) ? html`
 | |
|             <div class="add-item-container">
 | |
|               <input
 | |
|                 type="text"
 | |
|                 class="add-input"
 | |
|                 .placeholder=${this.placeholder}
 | |
|                 .value=${this.inputValue}
 | |
|                 @input=${this.handleInput}
 | |
|                 @keydown=${this.handleAddKeyDown}
 | |
|                 ?disabled=${this.disabled}
 | |
|               />
 | |
|               <dees-button
 | |
|                 class="add-button"
 | |
|                 @click=${this.addItem}
 | |
|                 ?disabled=${!this.inputValue.trim()}
 | |
|               >
 | |
|                 <dees-icon .icon=${'lucide:plus'}></dees-icon> Add
 | |
|               </dees-button>
 | |
|             </div>
 | |
|           ` : ''}
 | |
|         </div>
 | |
| 
 | |
|         ${this.validationText ? html`
 | |
|           <div class="validation-message">${this.validationText}</div>
 | |
|         ` : ''}
 | |
|         
 | |
|         ${this.description ? html`
 | |
|           <div class="description">${this.description}</div>
 | |
|         ` : ''}
 | |
|       </div>
 | |
|     `;
 | |
|   }
 | |
| 
 | |
|   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<boolean> {
 | |
|     // 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<boolean> {
 | |
|     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;
 | |
|   }
 | |
| } |