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-dropdown.js'; | ||||||
| export * from './dees-input-fileupload.js'; | export * from './dees-input-fileupload.js'; | ||||||
| export * from './dees-input-iban.js'; | export * from './dees-input-iban.js'; | ||||||
|  | export * from './dees-input-list.js'; | ||||||
| export * from './profilepicture/dees-input-profilepicture.js'; | export * from './profilepicture/dees-input-profilepicture.js'; | ||||||
| export * from './dees-input-typelist.js'; | export * from './dees-input-typelist.js'; | ||||||
| export * from './dees-input-phone.js'; | export * from './dees-input-phone.js'; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user