feat(dees-input-list): add new input list component with demo and validation features
This commit is contained in:
		
							
								
								
									
										275
									
								
								ts_web/elements/dees-input-list.demo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								ts_web/elements/dees-input-list.demo.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,275 @@ | ||||
| import { html, css } from '@design.estate/dees-element'; | ||||
| import '@design.estate/dees-wcctools/demotools'; | ||||
| import './dees-panel.js'; | ||||
| import './dees-form.js'; | ||||
| import './dees-input-text.js'; | ||||
| import './dees-form-submit.js'; | ||||
|  | ||||
| export const demoFunc = () => html` | ||||
|   <dees-demowrapper> | ||||
|     <style> | ||||
|       ${css` | ||||
|         .demo-container { | ||||
|           display: flex; | ||||
|           flex-direction: column; | ||||
|           gap: 24px; | ||||
|           padding: 24px; | ||||
|           max-width: 1200px; | ||||
|           margin: 0 auto; | ||||
|         } | ||||
|          | ||||
|         dees-panel { | ||||
|           margin-bottom: 24px; | ||||
|         } | ||||
|          | ||||
|         dees-panel:last-child { | ||||
|           margin-bottom: 0; | ||||
|         } | ||||
|          | ||||
|         .grid-layout { | ||||
|           display: grid; | ||||
|           grid-template-columns: 1fr 1fr; | ||||
|           gap: 16px; | ||||
|         } | ||||
|          | ||||
|         @media (max-width: 768px) { | ||||
|           .grid-layout { | ||||
|             grid-template-columns: 1fr; | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         .output-preview { | ||||
|           margin-top: 16px; | ||||
|           padding: 16px; | ||||
|           background: #f3f4f6; | ||||
|           border-radius: 4px; | ||||
|           font-size: 12px; | ||||
|           color: #374151; | ||||
|           word-break: break-all; | ||||
|           max-height: 200px; | ||||
|           overflow-y: auto; | ||||
|         } | ||||
|          | ||||
|         @media (prefers-color-scheme: dark) { | ||||
|           .output-preview { | ||||
|             background: #2c2c2c; | ||||
|             color: #e4e4e7; | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         .feature-note { | ||||
|           margin-top: 12px; | ||||
|           padding: 12px; | ||||
|           background: #eff6ff; | ||||
|           border-left: 3px solid #3b82f6; | ||||
|           border-radius: 4px; | ||||
|           font-size: 13px; | ||||
|           color: #1e40af; | ||||
|         } | ||||
|          | ||||
|         @media (prefers-color-scheme: dark) { | ||||
|           .feature-note { | ||||
|             background: #1e3a5f; | ||||
|             color: #93c5fd; | ||||
|           } | ||||
|         } | ||||
|       `} | ||||
|     </style> | ||||
|      | ||||
|     <div class="demo-container"> | ||||
|       <dees-panel .title=${'1. Basic List Input'} .subtitle=${'Simple list management with add, edit, and delete'}> | ||||
|         <dees-input-list | ||||
|           .label=${'Shopping List'} | ||||
|           .placeholder=${'Add item to your list...'} | ||||
|           .value=${['Milk', 'Bread', 'Eggs', 'Cheese']} | ||||
|           .description=${'Double-click to edit items, or use the edit button'} | ||||
|         ></dees-input-list> | ||||
|         <div class="feature-note"> | ||||
|           💡 Double-click any item to quickly edit it inline | ||||
|         </div> | ||||
|       </dees-panel> | ||||
|  | ||||
|       <dees-panel .title=${'2. Sortable List'} .subtitle=${'Drag and drop to reorder items'}> | ||||
|         <dees-input-list | ||||
|           .label=${'Task Priority'} | ||||
|           .placeholder=${'Add a task...'} | ||||
|           .sortable=${true} | ||||
|           .value=${[ | ||||
|             'Review pull requests', | ||||
|             'Fix critical bug', | ||||
|             'Update documentation', | ||||
|             'Deploy to production', | ||||
|             'Team standup meeting' | ||||
|           ]} | ||||
|           .description=${'Drag items using the handle to reorder them'} | ||||
|         ></dees-input-list> | ||||
|         <div class="feature-note"> | ||||
|           🔄 Drag the grip handle to reorder tasks by priority | ||||
|         </div> | ||||
|       </dees-panel> | ||||
|  | ||||
|       <dees-panel .title=${'3. Validation & Constraints'} .subtitle=${'Lists with minimum/maximum items and duplicate prevention'}> | ||||
|         <div class="grid-layout"> | ||||
|           <dees-input-list | ||||
|             .label=${'Team Members (Min 2, Max 5)'} | ||||
|             .placeholder=${'Add team member...'} | ||||
|             .minItems=${2} | ||||
|             .maxItems=${5} | ||||
|             .value=${['Alice', 'Bob']} | ||||
|             .required=${true} | ||||
|             .description=${'Add 2-5 team members'} | ||||
|           ></dees-input-list> | ||||
|            | ||||
|           <dees-input-list | ||||
|             .label=${'Unique Tags (No Duplicates)'} | ||||
|             .placeholder=${'Add unique tag...'} | ||||
|             .allowDuplicates=${false} | ||||
|             .value=${['frontend', 'backend', 'database']} | ||||
|             .description=${'Duplicate items are not allowed'} | ||||
|           ></dees-input-list> | ||||
|         </div> | ||||
|       </dees-panel> | ||||
|  | ||||
|       <dees-panel .title=${'4. Delete Confirmation'} .subtitle=${'Require confirmation before deleting items'}> | ||||
|         <dees-input-list | ||||
|           .label=${'Important Documents'} | ||||
|           .placeholder=${'Add document name...'} | ||||
|           .confirmDelete=${true} | ||||
|           .value=${[ | ||||
|             'Contract_2024.pdf', | ||||
|             'Financial_Report_Q3.xlsx', | ||||
|             'Project_Proposal.docx', | ||||
|             'Meeting_Notes.txt' | ||||
|           ]} | ||||
|           .description=${'Deletion requires confirmation for safety'} | ||||
|         ></dees-input-list> | ||||
|       </dees-panel> | ||||
|  | ||||
|       <dees-panel .title=${'5. Disabled State'} .subtitle=${'Read-only list display'}> | ||||
|         <dees-input-list | ||||
|           .label=${'System Defaults'} | ||||
|           .value=${['Default Setting 1', 'Default Setting 2', 'Default Setting 3']} | ||||
|           .disabled=${true} | ||||
|           .description=${'These items cannot be modified'} | ||||
|         ></dees-input-list> | ||||
|       </dees-panel> | ||||
|  | ||||
|       <dees-panel .title=${'6. Form Integration'} .subtitle=${'List input working within a form context'}> | ||||
|         <dees-form> | ||||
|           <dees-input-text | ||||
|             .label=${'Recipe Name'} | ||||
|             .placeholder=${'My Amazing Recipe'} | ||||
|             .required=${true} | ||||
|             .key=${'name'} | ||||
|           ></dees-input-text> | ||||
|            | ||||
|           <div class="grid-layout"> | ||||
|             <dees-input-list | ||||
|               .label=${'Ingredients'} | ||||
|               .placeholder=${'Add ingredient...'} | ||||
|               .required=${true} | ||||
|               .minItems=${3} | ||||
|               .key=${'ingredients'} | ||||
|               .sortable=${true} | ||||
|               .value=${[ | ||||
|                 '2 cups flour', | ||||
|                 '1 cup sugar', | ||||
|                 '3 eggs' | ||||
|               ]} | ||||
|               .description=${'Add at least 3 ingredients'} | ||||
|             ></dees-input-list> | ||||
|              | ||||
|             <dees-input-list | ||||
|               .label=${'Instructions'} | ||||
|               .placeholder=${'Add instruction step...'} | ||||
|               .required=${true} | ||||
|               .minItems=${2} | ||||
|               .key=${'instructions'} | ||||
|               .sortable=${true} | ||||
|               .value=${[ | ||||
|                 'Preheat oven to 350°F', | ||||
|                 'Mix dry ingredients' | ||||
|               ]} | ||||
|               .description=${'Add cooking instructions in order'} | ||||
|             ></dees-input-list> | ||||
|           </div> | ||||
|            | ||||
|           <dees-input-text | ||||
|             .label=${'Notes'} | ||||
|             .inputType=${'textarea'} | ||||
|             .placeholder=${'Any special notes or tips...'} | ||||
|             .key=${'notes'} | ||||
|           ></dees-input-text> | ||||
|            | ||||
|           <dees-form-submit .text=${'Save Recipe'}></dees-form-submit> | ||||
|         </dees-form> | ||||
|       </dees-panel> | ||||
|  | ||||
|       <dees-panel .title=${'7. Interactive Demo'} .subtitle=${'Build your own feature list and see the data'}> | ||||
|         <dees-input-list | ||||
|           id="interactive-list" | ||||
|           .label=${'Product Features'} | ||||
|           .placeholder=${'Add a feature...'} | ||||
|           .sortable=${true} | ||||
|           .confirmDelete=${false} | ||||
|           .allowDuplicates=${false} | ||||
|           .maxItems=${10} | ||||
|           @change=${(e: CustomEvent) => { | ||||
|             const preview = document.querySelector('#list-json'); | ||||
|             if (preview) { | ||||
|               const data = { | ||||
|                 items: e.detail.value, | ||||
|                 count: e.detail.value.length, | ||||
|                 timestamp: new Date().toISOString() | ||||
|               }; | ||||
|               preview.textContent = JSON.stringify(data, null, 2); | ||||
|             } | ||||
|           }} | ||||
|         ></dees-input-list> | ||||
|          | ||||
|         <div class="output-preview" id="list-json"> | ||||
|           { | ||||
|             "items": [], | ||||
|             "count": 0, | ||||
|             "timestamp": "${new Date().toISOString()}" | ||||
|           } | ||||
|         </div> | ||||
|          | ||||
|         <div class="feature-note"> | ||||
|           ✨ Add, edit, remove, and reorder items to see the JSON output update in real-time | ||||
|         </div> | ||||
|       </dees-panel> | ||||
|  | ||||
|       <dees-panel .title=${'8. Advanced Configuration'} .subtitle=${'Combine all features for complex use cases'}> | ||||
|         <dees-input-list | ||||
|           .label=${'Project Milestones'} | ||||
|           .placeholder=${'Add milestone...'} | ||||
|           .value=${[ | ||||
|             'Project Kickoff - Week 1', | ||||
|             'Requirements Gathering - Week 2-3', | ||||
|             'Design Phase - Week 4-6', | ||||
|             'Development Sprint 1 - Week 7-9', | ||||
|             'Testing & QA - Week 10-11', | ||||
|             'Deployment - Week 12' | ||||
|           ]} | ||||
|           .sortable=${true} | ||||
|           .confirmDelete=${true} | ||||
|           .allowDuplicates=${false} | ||||
|           .minItems=${3} | ||||
|           .maxItems=${12} | ||||
|           .required=${true} | ||||
|           .description=${'Manage project milestones (3-12 items, sortable, no duplicates)'} | ||||
|         ></dees-input-list> | ||||
|       </dees-panel> | ||||
|  | ||||
|       <dees-panel .title=${'9. Empty State'} .subtitle=${'How the component looks with no items'}> | ||||
|         <dees-input-list | ||||
|           .label=${'Your Ideas'} | ||||
|           .placeholder=${'Share your ideas...'} | ||||
|           .value=${[]} | ||||
|           .description=${'Start adding items to build your list'} | ||||
|         ></dees-input-list> | ||||
|       </dees-panel> | ||||
|     </div> | ||||
|   </dees-demowrapper> | ||||
| `; | ||||
							
								
								
									
										614
									
								
								ts_web/elements/dees-input-list.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										614
									
								
								ts_web/elements/dees-input-list.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,614 @@ | ||||
| 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; | ||||
|       } | ||||
|  | ||||
|       .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; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   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; | ||||
|   } | ||||
| } | ||||
| @@ -32,6 +32,7 @@ export * from './dees-input-datepicker.js'; | ||||
| export * from './dees-input-dropdown.js'; | ||||
| export * from './dees-input-fileupload.js'; | ||||
| export * from './dees-input-iban.js'; | ||||
| export * from './dees-input-list.js'; | ||||
| export * from './profilepicture/dees-input-profilepicture.js'; | ||||
| export * from './dees-input-typelist.js'; | ||||
| export * from './dees-input-phone.js'; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user