@rozie-ui/sortable-list-lit 0.1.5

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/dist/index.mjs ADDED
@@ -0,0 +1,744 @@
1
+ import { LitElement, css, html } from "lit";
2
+ import { customElement, property, query, queryAssignedElements, state } from "lit/decorators.js";
3
+ import { SignalWatcher, signal } from "@lit-labs/preact-signals";
4
+ import { __rozieReconcileAfterDomMutation, createLitControllableProperty, rozieAttr, rozieClass, rozieListeners, rozieSpread, rozieStyle } from "@rozie/runtime-lit";
5
+ import { repeat } from "lit/directives/repeat.js";
6
+ import { keyed } from "lit/directives/keyed.js";
7
+ import SortableJS from "sortablejs";
8
+ //#region src/internal/useSortableJS.ts
9
+ /**
10
+ * useSortableJS — framework-agnostic SortableJS-vs-reconciler bridge.
11
+ *
12
+ * Why this exists
13
+ * ---------------
14
+ * Wrapping SortableJS for cross-framework consumption (Rozie's
15
+ * `examples/SortableList.rozie`) has four recurring fragilities every wrapper
16
+ * library independently re-discovers:
17
+ *
18
+ * 1. **Three-handler ambiguity.** SortableJS fires distinct `onUpdate`,
19
+ * `onAdd`, and `onRemove` events for the same logical drag in cross-list
20
+ * moves. Inline handlers in user code can ONLY disambiguate by reading
21
+ * `e.from === listEl` / `e.to === listEl`, which is the same disambiguation
22
+ * `onEnd` can perform in a SINGLE handler. Collapsing to one onEnd path
23
+ * removes the per-handler boilerplate.
24
+ *
25
+ * 2. **Fragile event shapes.** When SortableJS falls back to the mouse-event
26
+ * drag path (Playwright's mouse.move/down/up sequence; some touch devices;
27
+ * HTML5 DnD aborts), `e.item` may not be a valid Node by the time the
28
+ * handler runs (the source-side `onRemove` fires AFTER the destination
29
+ * has already detached the element). Inline
30
+ * `listEl.insertBefore(e.item, …)` throws inside SortableJS's
31
+ * `_dispatchEvent`. SortableJS catches and swallows the throw silently —
32
+ * the model writeback never runs, leaving the framework's view stale.
33
+ * THIS is the root cause of the duplicate-on-drop bug surfaced by the
34
+ * `sortable-nested-solid-deeper` VR spec.
35
+ *
36
+ * 3. **`e.oldIndex` is occasionally null.** Fallback-mode events lose the
37
+ * index. Identity-based item lookup (`items().indexOf(stashedItem)`) is
38
+ * strictly more reliable.
39
+ *
40
+ * 4. **Lit lit-html `repeat`-cache desync.** Every reconciler EXCEPT Lit
41
+ * handles the SortableJS DOM-restore + model writeback dance cleanly.
42
+ * Lit needs `$reconcileAfterDomMutation()` (a Rozie sigil that lowers to
43
+ * `__rozieReconcileAfterDomMutation(this)` on Lit + `void 0` elsewhere).
44
+ * The helper invokes the `afterCommit` callback after each `onCommit`;
45
+ * users wire `afterCommit: () => $reconcileAfterDomMutation()` to honour
46
+ * the Lit requirement without leaking lit-html internals into user code.
47
+ *
48
+ * Cross-target API contract
49
+ * -------------------------
50
+ * The helper is vanilla JS. It has NO framework imports. The same exported
51
+ * symbol resolves identically across React, Vue, Svelte, Angular, Solid and
52
+ * Lit consumer builds via the colocated relative `./internal/useSortableJS`
53
+ * copy vendored into each leaf package by scripts/codegen.mjs.
54
+ *
55
+ * Caller contract:
56
+ * - `items: () => T[]` is called fresh on every event. Wire it to the
57
+ * current snapshot (`$props.items` etc.).
58
+ * - `onCommit(next)` is called once per successful drag commit with the
59
+ * new items array. The caller writes it to whatever reactive surface
60
+ * drives re-renders.
61
+ * - `afterCommit?()` is called immediately after each `onCommit`. Used on
62
+ * Lit to invoke `$reconcileAfterDomMutation()`.
63
+ *
64
+ * Returns `{ destroy, instance }`. The caller wires
65
+ * - teardown via `return handle.destroy` from `$onMount`
66
+ * - runtime option updates via `instance.option('disabled', v)` from
67
+ * `$watch(() => $props.disabled, ...)`.
68
+ *
69
+ * @public
70
+ */
71
+ /** Symbol stashed on `e.item` between `onStart` and `onEnd` to ferry the
72
+ * dragged item DATA (not just the DOM node) across cross-list moves and
73
+ * survive `e.oldIndex` going null in fallback-mode events. The double
74
+ * underscore prefix signals "Rozie-internal, do not collide with user
75
+ * data". */
76
+ const ROZIE_ITEM_STASH_KEY = "__rozieItem";
77
+ /** Read the stashed item, falling back to a `null`/`undefined` when the
78
+ * element doesn't have the property. Centralised so the cast lives in
79
+ * exactly one place. */
80
+ function readStash(item) {
81
+ if (item === null || typeof item !== "object") return void 0;
82
+ return item[ROZIE_ITEM_STASH_KEY];
83
+ }
84
+ /** Try-catch wrapper for DOM operations that may throw on fragile event
85
+ * paths. Returning `false` lets the caller proceed to the model writeback
86
+ * — the framework's re-render off the new model brings the DOM back into
87
+ * sync (may flicker briefly, but vastly better than today's silent drop). */
88
+ function safeDom(op) {
89
+ try {
90
+ op();
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ /**
97
+ * Wire a SortableJS instance to a reactive items array. See module doc
98
+ * for the full rationale + cross-target contract.
99
+ */
100
+ function useSortableJS(listEl, opts) {
101
+ const baseOptions = { ...opts.options ?? {} };
102
+ /** Item-scoped child lookup. `listEl.children` now also includes the
103
+ * non-draggable `#header` / `#footer` slot holes (direct children of the
104
+ * list container), so the DOM-restore sites must index ONLY the item rows —
105
+ * `:scope > .rozie-sortable-item` — to keep the restore index item-relative
106
+ * and correct (no off-by-one from header/footer). */
107
+ const itemChildAt = (i) => listEl.querySelectorAll(":scope > .rozie-sortable-item")[i] ?? null;
108
+ delete baseOptions.onStart;
109
+ delete baseOptions.onEnd;
110
+ delete baseOptions.onUpdate;
111
+ delete baseOptions.onAdd;
112
+ delete baseOptions.onRemove;
113
+ delete baseOptions.onClone;
114
+ /** Stash dragged item DATA on `e.item` so cross-list moves can recover
115
+ * it on the destination side. */
116
+ const handleStart = (e) => {
117
+ const current = opts.items();
118
+ const fromIdx = typeof e.oldDraggableIndex === "number" ? e.oldDraggableIndex : typeof e.oldIndex === "number" ? e.oldIndex : -1;
119
+ if (fromIdx >= 0 && fromIdx < current.length) {
120
+ const item = e.item;
121
+ if (item !== null && typeof item === "object") item[ROZIE_ITEM_STASH_KEY] = current[fromIdx];
122
+ }
123
+ opts.onStart?.(e);
124
+ };
125
+ /** The single point of truth for committed drags. Disambiguates via
126
+ * `e.from` and `e.to`.
127
+ *
128
+ * SortableJS's event lifecycle is asymmetric across lists: `onEnd` fires
129
+ * ONLY on the source list (the one that owned `e.item` at drag start).
130
+ * The destination list of a cross-list move receives an `onAdd` event,
131
+ * NOT an `onEnd`. We register this handler against BOTH:
132
+ * - source-list `onEnd` — handles same-list reorder + cross-list source
133
+ * - destination-list `onAdd` — handles cross-list destination
134
+ *
135
+ * The single function services both event names because the
136
+ * `from === listEl` / `to === listEl` disambiguation is sufficient to
137
+ * determine our role; SortableJS's event name doesn't carry additional
138
+ * information we need. Registering both is the load-bearing fix — the
139
+ * inline-handlers design DID register onAdd; collapsing to onEnd-only
140
+ * would lose the destination-side commit entirely. */
141
+ const handleCommit = (e) => {
142
+ try {
143
+ const fromUs = e.from === listEl;
144
+ const toUs = e.to === listEl;
145
+ const sameList = fromUs && toUs;
146
+ if (!fromUs && !toUs) return;
147
+ const current = opts.items();
148
+ const kind = sameList ? "reorder" : fromUs ? "remove" : "add";
149
+ const stashedItem = readStash(e.item);
150
+ const oldIndexHint = typeof e.oldDraggableIndex === "number" ? e.oldDraggableIndex : typeof e.oldIndex === "number" ? e.oldIndex : -1;
151
+ const newIndexHint = typeof e.newDraggableIndex === "number" ? e.newDraggableIndex : typeof e.newIndex === "number" ? e.newIndex : -1;
152
+ let next;
153
+ let oldIndex;
154
+ let newIndex;
155
+ let moved;
156
+ if (sameList) {
157
+ const movedFromIdx = stashedItem !== void 0 && current.indexOf(stashedItem) !== -1 ? current.indexOf(stashedItem) : oldIndexHint;
158
+ if (movedFromIdx < 0 || movedFromIdx >= current.length) return;
159
+ moved = current[movedFromIdx];
160
+ safeDom(() => {
161
+ const ref = itemChildAt(movedFromIdx);
162
+ listEl.insertBefore(e.item, ref);
163
+ });
164
+ next = [...current];
165
+ next.splice(movedFromIdx, 1);
166
+ const targetIdx = newIndexHint >= 0 && newIndexHint <= next.length ? newIndexHint : next.length;
167
+ next.splice(targetIdx, 0, moved);
168
+ oldIndex = movedFromIdx;
169
+ newIndex = targetIdx;
170
+ } else if (fromUs) {
171
+ if (e.pullMode === "clone") return;
172
+ const movedFromIdx = stashedItem !== void 0 && current.indexOf(stashedItem) !== -1 ? current.indexOf(stashedItem) : oldIndexHint;
173
+ if (movedFromIdx < 0 || movedFromIdx >= current.length) return;
174
+ moved = current[movedFromIdx];
175
+ safeDom(() => {
176
+ const ref = itemChildAt(movedFromIdx);
177
+ listEl.insertBefore(e.item, ref);
178
+ });
179
+ next = [...current];
180
+ next.splice(movedFromIdx, 1);
181
+ oldIndex = movedFromIdx;
182
+ newIndex = -1;
183
+ } else {
184
+ moved = stashedItem;
185
+ if (moved === void 0) {
186
+ safeDom(() => {
187
+ e.item?.remove?.();
188
+ });
189
+ return;
190
+ }
191
+ safeDom(() => {
192
+ e.item?.remove?.();
193
+ });
194
+ next = [...current];
195
+ const targetIdx = newIndexHint >= 0 && newIndexHint <= next.length ? newIndexHint : next.length;
196
+ next.splice(targetIdx, 0, moved);
197
+ oldIndex = -1;
198
+ newIndex = targetIdx;
199
+ }
200
+ queueMicrotask(() => {
201
+ opts.onCommit(next);
202
+ opts.afterCommit?.();
203
+ opts.onChange?.({
204
+ kind,
205
+ oldIndex,
206
+ newIndex,
207
+ item: moved
208
+ });
209
+ });
210
+ } finally {
211
+ if (e.from === listEl) {
212
+ const item = e.item;
213
+ if (item !== null && typeof item === "object") delete item[ROZIE_ITEM_STASH_KEY];
214
+ opts.onEnd?.(e);
215
+ }
216
+ }
217
+ };
218
+ /** Bridge the `__rozieItem` stash from the original DOM node to its
219
+ * clone when SortableJS is in clone mode (`group.pull === 'clone'`).
220
+ *
221
+ * Clone-mode lifecycle:
222
+ * 1. `onStart` fires on the source — we stash the dragged item DATA
223
+ * on `e.item.__rozieItem`.
224
+ * 2. SortableJS clones the node — `clone` is the new DOM element
225
+ * that physically travels to the destination.
226
+ * 3. `onClone` fires with `{ item, clone }` — THIS HOOK — we copy
227
+ * the stash from `item` to `clone` so step 4 can find it.
228
+ * 4. `onAdd` fires on the destination with `e.item === clone`
229
+ * (NOT the original). Destination-side `handleCommit` reads
230
+ * `__rozieItem` off the clone via `readStash` → recovers the
231
+ * dragged item DATA, splices it into its array.
232
+ * 5. `onEnd` fires on the source with `e.item === original`.
233
+ * `handleCommit` (clone-mode short-circuit) returns early so
234
+ * no spurious `remove` change is fired; the `finally` cleans
235
+ * up the stash on the original.
236
+ *
237
+ * The `clone` DOM property is not in SortableJS's strict d.ts for
238
+ * `SortableEvent` (it's documented in their JS source but not the
239
+ * exported types), so we widen the parameter type locally. */
240
+ const handleClone = (e) => {
241
+ const original = e.item;
242
+ const clone = e.clone;
243
+ if (original !== null && typeof original === "object" && clone !== null && typeof clone === "object" && ROZIE_ITEM_STASH_KEY in original) clone[ROZIE_ITEM_STASH_KEY] = original[ROZIE_ITEM_STASH_KEY];
244
+ };
245
+ const instance = new SortableJS(listEl, {
246
+ draggable: ".rozie-sortable-item",
247
+ ...baseOptions,
248
+ onStart: handleStart,
249
+ onEnd: handleCommit,
250
+ onAdd: handleCommit,
251
+ onClone: handleClone
252
+ });
253
+ return {
254
+ destroy: () => {
255
+ instance.destroy();
256
+ },
257
+ instance
258
+ };
259
+ }
260
+ //#endregion
261
+ //#region \0@oxc-project+runtime@0.127.0/helpers/decorate.js
262
+ function __decorate(decorators, target, key, desc) {
263
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
264
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
265
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
266
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
267
+ }
268
+ //#endregion
269
+ //#region src/SortableList.ts
270
+ let SortableList = class SortableList extends SignalWatcher(LitElement) {
271
+ constructor(..._args) {
272
+ super(..._args);
273
+ this._items_attr = [];
274
+ this._itemsControllable = createLitControllableProperty({
275
+ host: this,
276
+ eventName: "items-change",
277
+ defaultValue: [],
278
+ initialControlledValue: void 0
279
+ });
280
+ this.itemKey = null;
281
+ this.handle = null;
282
+ this.group = null;
283
+ this.animation = 150;
284
+ this.disabled = false;
285
+ this.disableKeyboard = false;
286
+ this.options = {};
287
+ this.labelFor = null;
288
+ this.ghostClass = null;
289
+ this.chosenClass = null;
290
+ this.dragClass = null;
291
+ this.filter = null;
292
+ this.easing = null;
293
+ this.forceFallback = false;
294
+ this.swapThreshold = 1;
295
+ this.cloneable = false;
296
+ this.listClass = "";
297
+ this.itemClass = "";
298
+ this.itemStyle = null;
299
+ this._liftedIndex = signal(null);
300
+ this._ariaLiveText = signal("");
301
+ this.__rozieFirstUpdateDone = false;
302
+ this._hasSlotHeader = false;
303
+ this._hasSlotDefault = false;
304
+ this._hasSlotFooter = false;
305
+ this._disconnectCleanups = [];
306
+ this._rozieTornDown = false;
307
+ this._rozieReconcileSeq = 0;
308
+ this.instance = null;
309
+ this.__rowKey = {
310
+ map: /* @__PURE__ */ new WeakMap(),
311
+ seq: 0
312
+ };
313
+ this.keyFor = (item, index) => {
314
+ if (typeof this.itemKey === "function") return this.itemKey(item, index);
315
+ if (typeof this.itemKey === "string" && item !== null && typeof item === "object" && item[this.itemKey] != null) return item[this.itemKey];
316
+ if (item !== null && typeof item === "object" || typeof item === "function") {
317
+ if (!this.__rowKey.map.has(item)) this.__rowKey.map.set(item, "__rk" + this.__rowKey.seq++);
318
+ return this.__rowKey.map.get(item);
319
+ }
320
+ return index;
321
+ };
322
+ this.itemClassFor = (item, index) => {
323
+ const v = this.itemClass;
324
+ return typeof v === "function" ? v(item, index) : v;
325
+ };
326
+ this.itemStyleFor = (item, index) => {
327
+ const s = typeof this.itemStyle === "function" ? this.itemStyle(item, index) : this.itemStyle;
328
+ return s == null || s === "" ? null : s;
329
+ };
330
+ this.getLabel = (idx) => {
331
+ const item = this.items[idx];
332
+ if (this.labelFor !== null) return this.labelFor(item, idx);
333
+ if (item !== null && typeof item === "object" && "label" in item) return item.label;
334
+ return String(item);
335
+ };
336
+ this.keyboardEnabled = () => !this.disabled && !this.disableKeyboard;
337
+ this.onRowKeyDown = ($event, index) => {
338
+ if (!this.keyboardEnabled()) return;
339
+ const key = $event.key;
340
+ if (key === " " || key === "Spacebar" || key === "Enter") {
341
+ $event.preventDefault();
342
+ if (this._liftedIndex.value === null) {
343
+ this._liftedIndex.value = index;
344
+ this._ariaLiveText.value = "Lifted " + this.getLabel(index);
345
+ return;
346
+ }
347
+ const dropped = this.getLabel(this._liftedIndex.value);
348
+ const at = this._liftedIndex.value;
349
+ this._liftedIndex.value = null;
350
+ this._ariaLiveText.value = "Dropped " + dropped + " at position " + (at + 1);
351
+ return;
352
+ }
353
+ if (key === "Escape") {
354
+ if (this._liftedIndex.value === null) return;
355
+ $event.preventDefault();
356
+ const cancelled = this.getLabel(this._liftedIndex.value);
357
+ this._liftedIndex.value = null;
358
+ this._ariaLiveText.value = "Cancelled lift of " + cancelled;
359
+ return;
360
+ }
361
+ if (key === "ArrowDown" || key === "ArrowUp") {
362
+ if (this._liftedIndex.value === null) return;
363
+ $event.preventDefault();
364
+ const dir = key === "ArrowDown" ? 1 : -1;
365
+ const from = this._liftedIndex.value;
366
+ const to = from + dir;
367
+ if (to < 0 || to >= this.items.length) return;
368
+ const next = [...this.items];
369
+ const [moved] = next.splice(from, 1);
370
+ next.splice(to, 0, moved);
371
+ this._itemsControllable.write(next);
372
+ this._liftedIndex.value = to;
373
+ this._ariaLiveText.value = "Moved " + this.getLabel(to) + " to position " + (to + 1);
374
+ queueMicrotask(() => (this.renderRoot.querySelectorAll("[role=\"listitem\"]")?.[to])?.focus?.());
375
+ this.dispatchEvent(new CustomEvent("change", {
376
+ detail: {
377
+ oldIndex: from,
378
+ newIndex: to,
379
+ item: moved
380
+ },
381
+ bubbles: true,
382
+ composed: true
383
+ }));
384
+ }
385
+ };
386
+ }
387
+ static {
388
+ this.styles = css`
389
+ .rozie-sortable-wrap[data-rozie-s-0af24eae] { display: block; }
390
+ .rozie-sortable-list[data-rozie-s-0af24eae] { display: block; }
391
+ .rozie-sortable-item[data-rozie-s-0af24eae] { display: block; outline: none; }
392
+ .rozie-sortable-item[data-rozie-s-0af24eae]:focus { outline: 2px solid rgba(0, 102, 204, 0.6); outline-offset: -2px; }
393
+ .rozie-sortable-item-lifted[data-rozie-s-0af24eae] {
394
+ background: rgba(0, 102, 204, 0.08);
395
+ box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.4) inset;
396
+ }
397
+ .rozie-sortable-aria-live[data-rozie-s-0af24eae] {
398
+ position: absolute;
399
+ width: 1px;
400
+ height: 1px;
401
+ padding: 0;
402
+ margin: -1px;
403
+ overflow: hidden;
404
+ clip: rect(0, 0, 0, 0);
405
+ white-space: nowrap;
406
+ border: 0;
407
+ }
408
+ `;
409
+ }
410
+ _armListeners() {
411
+ {
412
+ const slotEl = this.shadowRoot?.querySelector("slot[name=\"header\"]");
413
+ if (slotEl !== null && slotEl !== void 0) {
414
+ const update = () => {
415
+ this._hasSlotHeader = this._slotHeaderElements.length > 0;
416
+ };
417
+ slotEl.addEventListener("slotchange", update);
418
+ this._disconnectCleanups.push(() => slotEl.removeEventListener("slotchange", update));
419
+ update();
420
+ }
421
+ }
422
+ {
423
+ const slotEl = this.shadowRoot?.querySelector("slot:not([name])");
424
+ if (slotEl !== null && slotEl !== void 0) {
425
+ const update = () => {
426
+ this._hasSlotDefault = this._slotDefaultElements.length > 0;
427
+ };
428
+ slotEl.addEventListener("slotchange", update);
429
+ this._disconnectCleanups.push(() => slotEl.removeEventListener("slotchange", update));
430
+ update();
431
+ }
432
+ }
433
+ {
434
+ const slotEl = this.shadowRoot?.querySelector("slot[name=\"footer\"]");
435
+ if (slotEl !== null && slotEl !== void 0) {
436
+ const update = () => {
437
+ this._hasSlotFooter = this._slotFooterElements.length > 0;
438
+ };
439
+ slotEl.addEventListener("slotchange", update);
440
+ this._disconnectCleanups.push(() => slotEl.removeEventListener("slotchange", update));
441
+ update();
442
+ }
443
+ }
444
+ }
445
+ connectedCallback() {
446
+ this._hasSlotHeader = Array.from(this.children).some((el) => el.getAttribute("slot") === "header");
447
+ this._hasSlotDefault = Array.from(this.children).some((el) => !el.hasAttribute("slot") && (el.nodeType !== 3 || (el.textContent?.trim().length ?? 0) > 0));
448
+ this._hasSlotFooter = Array.from(this.children).some((el) => el.getAttribute("slot") === "footer");
449
+ super.connectedCallback();
450
+ if (this.hasUpdated && this._rozieTornDown) {
451
+ this._rozieTornDown = false;
452
+ this._armListeners();
453
+ }
454
+ }
455
+ firstUpdated() {
456
+ this._armListeners();
457
+ this._disconnectCleanups.push((() => this.instance?.destroy()));
458
+ const sortable = useSortableJS(this._refListEl, {
459
+ items: () => this.items,
460
+ onCommit: (next) => {
461
+ this._itemsControllable.write(next);
462
+ },
463
+ options: {
464
+ animation: this.animation,
465
+ disabled: this.disabled,
466
+ group: this.cloneable && typeof this.group === "string" ? {
467
+ name: this.group,
468
+ pull: "clone",
469
+ put: true
470
+ } : this.group,
471
+ handle: this.handle,
472
+ ghostClass: this.ghostClass,
473
+ chosenClass: this.chosenClass,
474
+ dragClass: this.dragClass,
475
+ filter: this.filter,
476
+ forceFallback: this.forceFallback,
477
+ swapThreshold: this.swapThreshold,
478
+ easing: this.easing,
479
+ ...this.options
480
+ },
481
+ afterCommit: () => __rozieReconcileAfterDomMutation(this),
482
+ onChange: ({ kind, oldIndex, newIndex, item }) => {
483
+ if (kind === "reorder") this.dispatchEvent(new CustomEvent("change", {
484
+ detail: {
485
+ oldIndex,
486
+ newIndex,
487
+ item
488
+ },
489
+ bubbles: true,
490
+ composed: true
491
+ }));
492
+ else if (kind === "add") this.dispatchEvent(new CustomEvent("add", {
493
+ detail: {
494
+ newIndex,
495
+ item
496
+ },
497
+ bubbles: true,
498
+ composed: true
499
+ }));
500
+ else if (kind === "remove") this.dispatchEvent(new CustomEvent("remove", {
501
+ detail: {
502
+ oldIndex,
503
+ item
504
+ },
505
+ bubbles: true,
506
+ composed: true
507
+ }));
508
+ },
509
+ onStart: (e) => this.dispatchEvent(new CustomEvent("start", {
510
+ detail: e,
511
+ bubbles: true,
512
+ composed: true
513
+ })),
514
+ onEnd: (e) => this.dispatchEvent(new CustomEvent("end", {
515
+ detail: e,
516
+ bubbles: true,
517
+ composed: true
518
+ }))
519
+ });
520
+ this.instance = sortable.instance;
521
+ }
522
+ updated(changedProperties) {
523
+ if (this.__rozieFirstUpdateDone && changedProperties.has("disabled")) ((v) => this.instance?.option("disabled", v))(this.disabled);
524
+ if (this.__rozieFirstUpdateDone && changedProperties.has("group")) ((v) => this.instance?.option("group", v))(this.group);
525
+ if (this.__rozieFirstUpdateDone && changedProperties.has("handle")) ((v) => this.instance?.option("handle", v))(this.handle);
526
+ if (this.__rozieFirstUpdateDone && changedProperties.has("ghostClass")) ((v) => this.instance?.option("ghostClass", v))(this.ghostClass);
527
+ if (this.__rozieFirstUpdateDone && changedProperties.has("chosenClass")) ((v) => this.instance?.option("chosenClass", v))(this.chosenClass);
528
+ if (this.__rozieFirstUpdateDone && changedProperties.has("dragClass")) ((v) => this.instance?.option("dragClass", v))(this.dragClass);
529
+ if (this.__rozieFirstUpdateDone && changedProperties.has("filter")) ((v) => this.instance?.option("filter", v))(this.filter);
530
+ if (this.__rozieFirstUpdateDone && changedProperties.has("easing")) ((v) => this.instance?.option("easing", v))(this.easing);
531
+ this.__rozieFirstUpdateDone = true;
532
+ }
533
+ disconnectedCallback() {
534
+ super.disconnectedCallback();
535
+ queueMicrotask(() => {
536
+ if (this.isConnected || this._rozieTornDown) return;
537
+ this._rozieTornDown = true;
538
+ for (const fn of this._disconnectCleanups) fn();
539
+ this._disconnectCleanups = [];
540
+ });
541
+ }
542
+ attributeChangedCallback(name, old, value) {
543
+ super.attributeChangedCallback(name, old, value);
544
+ if (name === "items") this._itemsControllable.notifyAttributeChange(value);
545
+ }
546
+ render() {
547
+ return html`
548
+ <div class="rozie-sortable-wrap" ${rozieSpread(this.$attrs)} ${rozieListeners(this.$listeners)} data-rozie-ref="__rozieRoot" data-rozie-s-0af24eae>
549
+ <div class="${rozieClass(["rozie-sortable-list", this.listClass])}" part="list" data-rozie-ref="listEl" data-rozie-s-0af24eae>${keyed(this._rozieReconcileSeq ?? 0, html`
550
+ <slot name="header"></slot>
551
+ ${repeat(this.items, (item, index) => this.keyFor(item, index), (item, index) => html`<div class="${rozieClass([
552
+ "rozie-sortable-item",
553
+ this.itemClassFor(item, index),
554
+ { "rozie-sortable-item-lifted": this._liftedIndex.value === index }
555
+ ])}" key=${rozieAttr(this.keyFor(item, index))} style=${rozieStyle(this.itemStyleFor(item, index))} data-id=${rozieAttr(this.keyFor(item, index))} role="listitem" tabindex=${rozieAttr(this.keyboardEnabled() ? 0 : null)} @keydown=${($event) => {
556
+ this.onRowKeyDown($event, index);
557
+ }} data-rozie-s-0af24eae>
558
+ ${this.__rozieDefaultSlot__ !== void 0 ? this.__rozieDefaultSlot__({
559
+ item,
560
+ index
561
+ }) : html`<slot data-rozie-params=${(() => {
562
+ try {
563
+ return JSON.stringify({
564
+ item,
565
+ index
566
+ });
567
+ } catch {
568
+ return "{}";
569
+ }
570
+ })()}></slot>`}
571
+ </div>`)}
572
+ <slot name="footer"></slot>
573
+ `)}</div>
574
+ <div class="rozie-sortable-aria-live" data-rozie-sortable-aria-live="" aria-live="polite" aria-atomic="true" data-rozie-s-0af24eae>${this._ariaLiveText.value}</div>
575
+ </div>
576
+ `;
577
+ }
578
+ getInstance() {
579
+ return this.instance;
580
+ }
581
+ toArray() {
582
+ return this.instance ? this.instance.toArray() : [];
583
+ }
584
+ sort(order, useAnimation = true) {
585
+ this.instance?.sort(order, useAnimation);
586
+ }
587
+ option(name, value) {
588
+ if (!this.instance) return void 0;
589
+ if (value === void 0) return this.instance.option(name);
590
+ this.instance.option(name, value);
591
+ return value;
592
+ }
593
+ get items() {
594
+ return this._itemsControllable.read();
595
+ }
596
+ set items(v) {
597
+ this._itemsControllable.notifyPropertyWrite(v);
598
+ }
599
+ /**
600
+ * Plan 14-05 — cross-framework attribute fallthrough source. Reads the
601
+ * host custom element's attributes on each call so a consumer-side bound
602
+ * attribute flows through on every render. The `rozieSpread` directive
603
+ * (D-02) does the cross-render diff downstream.
604
+ *
605
+ * Phase 15 follow-up Bug A — declared-prop attribute names are filtered
606
+ * out so `$attrs` returns "rest after declared props" (semantic parity
607
+ * with React/Vue/Svelte/Solid/Angular). Both Lit attribute-naming
608
+ * forms are folded into the skip set: kebab-case for model props
609
+ * (explicit `attribute:`) AND lowercased property name (Lit's default).
610
+ */
611
+ get $attrs() {
612
+ const __skip = new Set([
613
+ "items",
614
+ "item-key",
615
+ "itemkey",
616
+ "handle",
617
+ "group",
618
+ "animation",
619
+ "disabled",
620
+ "disable-keyboard",
621
+ "disablekeyboard",
622
+ "options",
623
+ "label-for",
624
+ "labelfor",
625
+ "ghost-class",
626
+ "ghostclass",
627
+ "chosen-class",
628
+ "chosenclass",
629
+ "drag-class",
630
+ "dragclass",
631
+ "filter",
632
+ "easing",
633
+ "force-fallback",
634
+ "forcefallback",
635
+ "swap-threshold",
636
+ "swapthreshold",
637
+ "cloneable",
638
+ "list-class",
639
+ "listclass",
640
+ "item-class",
641
+ "itemclass",
642
+ "item-style",
643
+ "itemstyle"
644
+ ]);
645
+ const out = {};
646
+ for (const a of Array.from(this.attributes)) {
647
+ if (__skip.has(a.name)) continue;
648
+ out[a.name] = a.value;
649
+ }
650
+ return out;
651
+ }
652
+ /**
653
+ * Phase 15 D-19 — consumer-passed listener cluster placeholder.
654
+ * Lit attaches event listeners directly on the host element via
655
+ * `addEventListener` (no per-instance prop rest binding), so the
656
+ * runtime value is undefined; the `rozieListeners` directive's
657
+ * nullish coercion (`obj ?? {}`) handles the no-op cleanly.
658
+ * The declaration exists to satisfy `tsc --noEmit` on consumer
659
+ * projects with strict mode — bare `$listeners` in `render()`
660
+ * would otherwise raise TS2304 (Cannot find name).
661
+ */
662
+ get $listeners() {}
663
+ };
664
+ __decorate([property({
665
+ type: Array,
666
+ attribute: "items"
667
+ })], SortableList.prototype, "_items_attr", void 0);
668
+ __decorate([property({ type: String })], SortableList.prototype, "itemKey", void 0);
669
+ __decorate([property({
670
+ type: String,
671
+ reflect: true
672
+ })], SortableList.prototype, "handle", void 0);
673
+ __decorate([property({
674
+ type: String,
675
+ reflect: true
676
+ })], SortableList.prototype, "group", void 0);
677
+ __decorate([property({
678
+ type: Number,
679
+ reflect: true
680
+ })], SortableList.prototype, "animation", void 0);
681
+ __decorate([property({
682
+ type: Boolean,
683
+ reflect: true
684
+ })], SortableList.prototype, "disabled", void 0);
685
+ __decorate([property({
686
+ type: Boolean,
687
+ reflect: true
688
+ })], SortableList.prototype, "disableKeyboard", void 0);
689
+ __decorate([property({ type: Object })], SortableList.prototype, "options", void 0);
690
+ __decorate([property({ type: Function })], SortableList.prototype, "labelFor", void 0);
691
+ __decorate([property({
692
+ type: String,
693
+ reflect: true
694
+ })], SortableList.prototype, "ghostClass", void 0);
695
+ __decorate([property({
696
+ type: String,
697
+ reflect: true
698
+ })], SortableList.prototype, "chosenClass", void 0);
699
+ __decorate([property({
700
+ type: String,
701
+ reflect: true
702
+ })], SortableList.prototype, "dragClass", void 0);
703
+ __decorate([property({
704
+ type: String,
705
+ reflect: true
706
+ })], SortableList.prototype, "filter", void 0);
707
+ __decorate([property({
708
+ type: String,
709
+ reflect: true
710
+ })], SortableList.prototype, "easing", void 0);
711
+ __decorate([property({
712
+ type: Boolean,
713
+ reflect: true
714
+ })], SortableList.prototype, "forceFallback", void 0);
715
+ __decorate([property({
716
+ type: Number,
717
+ reflect: true
718
+ })], SortableList.prototype, "swapThreshold", void 0);
719
+ __decorate([property({
720
+ type: Boolean,
721
+ reflect: true
722
+ })], SortableList.prototype, "cloneable", void 0);
723
+ __decorate([property({ type: String })], SortableList.prototype, "listClass", void 0);
724
+ __decorate([property({ type: String })], SortableList.prototype, "itemClass", void 0);
725
+ __decorate([property({ type: String })], SortableList.prototype, "itemStyle", void 0);
726
+ __decorate([query("[data-rozie-ref=\"listEl\"]")], SortableList.prototype, "_refListEl", void 0);
727
+ __decorate([query("[data-rozie-ref=\"__rozieRoot\"]")], SortableList.prototype, "_ref__rozieRoot", void 0);
728
+ __decorate([state()], SortableList.prototype, "_hasSlotHeader", void 0);
729
+ __decorate([queryAssignedElements({
730
+ slot: "header",
731
+ flatten: true
732
+ })], SortableList.prototype, "_slotHeaderElements", void 0);
733
+ __decorate([state()], SortableList.prototype, "_hasSlotDefault", void 0);
734
+ __decorate([queryAssignedElements({ flatten: true })], SortableList.prototype, "_slotDefaultElements", void 0);
735
+ __decorate([property({ attribute: false })], SortableList.prototype, "__rozieDefaultSlot__", void 0);
736
+ __decorate([state()], SortableList.prototype, "_hasSlotFooter", void 0);
737
+ __decorate([queryAssignedElements({
738
+ slot: "footer",
739
+ flatten: true
740
+ })], SortableList.prototype, "_slotFooterElements", void 0);
741
+ SortableList = __decorate([customElement("rozie-sortable-list")], SortableList);
742
+ var SortableList_default = SortableList;
743
+ //#endregion
744
+ export { SortableList_default as SortableList, SortableList_default as default };