@kubex/zinc 1.0.25 → 1.0.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/project.md +556 -0
- package/.github/workflows/js_build_and_deploy.yaml +9 -10
- package/dist/custom-elements.json +13575 -8856
- package/dist/vscode.html-custom-data.json +583 -48
- package/dist/web-types.json +1441 -181
- package/dist/zn.d.ts +1645 -660
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +1412 -893
- package/docs/_utilities/code-previews.cjs +10 -1
- package/docs/assets/scripts/docs.js +42 -18
- package/docs/data/data-table.json +229 -66
- package/docs/data/empty.json +6 -0
- package/docs/data/products-table.json +41 -193
- package/docs/pages/components/action-bar.md +121 -6
- package/docs/pages/components/alert.md +118 -15
- package/docs/pages/components/animated-button.md +299 -0
- package/docs/pages/components/audio-select.md +534 -0
- package/docs/pages/components/bulk-actions.md +79 -5
- package/docs/pages/components/button-group.md +235 -27
- package/docs/pages/components/button.md +139 -1
- package/docs/pages/components/chart.md +365 -9
- package/docs/pages/components/checkbox.md +387 -58
- package/docs/pages/components/chip.md +253 -33
- package/docs/pages/components/collapsible.md +393 -46
- package/docs/pages/components/cols.md +152 -30
- package/docs/pages/components/confirm.md +182 -16
- package/docs/pages/components/copy-button.md +169 -6
- package/docs/pages/components/data-select.md +194 -15
- package/docs/pages/components/data-table-filter.md +96 -5
- package/docs/pages/components/data-table-search.md +451 -0
- package/docs/pages/components/data-table-sort.md +66 -5
- package/docs/pages/components/data-table.md +727 -66
- package/docs/pages/components/datepicker.md +402 -7
- package/docs/pages/components/defined-label.md +120 -5
- package/docs/pages/components/dialog.md +339 -59
- package/docs/pages/components/dropdown.md +366 -11
- package/docs/pages/components/editor.md +604 -7
- package/docs/pages/components/empty-state.md +315 -16
- package/docs/pages/components/expanding-action.md +212 -28
- package/docs/pages/components/file.md +421 -12
- package/docs/pages/components/filter-container.md +158 -5
- package/docs/pages/components/filter-wrapper.md +167 -6
- package/docs/pages/components/form-group.md +383 -6
- package/docs/pages/components/hover-container.md +237 -6
- package/docs/pages/components/icon-picker.md +121 -0
- package/docs/pages/components/icon.md +454 -48
- package/docs/pages/components/inline-edit.md +787 -19
- package/docs/pages/components/input-group.md +207 -13
- package/docs/pages/components/input.md +194 -0
- package/docs/pages/components/item.md +18 -0
- package/docs/pages/components/key-container.md +115 -0
- package/docs/pages/components/key.md +101 -0
- package/docs/pages/components/menu.md +512 -6
- package/docs/pages/components/note.md +239 -36
- package/docs/pages/components/opt-group.md +107 -0
- package/docs/pages/components/pagination.md +332 -6
- package/docs/pages/components/pane.md +222 -6
- package/docs/pages/components/panel.md +213 -26
- package/docs/pages/components/popup.md +1354 -6
- package/docs/pages/components/priority-list.md +233 -0
- package/docs/pages/components/progress-bar.md +387 -6
- package/docs/pages/components/radio-group.md +242 -5
- package/docs/pages/components/radio.md +474 -6
- package/docs/pages/components/rating.md +331 -7
- package/docs/pages/components/reveal.md +524 -6
- package/docs/pages/components/scroll-container.md +552 -6
- package/docs/pages/components/select.md +762 -160
- package/docs/pages/components/skeleton.md +473 -30
- package/docs/pages/components/slideout.md +261 -5
- package/docs/pages/components/sp.md +99 -41
- package/docs/pages/components/split-button.md +347 -9
- package/docs/pages/components/split-pane.md +445 -6
- package/docs/pages/components/stat.md +549 -9
- package/docs/pages/components/status-indicator.md +230 -1
- package/docs/pages/components/stepper.md +563 -6
- package/docs/pages/components/tabs.md +439 -13
- package/docs/pages/components/textarea.md +415 -17
- package/docs/pages/components/tile.md +385 -26
- package/docs/pages/components/toggle.md +424 -6
- package/docs/pages/components/tooltip.md +367 -0
- package/docs/pages/components/translation-group.md +210 -0
- package/docs/pages/components/translations.md +319 -19
- package/docs/pages/components/vertical-stepper.md +401 -8
- package/docs/pages/components/well.md +300 -3
- package/docs/pages/getting-started/form-controls.md +162 -0
- package/docs/pages/index.md +116 -15
- package/package.json +5 -6
- package/scss/_global-spacing.scss +4 -0
- package/scss/_root.scss +2 -2
- package/scss/boot.scss +19 -0
- package/scss/shared/layout.scss +5 -0
- package/scss/themes/_light.scss +3 -3
- package/src/components/alert/alert.scss +1 -0
- package/src/components/animated-button/README.md +306 -0
- package/src/components/animated-button/animated-button.component.ts +229 -0
- package/src/components/animated-button/animated-button.scss +228 -0
- package/src/components/animated-button/animated-button.test.ts +143 -0
- package/src/components/animated-button/index.ts +12 -0
- package/src/components/button/button.component.ts +5 -0
- package/src/components/button-group/button-group.component.ts +25 -13
- package/src/components/button-group/button-group.scss +6 -0
- package/src/components/checkbox/checkbox.component.ts +4 -0
- package/src/components/checkbox/checkbox.scss +7 -4
- package/src/components/checkbox-group/checkbox-group.scss +7 -0
- package/src/components/chip/chip.component.ts +7 -2
- package/src/components/chip/chip.scss +16 -2
- package/src/components/collapsible/collapsible.component.ts +27 -20
- package/src/components/cols/cols.component.ts +14 -2
- package/src/components/cols/cols.scss +6 -12
- package/src/components/confirm/confirm.component.ts +7 -0
- package/src/components/copy-button/copy-button.component.ts +12 -11
- package/src/components/data-select/data-select.component.ts +215 -44
- package/src/components/data-select/data-select.scss +66 -6
- package/src/components/data-select/providers/currency-data-provider.ts +20 -6
- package/src/components/data-select/providers/provider.ts +1 -0
- package/src/components/data-select/providers/us-state-data-provider.ts +68 -0
- package/src/components/data-table/data-table.component.ts +81 -22
- package/src/components/data-table-search/data-table-search.component.ts +184 -0
- package/src/components/data-table-search/data-table-search.scss +17 -0
- package/src/components/data-table-search/data-table-search.test.ts +11 -0
- package/src/components/data-table-search/index.ts +12 -0
- package/src/components/datepicker/datepicker.component.ts +347 -13
- package/src/components/dialog/dialog.component.ts +1 -1
- package/src/components/empty-state/empty-state.scss +6 -1
- package/src/components/header/header.scss +3 -3
- package/src/components/icon/icon.component.ts +23 -1
- package/src/components/icon/icon.scss +23 -0
- package/src/components/icon-picker/brand-icons.ts +24 -0
- package/src/components/icon-picker/icon-picker.component.ts +407 -0
- package/src/components/icon-picker/icon-picker.scss +205 -0
- package/src/components/icon-picker/index.ts +12 -0
- package/src/components/icon-picker/line-icons.ts +612 -0
- package/src/components/icon-picker/material-icons.ts +14543 -0
- package/src/components/inline-edit/inline-edit.component.ts +190 -46
- package/src/components/inline-edit/inline-edit.scss +29 -0
- package/src/components/inline-edit/inline-edit.test.ts +314 -1
- package/src/components/input/input.component.ts +324 -5
- package/src/components/input/input.scss +87 -0
- package/src/components/input-group/input-group.component.ts +28 -7
- package/src/components/input-group/input-group.scss +48 -10
- package/src/components/item/item.component.ts +24 -3
- package/src/components/item/item.scss +18 -0
- package/src/components/key/index.ts +12 -0
- package/src/components/key/key.component.ts +47 -0
- package/src/components/key/key.scss +38 -0
- package/src/components/key-container/index.ts +12 -0
- package/src/components/key-container/key-container.component.ts +142 -0
- package/src/components/key-container/key-container.scss +33 -0
- package/src/components/menu-item/menu-item.component.ts +20 -0
- package/src/components/menu-item/menu-item.scss +69 -2
- package/src/components/navbar/navbar.component.ts +103 -11
- package/src/components/navbar/navbar.scss +6 -1
- package/src/components/opt-group/index.ts +12 -0
- package/src/components/opt-group/opt-group.component.ts +74 -0
- package/src/components/opt-group/opt-group.scss +37 -0
- package/src/components/opt-group/opt-group.test.ts +27 -0
- package/src/components/option/option.component.ts +1 -16
- package/src/components/option/option.scss +4 -0
- package/src/components/panel/panel.component.ts +10 -8
- package/src/components/panel/panel.scss +20 -0
- package/src/components/priority-list/index.ts +12 -0
- package/src/components/priority-list/priority-list.component.ts +611 -0
- package/src/components/priority-list/priority-list.scss +185 -0
- package/src/components/priority-list/priority-list.test.ts +139 -0
- package/src/components/progress-bar/progress-bar.component.ts +22 -16
- package/src/components/radio/radio.scss +10 -3
- package/src/components/select/select.component.ts +812 -72
- package/src/components/select/select.scss +108 -2
- package/src/components/select/select.test.ts +147 -2
- package/src/components/settings-container/settings-container.component.ts +44 -19
- package/src/components/settings-container/settings-container.scss +1 -1
- package/src/components/skeleton/skeleton.component.ts +6 -1
- package/src/components/slideout/slideout.scss +8 -3
- package/src/components/sp/sp.component.ts +3 -1
- package/src/components/sp/sp.scss +19 -0
- package/src/components/stepper/stepper.component.ts +16 -10
- package/src/components/textarea/textarea.scss +0 -4
- package/src/components/tile/tile.component.ts +12 -3
- package/src/components/tile/tile.scss +1 -1
- package/src/components/timer/timer.component.ts +15 -2
- package/src/components/toggle/toggle.component.ts +29 -26
- package/src/components/toggle/toggle.scss +13 -1
- package/src/components/translation-group/index.ts +12 -0
- package/src/components/translation-group/translation-group.component.ts +207 -0
- package/src/components/translation-group/translation-group.scss +8 -0
- package/src/components/translations/translations.component.ts +90 -44
- package/src/components/translations/translations.scss +4 -0
- package/src/components/vertical-stepper/vertical-stepper.component.ts +14 -9
- package/src/events/events.ts +12 -7
- package/src/events/zn-language-change.ts +7 -0
- package/src/events/zn-purchase.ts +11 -0
- package/src/events/zn-redirect.ts +7 -0
- package/src/events/zn-reorder.ts +7 -0
- package/src/events/zn-search-change.ts +11 -0
- package/src/events/zn-submit.ts +1 -1
- package/src/form-control.scss +4 -0
- package/src/internal/form-navigation.ts +510 -0
- package/src/internal/form.ts +6 -0
- package/src/zinc.ts +91 -83
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import {classMap} from "lit/directives/class-map.js";
|
|
2
|
+
import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
|
|
3
|
+
import {
|
|
4
|
+
FormControlController,
|
|
5
|
+
validValidityState,
|
|
6
|
+
} from "../../internal/form";
|
|
7
|
+
import {HasSlotController} from '../../internal/slot';
|
|
8
|
+
import {property, query, state} from 'lit/decorators.js';
|
|
9
|
+
import {repeat} from "lit/directives/repeat.js";
|
|
10
|
+
import ZincElement from '../../internal/zinc-element';
|
|
11
|
+
import ZnIcon from '../icon';
|
|
12
|
+
import type {ZincFormControl} from '../../internal/zinc-element';
|
|
13
|
+
|
|
14
|
+
import styles from './priority-list.scss';
|
|
15
|
+
|
|
16
|
+
export interface PriorityItem {
|
|
17
|
+
key: string;
|
|
18
|
+
priority: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @summary A reorderable list where each item receives a numerical priority based on its position. Supports drag-and-drop
|
|
23
|
+
* and keyboard reordering. Priority values are submitted as form data via hidden inputs.
|
|
24
|
+
*
|
|
25
|
+
* @documentation https://zinc.style/components/priority-list
|
|
26
|
+
* @status experimental
|
|
27
|
+
* @since 1.0
|
|
28
|
+
*
|
|
29
|
+
* @dependency zn-icon
|
|
30
|
+
*
|
|
31
|
+
* @slot - The default slot where list items are placed. Each slotted element must have a `value` attribute that
|
|
32
|
+
* uniquely identifies the item. The component automatically assigns slot names to project each item into the
|
|
33
|
+
* correct position. The item's position in the list determines its priority (top = `priority-start`, incrementing).
|
|
34
|
+
* @slot label - The component's label. Required for proper accessibility. Alternatively, use the `label` attribute.
|
|
35
|
+
* @slot label-tooltip - Used to add text displayed in a tooltip next to the label.
|
|
36
|
+
* @slot help-text - Text that describes how to use the component. Alternatively, use the `help-text` attribute.
|
|
37
|
+
*
|
|
38
|
+
* @event zn-change - Emitted when the order of items changes.
|
|
39
|
+
* @event zn-reorder - Emitted when items are reordered. Call `getPriorityMap()` on the component to get
|
|
40
|
+
* the updated `{ key: string, priority: number }[]` array.
|
|
41
|
+
*
|
|
42
|
+
* @csspart form-control - The form control wrapper.
|
|
43
|
+
* @csspart form-control-label - The label wrapper.
|
|
44
|
+
* @csspart form-control-input - The input area wrapper.
|
|
45
|
+
* @csspart form-control-help-text - The help text wrapper.
|
|
46
|
+
* @csspart list - The list container.
|
|
47
|
+
* @csspart item - An individual list item row.
|
|
48
|
+
* @csspart drag-handle - The drag handle icon.
|
|
49
|
+
* @csspart priority - The priority number badge.
|
|
50
|
+
* @csspart content - The content area of an item.
|
|
51
|
+
*
|
|
52
|
+
* @cssproperty --zn-priority-list-item-gap - The gap between list items. Defaults to `var(--zn-spacing-2x-small)`.
|
|
53
|
+
* @cssproperty --zn-priority-list-item-padding - The padding inside each item. Defaults to `var(--zn-spacing-small) var(--zn-spacing-medium)`.
|
|
54
|
+
* @cssproperty --zn-priority-list-handle-color - The color of the drag handle. Defaults to `var(--zn-color-neutral-500)`.
|
|
55
|
+
* @cssproperty --zn-priority-list-priority-color - The color of the priority number. Defaults to `var(--zn-color-neutral-600)`.
|
|
56
|
+
*/
|
|
57
|
+
export default class ZnPriorityList extends ZincElement implements ZincFormControl {
|
|
58
|
+
static dependencies = {
|
|
59
|
+
'zn-icon': ZnIcon,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
63
|
+
|
|
64
|
+
protected readonly formControlController = new FormControlController(this, {
|
|
65
|
+
value: (control: ZnPriorityList) => {
|
|
66
|
+
return JSON.stringify(control.getPriorityMap());
|
|
67
|
+
},
|
|
68
|
+
defaultValue: (control: ZnPriorityList) => control.defaultValue,
|
|
69
|
+
setValue: (control: ZnPriorityList, value: string) => {
|
|
70
|
+
if (value) {
|
|
71
|
+
try {
|
|
72
|
+
const parsed: unknown = JSON.parse(value);
|
|
73
|
+
if (Array.isArray(parsed) && parsed.every((item: unknown): item is string => typeof item === 'string')) {
|
|
74
|
+
control.value = parsed as string[];
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// ignore parse errors
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
|
84
|
+
|
|
85
|
+
@query('.priority-list__hidden-inputs') hiddenInputsContainer: HTMLDivElement;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The name prefix for form submission. Each item generates a hidden input named `{name}[{itemValue}]`
|
|
89
|
+
* with the priority number as its value.
|
|
90
|
+
*/
|
|
91
|
+
@property() name = '';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The current ordered list of item keys, reflecting the visual order. This is the source of truth for ordering.
|
|
95
|
+
* Each entry should match a `value` attribute on a slotted child element.
|
|
96
|
+
*/
|
|
97
|
+
@property({type: Array})
|
|
98
|
+
get value(): string[] {
|
|
99
|
+
return this._value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
set value(val: string[]) {
|
|
103
|
+
const oldVal = this._value;
|
|
104
|
+
this._value = [...val];
|
|
105
|
+
this._assignSlotNames();
|
|
106
|
+
this.requestUpdate('value', oldVal);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private _value: string[] = [];
|
|
110
|
+
|
|
111
|
+
@state() defaultValue: string[] = [];
|
|
112
|
+
|
|
113
|
+
/** The component's label. Required for proper accessibility. */
|
|
114
|
+
@property() label = '';
|
|
115
|
+
|
|
116
|
+
/** Text that appears in a tooltip next to the label. */
|
|
117
|
+
@property({attribute: 'label-tooltip'}) labelTooltip = '';
|
|
118
|
+
|
|
119
|
+
/** Help text that describes how to use the component. */
|
|
120
|
+
@property({attribute: 'help-text'}) helpText = '';
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Associates the control with a form by id. The form must be in the same document or shadow root.
|
|
124
|
+
*/
|
|
125
|
+
@property({reflect: true}) form = '';
|
|
126
|
+
|
|
127
|
+
/** The size of the list items. */
|
|
128
|
+
@property({reflect: true}) size: 'small' | 'medium' | 'large' = 'medium';
|
|
129
|
+
|
|
130
|
+
/** Whether the priority list is disabled. */
|
|
131
|
+
@property({type: Boolean, reflect: true}) disabled = false;
|
|
132
|
+
|
|
133
|
+
/** Ensures the form control has a value before allowing submission. */
|
|
134
|
+
@property({type: Boolean, reflect: true}) required = false;
|
|
135
|
+
|
|
136
|
+
/** The starting priority number. Defaults to 1. */
|
|
137
|
+
@property({type: Number, attribute: 'priority-start'}) priorityStart = 1;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* When set, the associated form will be submitted with the given action URL whenever items are reordered.
|
|
141
|
+
* If set to an empty string, the form will be submitted using its existing action.
|
|
142
|
+
*/
|
|
143
|
+
@property({attribute: 'formaction'}) formAction: string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* A comma-separated list of item keys defining the initial display order.
|
|
147
|
+
* Keys must match the `value` attributes on slotted children.
|
|
148
|
+
* Any slotted items not listed are appended at the end in DOM order.
|
|
149
|
+
*/
|
|
150
|
+
@property() order = '';
|
|
151
|
+
|
|
152
|
+
// Drag state
|
|
153
|
+
@state() private draggedKey: string | null = null;
|
|
154
|
+
@state() private isDragging = false;
|
|
155
|
+
private dragOverKey: string | null = null;
|
|
156
|
+
|
|
157
|
+
/** Gets the validity state object. */
|
|
158
|
+
get validity(): ValidityState {
|
|
159
|
+
return validValidityState;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Gets the validation message. */
|
|
163
|
+
get validationMessage(): string {
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
connectedCallback() {
|
|
168
|
+
super.connectedCallback();
|
|
169
|
+
this.defaultValue = [...this._value];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
protected firstUpdated(_changedProperties: PropertyValues) {
|
|
173
|
+
super.firstUpdated(_changedProperties);
|
|
174
|
+
this.formControlController.updateValidity();
|
|
175
|
+
this._initializeOrderFromSlot();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Initialize item order from slotted children if no explicit value was provided.
|
|
180
|
+
* If an `order` attribute is set, use it to define the initial order,
|
|
181
|
+
* appending any slotted items not listed in the order at the end.
|
|
182
|
+
*/
|
|
183
|
+
private _initializeOrderFromSlot() {
|
|
184
|
+
if (this._value.length === 0) {
|
|
185
|
+
const items = this._getSlottedItems();
|
|
186
|
+
const domKeys = items.map(el => el.getAttribute('value') || '').filter(v => v !== '');
|
|
187
|
+
|
|
188
|
+
if (this.order) {
|
|
189
|
+
const orderedKeys = this.order.split(',').map(k => k.trim()).filter(k => k !== '');
|
|
190
|
+
const domKeySet = new Set(domKeys);
|
|
191
|
+
// Only include keys that exist in the DOM
|
|
192
|
+
const validOrdered = orderedKeys.filter(k => domKeySet.has(k));
|
|
193
|
+
// Append any DOM items not listed in the order attribute
|
|
194
|
+
const orderedSet = new Set(validOrdered);
|
|
195
|
+
const remaining = domKeys.filter(k => !orderedSet.has(k));
|
|
196
|
+
this._value = [...validOrdered, ...remaining];
|
|
197
|
+
} else {
|
|
198
|
+
this._value = domKeys;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.defaultValue = [...this._value];
|
|
202
|
+
}
|
|
203
|
+
this._assignSlotNames();
|
|
204
|
+
this._syncHiddenInputs();
|
|
205
|
+
this.requestUpdate();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Gets all direct children with a `value` attribute (excluding those in reserved slots).
|
|
210
|
+
*/
|
|
211
|
+
private _getSlottedItems(): HTMLElement[] {
|
|
212
|
+
const reservedSlots = ['label', 'label-tooltip', 'help-text'];
|
|
213
|
+
return [...this.querySelectorAll<HTMLElement>(':scope > [value]')].filter(el => {
|
|
214
|
+
const slot = el.getAttribute('slot');
|
|
215
|
+
// Exclude elements in reserved semantic slots; allow unslotted or internally assigned items
|
|
216
|
+
return !slot || slot.startsWith('_item-') || !reservedSlots.includes(slot);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Assigns `slot` attributes to light DOM children based on their `value` attribute,
|
|
222
|
+
* so they project into the correct named slot in the shadow DOM.
|
|
223
|
+
*/
|
|
224
|
+
private _assignSlotNames() {
|
|
225
|
+
const items = [...this.querySelectorAll<HTMLElement>(':scope > [value]')];
|
|
226
|
+
items.forEach(el => {
|
|
227
|
+
const itemValue = el.getAttribute('value');
|
|
228
|
+
if (itemValue && !['label', 'label-tooltip', 'help-text'].includes(itemValue)) {
|
|
229
|
+
el.setAttribute('slot', `_item-${itemValue}`);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Returns the priority map: an array of { key, priority } objects.
|
|
236
|
+
*/
|
|
237
|
+
getPriorityMap(): PriorityItem[] {
|
|
238
|
+
return this._value.map((key, index) => ({
|
|
239
|
+
key,
|
|
240
|
+
priority: this.priorityStart + index,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Syncs hidden inputs so form data includes priority values.
|
|
246
|
+
*/
|
|
247
|
+
private _syncHiddenInputs() {
|
|
248
|
+
if (!this.hiddenInputsContainer) return;
|
|
249
|
+
|
|
250
|
+
this.hiddenInputsContainer.innerHTML = '';
|
|
251
|
+
|
|
252
|
+
if (!this.name) return;
|
|
253
|
+
|
|
254
|
+
this._value.forEach((key, index) => {
|
|
255
|
+
const input = document.createElement('input');
|
|
256
|
+
input.type = 'hidden';
|
|
257
|
+
input.name = `${this.name}[${key}]`;
|
|
258
|
+
input.value = String(this.priorityStart + index);
|
|
259
|
+
this.hiddenInputsContainer.appendChild(input);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Moves an item from one index to another.
|
|
265
|
+
*/
|
|
266
|
+
private _moveItem(fromIndex: number, toIndex: number) {
|
|
267
|
+
if (fromIndex === toIndex) return;
|
|
268
|
+
if (fromIndex < 0 || toIndex < 0) return;
|
|
269
|
+
if (fromIndex >= this._value.length || toIndex >= this._value.length) return;
|
|
270
|
+
|
|
271
|
+
const newOrder = [...this._value];
|
|
272
|
+
const [moved] = newOrder.splice(fromIndex, 1);
|
|
273
|
+
newOrder.splice(toIndex, 0, moved);
|
|
274
|
+
|
|
275
|
+
this._value = newOrder;
|
|
276
|
+
this._assignSlotNames();
|
|
277
|
+
this._syncHiddenInputs();
|
|
278
|
+
this.requestUpdate();
|
|
279
|
+
|
|
280
|
+
this.emit('zn-change');
|
|
281
|
+
this.emit('zn-reorder');
|
|
282
|
+
this._submitForm();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Submits the associated form if a `formaction` attribute is set.
|
|
287
|
+
*/
|
|
288
|
+
private _submitForm() {
|
|
289
|
+
if (this.formAction === undefined || this.formAction === null) return;
|
|
290
|
+
|
|
291
|
+
const form = this.getForm();
|
|
292
|
+
if (form) {
|
|
293
|
+
const submitter = document.createElement('button');
|
|
294
|
+
submitter.type = 'submit';
|
|
295
|
+
submitter.hidden = true;
|
|
296
|
+
if (this.formAction) {
|
|
297
|
+
submitter.setAttribute('formaction', this.formAction);
|
|
298
|
+
}
|
|
299
|
+
form.appendChild(submitter);
|
|
300
|
+
form.requestSubmit(submitter);
|
|
301
|
+
submitter.remove();
|
|
302
|
+
} else if (this.formAction) {
|
|
303
|
+
// No parent form — create a temporary one to submit the priority data
|
|
304
|
+
const tempForm = document.createElement('form');
|
|
305
|
+
tempForm.method = 'post';
|
|
306
|
+
tempForm.action = this.formAction;
|
|
307
|
+
tempForm.hidden = true;
|
|
308
|
+
|
|
309
|
+
this._value.forEach((key, index) => {
|
|
310
|
+
if (this.name) {
|
|
311
|
+
const input = document.createElement('input');
|
|
312
|
+
input.type = 'hidden';
|
|
313
|
+
input.name = `${this.name}[${key}]`;
|
|
314
|
+
input.value = String(this.priorityStart + index);
|
|
315
|
+
tempForm.appendChild(input);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
document.body.appendChild(tempForm);
|
|
320
|
+
tempForm.submit();
|
|
321
|
+
tempForm.remove();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --- Drag and Drop Handlers ---
|
|
326
|
+
|
|
327
|
+
private _handleDragStart(event: DragEvent, key: string) {
|
|
328
|
+
if (this.disabled) {
|
|
329
|
+
event.preventDefault();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.draggedKey = key;
|
|
334
|
+
this.isDragging = true;
|
|
335
|
+
|
|
336
|
+
if (event.dataTransfer) {
|
|
337
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
338
|
+
event.dataTransfer.setData('text/plain', key);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private _handleDragEnd() {
|
|
343
|
+
if (this.isDragging) {
|
|
344
|
+
// Emit events for the final position change
|
|
345
|
+
this._syncHiddenInputs();
|
|
346
|
+
this.emit('zn-change');
|
|
347
|
+
this.emit('zn-reorder');
|
|
348
|
+
this._submitForm();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this.draggedKey = null;
|
|
352
|
+
this.dragOverKey = null;
|
|
353
|
+
this.isDragging = false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private _handleDragOver(event: DragEvent, key: string) {
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
|
|
359
|
+
if (!this.draggedKey || this.draggedKey === key) return;
|
|
360
|
+
|
|
361
|
+
if (event.dataTransfer) {
|
|
362
|
+
event.dataTransfer.dropEffect = 'move';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Move the item in real-time so other items shift around
|
|
366
|
+
const fromIndex = this._value.indexOf(this.draggedKey);
|
|
367
|
+
const toIndex = this._value.indexOf(key);
|
|
368
|
+
|
|
369
|
+
if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return;
|
|
370
|
+
|
|
371
|
+
const newOrder = [...this._value];
|
|
372
|
+
const [moved] = newOrder.splice(fromIndex, 1);
|
|
373
|
+
newOrder.splice(toIndex, 0, moved);
|
|
374
|
+
this._value = newOrder;
|
|
375
|
+
this._assignSlotNames();
|
|
376
|
+
this.requestUpdate();
|
|
377
|
+
|
|
378
|
+
this.dragOverKey = key;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private _handleDragLeave(event: DragEvent, key: string) {
|
|
382
|
+
const related = event.relatedTarget as Node | null;
|
|
383
|
+
const target = event.currentTarget as HTMLElement;
|
|
384
|
+
if (related && target.contains(related)) return;
|
|
385
|
+
|
|
386
|
+
if (this.dragOverKey === key) {
|
|
387
|
+
this.dragOverKey = null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private _handleDrop(event: DragEvent) {
|
|
392
|
+
event.preventDefault();
|
|
393
|
+
// Item is already in position from real-time dragover moves
|
|
394
|
+
// dragend handles cleanup and event emission
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// --- Keyboard Handlers ---
|
|
398
|
+
|
|
399
|
+
private _handleKeyDown(event: KeyboardEvent, key: string) {
|
|
400
|
+
if (this.disabled) return;
|
|
401
|
+
|
|
402
|
+
const index = this._value.indexOf(key);
|
|
403
|
+
if (index === -1) return;
|
|
404
|
+
|
|
405
|
+
let newIndex: number | null = null;
|
|
406
|
+
|
|
407
|
+
switch (event.key) {
|
|
408
|
+
case 'ArrowUp':
|
|
409
|
+
if (index > 0) {
|
|
410
|
+
newIndex = index - 1;
|
|
411
|
+
}
|
|
412
|
+
break;
|
|
413
|
+
case 'ArrowDown':
|
|
414
|
+
if (index < this._value.length - 1) {
|
|
415
|
+
newIndex = index + 1;
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
case 'Home':
|
|
419
|
+
if (index > 0) {
|
|
420
|
+
newIndex = 0;
|
|
421
|
+
}
|
|
422
|
+
break;
|
|
423
|
+
case 'End':
|
|
424
|
+
if (index < this._value.length - 1) {
|
|
425
|
+
newIndex = this._value.length - 1;
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
default:
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (newIndex !== null) {
|
|
433
|
+
event.preventDefault();
|
|
434
|
+
this._moveItem(index, newIndex);
|
|
435
|
+
|
|
436
|
+
// Focus the moved item after re-render
|
|
437
|
+
this.updateComplete.then(() => {
|
|
438
|
+
const items = this.shadowRoot?.querySelectorAll<HTMLElement>('.priority-list__item');
|
|
439
|
+
items?.[newIndex!]?.focus();
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// --- Slot Change Handler ---
|
|
445
|
+
|
|
446
|
+
private _handleSlotChange() {
|
|
447
|
+
// Query all items with a value attribute (including those already assigned to _item- slots)
|
|
448
|
+
const allItems = [...this.querySelectorAll<HTMLElement>(':scope > [value]')];
|
|
449
|
+
const reservedSlots = ['label', 'label-tooltip', 'help-text'];
|
|
450
|
+
const slottedKeys = allItems
|
|
451
|
+
.filter(el => {
|
|
452
|
+
const slot = el.getAttribute('slot');
|
|
453
|
+
return !slot || slot.startsWith('_item-') || !reservedSlots.includes(slot);
|
|
454
|
+
})
|
|
455
|
+
.map(el => el.getAttribute('value') || '')
|
|
456
|
+
.filter(v => v !== '');
|
|
457
|
+
|
|
458
|
+
// If no items found but we already have an order, skip — this is likely a
|
|
459
|
+
// slotchange firing because items moved from the default slot to named slots
|
|
460
|
+
if (slottedKeys.length === 0 && this._value.length > 0) return;
|
|
461
|
+
|
|
462
|
+
const currentKeys = new Set(this._value);
|
|
463
|
+
const slottedKeysSet = new Set(slottedKeys);
|
|
464
|
+
|
|
465
|
+
// Add new keys that are not in the current order
|
|
466
|
+
const newKeys = slottedKeys.filter(k => !currentKeys.has(k));
|
|
467
|
+
// Remove keys that are no longer in the DOM
|
|
468
|
+
const remainingKeys = this._value.filter(k => slottedKeysSet.has(k));
|
|
469
|
+
|
|
470
|
+
if (newKeys.length > 0 || remainingKeys.length !== this._value.length) {
|
|
471
|
+
this._value = [...remainingKeys, ...newKeys];
|
|
472
|
+
this._assignSlotNames();
|
|
473
|
+
this._syncHiddenInputs();
|
|
474
|
+
this.requestUpdate();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// --- Form Control Methods ---
|
|
479
|
+
|
|
480
|
+
/** Checks for validity but does not show a validation message. */
|
|
481
|
+
checkValidity(): boolean {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Gets the associated form, if one exists. */
|
|
486
|
+
getForm(): HTMLFormElement | null {
|
|
487
|
+
return this.formControlController.getForm();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
|
491
|
+
reportValidity(): boolean {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Sets a custom validation message. Pass an empty string to restore validity. */
|
|
496
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
497
|
+
setCustomValidity(_message = '') {
|
|
498
|
+
// No custom validation needed for this component
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
protected updated(changedProperties: PropertyValues) {
|
|
502
|
+
super.updated(changedProperties);
|
|
503
|
+
|
|
504
|
+
if (changedProperties.has('value') || changedProperties.has('name') || changedProperties.has('priorityStart')) {
|
|
505
|
+
this._syncHiddenInputs();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
render() {
|
|
510
|
+
const hasLabelSlot = this.hasSlotController.test('label');
|
|
511
|
+
const hasLabelTooltipSlot = this.hasSlotController.test('label-tooltip');
|
|
512
|
+
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
|
513
|
+
const hasLabel = this.label ? true : hasLabelSlot;
|
|
514
|
+
const hasLabelTooltip = this.labelTooltip ? true : hasLabelTooltipSlot;
|
|
515
|
+
const hasHelpText = this.helpText ? true : hasHelpTextSlot;
|
|
516
|
+
|
|
517
|
+
return html`
|
|
518
|
+
<fieldset
|
|
519
|
+
part="form-control"
|
|
520
|
+
class=${classMap({
|
|
521
|
+
'form-control': true,
|
|
522
|
+
'form-control--small': this.size === 'small',
|
|
523
|
+
'form-control--medium': this.size === 'medium',
|
|
524
|
+
'form-control--large': this.size === 'large',
|
|
525
|
+
'form-control--has-label': hasLabel,
|
|
526
|
+
'form-control--has-label-tooltip': hasLabelTooltip,
|
|
527
|
+
'form-control--has-help-text': hasHelpText,
|
|
528
|
+
})}
|
|
529
|
+
aria-labelledby="label"
|
|
530
|
+
aria-describedby="help-text"
|
|
531
|
+
?disabled=${this.disabled}>
|
|
532
|
+
<label
|
|
533
|
+
part="form-control-label"
|
|
534
|
+
id="label"
|
|
535
|
+
class="form-control__label"
|
|
536
|
+
aria-hidden=${hasLabel ? 'false' : 'true'}>
|
|
537
|
+
<slot name="label">${this.label}</slot>
|
|
538
|
+
${hasLabelTooltip
|
|
539
|
+
? html`
|
|
540
|
+
<zn-tooltip class="form-control--label-tooltip">
|
|
541
|
+
<div slot="content">
|
|
542
|
+
<slot name="label-tooltip">${this.labelTooltip}</slot>
|
|
543
|
+
</div>
|
|
544
|
+
<zn-icon src="info"></zn-icon>
|
|
545
|
+
</zn-tooltip>`
|
|
546
|
+
: ''}
|
|
547
|
+
</label>
|
|
548
|
+
|
|
549
|
+
<div part="form-control-input" class="form-control-input">
|
|
550
|
+
<div
|
|
551
|
+
part="list"
|
|
552
|
+
class=${classMap({
|
|
553
|
+
'priority-list': true,
|
|
554
|
+
'priority-list--disabled': this.disabled,
|
|
555
|
+
'priority-list--drag-active': this.isDragging,
|
|
556
|
+
})}
|
|
557
|
+
role="listbox"
|
|
558
|
+
aria-label=${this.label || 'Reorderable list'}
|
|
559
|
+
aria-orientation="vertical">
|
|
560
|
+
${repeat(this._value, (key) => key, (key, index) => html`
|
|
561
|
+
<div
|
|
562
|
+
part="item"
|
|
563
|
+
class=${classMap({
|
|
564
|
+
'priority-list__item': true,
|
|
565
|
+
'priority-list__item--dragging': this.draggedKey === key,
|
|
566
|
+
'priority-list__item--disabled': this.disabled,
|
|
567
|
+
})}
|
|
568
|
+
role="option"
|
|
569
|
+
tabindex=${this.disabled ? '-1' : '0'}
|
|
570
|
+
aria-selected="false"
|
|
571
|
+
aria-label="${key} - priority ${this.priorityStart + index}"
|
|
572
|
+
draggable=${this.disabled ? 'false' : 'true'}
|
|
573
|
+
@dragstart=${(e: DragEvent) => this._handleDragStart(e, key)}
|
|
574
|
+
@dragend=${this._handleDragEnd}
|
|
575
|
+
@dragover=${(e: DragEvent) => this._handleDragOver(e, key)}
|
|
576
|
+
@dragleave=${(e: DragEvent) => this._handleDragLeave(e, key)}
|
|
577
|
+
@drop=${(e: DragEvent) => this._handleDrop(e)}
|
|
578
|
+
@keydown=${(e: KeyboardEvent) => this._handleKeyDown(e, key)}>
|
|
579
|
+
<span class="priority-list__handle" part="drag-handle">
|
|
580
|
+
<zn-icon src="drag_indicator" size="16"></zn-icon>
|
|
581
|
+
</span>
|
|
582
|
+
<span class="priority-list__priority" part="priority">${this.priorityStart + index}</span>
|
|
583
|
+
<span class="priority-list__content" part="content">
|
|
584
|
+
<slot name="_item-${key}"></slot>
|
|
585
|
+
</span>
|
|
586
|
+
<span class="priority-list__actions" part="actions">
|
|
587
|
+
<slot name="action-${key}"></slot>
|
|
588
|
+
</span>
|
|
589
|
+
</div>
|
|
590
|
+
`)}
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<!-- Hidden default slot to capture light DOM children for initial detection -->
|
|
594
|
+
<div class="priority-list__collector">
|
|
595
|
+
<slot @slotchange=${this._handleSlotChange}></slot>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
<div class="priority-list__hidden-inputs"></div>
|
|
599
|
+
</div>
|
|
600
|
+
|
|
601
|
+
<div
|
|
602
|
+
part="form-control-help-text"
|
|
603
|
+
id="help-text"
|
|
604
|
+
class="form-control__help-text"
|
|
605
|
+
aria-hidden=${hasHelpText ? 'false' : 'true'}>
|
|
606
|
+
<slot name="help-text">${this.helpText}</slot>
|
|
607
|
+
</div>
|
|
608
|
+
</fieldset>
|
|
609
|
+
`;
|
|
610
|
+
}
|
|
611
|
+
}
|