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