@tillsc/progressive-web-components 0.1.0 → 0.1.2

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.
Files changed (57) hide show
  1. package/README.md +5 -2
  2. package/dist/all-bs5.js +212 -0
  3. package/dist/all.js +212 -0
  4. package/dist/zone-transfer.js +315 -0
  5. package/package.json +5 -5
  6. package/src/core/pwc-children-observer-element.js +53 -0
  7. package/src/core/pwc-element.js +76 -0
  8. package/src/core/pwc-sentinel-init-element.js +56 -0
  9. package/src/core/pwc-simple-init-element.js +25 -0
  10. package/src/core/utils.js +9 -0
  11. package/src/dialog-opener/INTERNALS.md +47 -0
  12. package/src/dialog-opener/README.md +145 -0
  13. package/src/dialog-opener/base.js +226 -0
  14. package/src/dialog-opener/bs5/dialog-opener.js +73 -0
  15. package/src/dialog-opener/bs5/index.js +10 -0
  16. package/src/dialog-opener/dialog-opener.css +21 -0
  17. package/src/dialog-opener/dialog-opener.js +41 -0
  18. package/src/dialog-opener/index.html +24 -0
  19. package/src/dialog-opener/index.js +12 -0
  20. package/src/dialog-opener/test/basic.html +109 -0
  21. package/src/dialog-opener/test/bs5-basic.html +100 -0
  22. package/src/dialog-opener/test/bs5-iframe-target.html +41 -0
  23. package/src/dialog-opener/test/iframe-target.html +28 -0
  24. package/src/index-bs5.js +5 -0
  25. package/src/index.js +4 -0
  26. package/src/modal-dialog/INTERNALS.md +55 -0
  27. package/src/modal-dialog/README.md +139 -0
  28. package/src/modal-dialog/base.js +117 -0
  29. package/src/modal-dialog/bs5/index.js +7 -0
  30. package/src/modal-dialog/bs5/modal-dialog.js +109 -0
  31. package/src/modal-dialog/index.html +24 -0
  32. package/src/modal-dialog/index.js +9 -0
  33. package/src/modal-dialog/modal-dialog.css +103 -0
  34. package/src/modal-dialog/modal-dialog.js +97 -0
  35. package/src/modal-dialog/test/basic.html +84 -0
  36. package/src/modal-dialog/test/bs5-basic.html +123 -0
  37. package/src/multiselect-dual-list/INTERNALS.md +101 -0
  38. package/src/multiselect-dual-list/README.md +191 -0
  39. package/src/multiselect-dual-list/base.js +215 -0
  40. package/src/multiselect-dual-list/bs5/index.js +10 -0
  41. package/src/multiselect-dual-list/bs5/multiselect-dual-list.js +103 -0
  42. package/src/multiselect-dual-list/index.html +26 -0
  43. package/src/multiselect-dual-list/index.js +9 -0
  44. package/src/multiselect-dual-list/multiselect-dual-list.css +123 -0
  45. package/src/multiselect-dual-list/multiselect-dual-list.js +100 -0
  46. package/src/multiselect-dual-list/test/basic.html +115 -0
  47. package/src/multiselect-dual-list/test/bs5-basic.html +106 -0
  48. package/src/multiselect-dual-list/test/dynamic-options.html +70 -0
  49. package/src/multiselect-dual-list/test/filter-api.html +66 -0
  50. package/src/zone-transfer/INTERNALS.md +80 -0
  51. package/src/zone-transfer/README.md +166 -0
  52. package/src/zone-transfer/index.html +24 -0
  53. package/src/zone-transfer/index.js +9 -0
  54. package/src/zone-transfer/test/basic.html +78 -0
  55. package/src/zone-transfer/test/keyboard.html +111 -0
  56. package/src/zone-transfer/zone-transfer.css +12 -0
  57. package/src/zone-transfer/zone-transfer.js +292 -0
@@ -0,0 +1,115 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>multiselect-dual-list: basic</title>
6
+ <script type="module" src="../../../dist/multiselect-dual-list.js"></script>
7
+ </head>
8
+ <body>
9
+ <pwc-multiselect-dual-list id="demo" available-label="Available" selected-label="Selected">
10
+ <select multiple name="items">
11
+ <option value="1">Root A</option>
12
+ <option value="2" data-parent="1">Child A1</option>
13
+ <option value="3" data-parent="1">Child A2</option>
14
+ <option value="4" selected>Pre-selected</option>
15
+ <option value="5" disabled>Disabled Item</option>
16
+ <option value="6">Root B</option>
17
+ </select>
18
+ </pwc-multiselect-dual-list>
19
+
20
+ <script type="module">
21
+ import { run } from "../../../static/test-harness.js";
22
+
23
+ run(async ({ assert, equal, waitFor, log }) => {
24
+ const host = document.getElementById("demo");
25
+
26
+ log("waiting for UI to build");
27
+ await waitFor(
28
+ () => host.querySelector(".pwc-msdl-container"),
29
+ { label: "container rendered" }
30
+ );
31
+
32
+ const container = host.querySelector(".pwc-msdl-container");
33
+ assert(container, "container exists");
34
+
35
+ // Check select is hidden
36
+ const select = host.querySelector("select");
37
+ equal(select.style.display, "none", "select is hidden");
38
+
39
+ // Check available list has all items
40
+ const availableItems = container.querySelectorAll(".pwc-msdl-available [data-value]");
41
+ equal(availableItems.length, 6, "6 available items rendered");
42
+
43
+ // Check pre-selected item is in selected list
44
+ const selectedItems = container.querySelectorAll(".pwc-msdl-selected [data-value]");
45
+ equal(selectedItems.length, 1, "1 pre-selected item");
46
+ equal(selectedItems[0].dataset.value, "4", "pre-selected item has value 4");
47
+
48
+ // Check disabled item has no add button
49
+ const disabledItem = container.querySelector("[data-value='5']");
50
+ assert(disabledItem.classList.contains("pwc-msdl-item--disabled"), "disabled item has disabled class");
51
+ assert(!disabledItem.querySelector("[data-action='add']"), "disabled item has no add button");
52
+
53
+ // Check hierarchy indentation
54
+ const child = container.querySelector(".pwc-msdl-available [data-value='2']");
55
+ assert(child.style.paddingLeft, "child item has indentation");
56
+
57
+ // Test adding an item
58
+ log("adding item via click");
59
+ const addBtn = container.querySelector(".pwc-msdl-available [data-value='1'] [data-action='add']");
60
+ addBtn.click();
61
+
62
+ await waitFor(
63
+ () => container.querySelectorAll(".pwc-msdl-selected [data-value]").length === 2,
64
+ { label: "item added to selected" }
65
+ );
66
+ equal(select.querySelector("option[value='1']").selected, true, "option 1 is selected in native select");
67
+
68
+ // Available entry should be marked as selected
69
+ const availEntry = container.querySelector(".pwc-msdl-available [data-value='1']");
70
+ assert(availEntry.classList.contains("pwc-msdl-item--selected"), "available entry marked as selected");
71
+
72
+ // Test removing an item
73
+ log("removing item via click");
74
+ const removeBtn = container.querySelector(".pwc-msdl-selected [data-value='4'] [data-action='remove']");
75
+ removeBtn.click();
76
+
77
+ await waitFor(
78
+ () => container.querySelectorAll(".pwc-msdl-selected [data-value]").length === 1,
79
+ { label: "item removed from selected" }
80
+ );
81
+ equal(select.querySelector("option[value='4']").selected, false, "option 4 is deselected in native select");
82
+
83
+ // Test filter
84
+ log("testing filter");
85
+ const filterInput = container.querySelector(".pwc-msdl-filter");
86
+ filterInput.value = "Root";
87
+ filterInput.dispatchEvent(new Event("input", { bubbles: true }));
88
+
89
+ await waitFor(
90
+ () => {
91
+ const visible = Array.from(container.querySelectorAll(".pwc-msdl-available [data-value]"))
92
+ .filter((el) => el.style.display !== "none");
93
+ return visible.length === 2; // Root A + Root B
94
+ },
95
+ { label: "filter shows only matching items" }
96
+ );
97
+
98
+ // Clear filter
99
+ filterInput.value = "";
100
+ filterInput.dispatchEvent(new Event("input", { bubbles: true }));
101
+
102
+ await waitFor(
103
+ () => {
104
+ const visible = Array.from(container.querySelectorAll(".pwc-msdl-available [data-value]"))
105
+ .filter((el) => el.style.display !== "none");
106
+ return visible.length === 6;
107
+ },
108
+ { label: "clearing filter shows all items" }
109
+ );
110
+
111
+ log("done");
112
+ });
113
+ </script>
114
+ </body>
115
+ </html>
@@ -0,0 +1,106 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>multiselect-dual-list: bs5-basic</title>
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />
7
+ <script type="module" src="../../../dist/multiselect-dual-list-bs5.js"></script>
8
+ </head>
9
+ <body class="p-3">
10
+ <pwc-multiselect-dual-list-bs5 id="demo" available-label="Available" selected-label="Selected">
11
+ <select multiple name="items">
12
+ <option value="1">Root A</option>
13
+ <option value="2" data-parent="1">Child A1</option>
14
+ <option value="3" selected>Pre-selected</option>
15
+ <option value="4" disabled>Disabled</option>
16
+ </select>
17
+ </pwc-multiselect-dual-list-bs5>
18
+
19
+ <script type="module">
20
+ import { run } from "../../../static/test-harness.js";
21
+
22
+ run(async ({ assert, equal, waitFor, log }) => {
23
+ const host = document.getElementById("demo");
24
+
25
+ log("waiting for UI to build");
26
+ await waitFor(
27
+ () => host.querySelector(".row"),
28
+ { label: "container rendered" }
29
+ );
30
+
31
+ const container = host.querySelector(".row");
32
+ assert(container, "container exists");
33
+
34
+ // Check select is hidden
35
+ const select = host.querySelector("select");
36
+ equal(select.style.display, "none", "select is hidden");
37
+
38
+ // Get columns: first = selected, last = available
39
+ const cols = container.querySelectorAll(".col");
40
+ const selectedCol = cols[0];
41
+ const availableCol = cols[1];
42
+
43
+ // Check list-group items rendered
44
+ const available = availableCol.querySelectorAll(".list-group [data-value]");
45
+ equal(available.length, 4, "4 available items rendered");
46
+ const selectedItems = selectedCol.querySelectorAll(".list-group [data-value]");
47
+ equal(selectedItems.length, 1, "1 pre-selected item");
48
+ equal(selectedItems[0].dataset.value, "3", "pre-selected item has value 3");
49
+
50
+ // Test adding an item
51
+ log("adding item via click");
52
+ const addBtn = container.querySelector("[data-value='1'] [data-action='add']");
53
+ addBtn.click();
54
+
55
+ await waitFor(
56
+ () => selectedCol.querySelectorAll(".list-group [data-value]").length === 2,
57
+ { label: "item added to selected" }
58
+ );
59
+ equal(select.querySelector("option[value='1']").selected, true, "option 1 is selected");
60
+
61
+ // Test removing an item
62
+ log("removing item via click");
63
+ const removeBtn = selectedCol.querySelector("[data-value='3'] [data-action='remove']");
64
+ removeBtn.click();
65
+
66
+ await waitFor(
67
+ () => selectedCol.querySelectorAll(".list-group [data-value]").length === 1,
68
+ { label: "item removed from selected" }
69
+ );
70
+ equal(select.querySelector("option[value='3']").selected, false, "option 3 deselected");
71
+
72
+ // Test filter
73
+ log("testing filter");
74
+ const filterInput = availableCol.querySelector("input[type='search']");
75
+ assert(filterInput, "filter input exists");
76
+
77
+ filterInput.value = "Root";
78
+ filterInput.dispatchEvent(new Event("input", { bubbles: true }));
79
+
80
+ await waitFor(
81
+ () => {
82
+ const visible = Array.from(availableCol.querySelectorAll(".list-group [data-value]"))
83
+ .filter((el) => !el.classList.contains("d-none"));
84
+ return visible.length === 1; // Root A
85
+ },
86
+ { label: "filter shows only matching items" }
87
+ );
88
+
89
+ // Clear filter
90
+ filterInput.value = "";
91
+ filterInput.dispatchEvent(new Event("input", { bubbles: true }));
92
+
93
+ await waitFor(
94
+ () => {
95
+ const visible = Array.from(availableCol.querySelectorAll(".list-group [data-value]"))
96
+ .filter((el) => !el.classList.contains("d-none"));
97
+ return visible.length === 4;
98
+ },
99
+ { label: "clearing filter shows all items" }
100
+ );
101
+
102
+ log("done");
103
+ });
104
+ </script>
105
+ </body>
106
+ </html>
@@ -0,0 +1,70 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>multiselect-dual-list: dynamic options (chunking)</title>
6
+ <script type="module" src="../../../dist/multiselect-dual-list.js"></script>
7
+ </head>
8
+ <body>
9
+ <pwc-multiselect-dual-list id="demo" available-label="Available" selected-label="Selected">
10
+ <select multiple name="items">
11
+ <option value="a1">Alpha</option>
12
+ <option value="a2" selected>Bravo</option>
13
+ <option value="a3">Charlie</option>
14
+ <option value="a4">Delta</option>
15
+ </select>
16
+ </pwc-multiselect-dual-list>
17
+
18
+ <script type="module">
19
+ import { run } from "../../../static/test-harness.js";
20
+
21
+ run(async ({ assert, equal, waitFor, log }) => {
22
+ const host = document.getElementById("demo");
23
+ await waitFor(() => host.querySelector(".pwc-msdl-container"), { label: "container rendered" });
24
+
25
+ const container = host.querySelector(".pwc-msdl-container");
26
+ const select = host.querySelector("select");
27
+
28
+ // Modify selection before chunking: select a1, deselect a2
29
+ log("modifying selection before chunking");
30
+ container.querySelector("[data-value='a1'] [data-action='add']").click();
31
+ container.querySelector(".pwc-msdl-selected [data-value='a2'] [data-action='remove']").click();
32
+ await waitFor(
33
+ () => container.querySelectorAll(".pwc-msdl-selected [data-value]").length === 1,
34
+ { label: "selection modified" }
35
+ );
36
+
37
+ // Simulate chunking: append new options
38
+ log("appending new options");
39
+ for (const [value, text, selected] of [["b1", "Echo", false], ["b2", "Foxtrot", true], ["b3", "Golf", false]]) {
40
+ const el = document.createElement("option");
41
+ el.value = value;
42
+ el.textContent = text;
43
+ el.selected = selected;
44
+ select.appendChild(el);
45
+ }
46
+
47
+ // Wait for observer to rebuild
48
+ await waitFor(
49
+ () => container.querySelectorAll(".pwc-msdl-available [data-value]").length === 7,
50
+ { label: "new options rendered" }
51
+ );
52
+
53
+ // Existing selection preserved
54
+ log("checking selection preserved");
55
+ assert(container.querySelector(".pwc-msdl-selected [data-value='a1']"), "a1 still selected");
56
+ assert(!container.querySelector(".pwc-msdl-selected [data-value='a2']"), "a2 still deselected");
57
+
58
+ // New options placed correctly
59
+ log("checking new options placement");
60
+ assert(!container.querySelector(".pwc-msdl-selected [data-value='b1']"), "b1 not selected");
61
+ assert(container.querySelector(".pwc-msdl-selected [data-value='b2']"), "b2 selected");
62
+ assert(!container.querySelector(".pwc-msdl-selected [data-value='b3']"), "b3 not selected");
63
+
64
+ equal(container.querySelectorAll(".pwc-msdl-selected [data-value]").length, 2, "2 selected (a1 + b2)");
65
+
66
+ log("done");
67
+ });
68
+ </script>
69
+ </body>
70
+ </html>
@@ -0,0 +1,66 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>multiselect-dual-list: filter API</title>
6
+ <script type="module" src="../../../dist/multiselect-dual-list.js"></script>
7
+ </head>
8
+ <body>
9
+ <pwc-multiselect-dual-list id="demo" available-label="Available" selected-label="Selected">
10
+ <select multiple name="items">
11
+ <option value="1">Alpha</option>
12
+ <option value="2">Bravo</option>
13
+ <option value="3">Charlie</option>
14
+ <option value="4" selected>Delta</option>
15
+ </select>
16
+ </pwc-multiselect-dual-list>
17
+
18
+ <script type="module">
19
+ import { run } from "../../../static/test-harness.js";
20
+
21
+ run(async ({ assert, equal, waitFor, log }) => {
22
+ const host = document.getElementById("demo");
23
+ await waitFor(() => host.querySelector(".pwc-msdl-container"), { label: "container rendered" });
24
+
25
+ let lastEvent = null;
26
+ host.addEventListener("pwc-multiselect-dual-list:filter", (e) => { lastEvent = e; });
27
+
28
+ // --- filterText getter/setter + event ---
29
+ log("filterText property");
30
+ equal(host.filterText, "", "filterText is empty initially");
31
+
32
+ host.filterText = "alpha";
33
+ equal(host.filterText, "alpha", "getter returns set value");
34
+ assert(lastEvent, "event dispatched on set");
35
+ equal(lastEvent.detail.filterText, "alpha", "detail.filterText");
36
+ equal(lastEvent.detail.matchCount, 1, "matchCount 1");
37
+ equal(lastEvent.detail.totalCount, 4, "totalCount 4");
38
+
39
+ // --- matchCount = 0 ---
40
+ log("no-match case");
41
+ host.filterText = "zzz-no-match";
42
+ equal(lastEvent.detail.matchCount, 0, "matchCount 0 for no-match");
43
+
44
+ // --- Filter re-applied after dynamic option change ---
45
+ log("filter persistence across rebuild");
46
+ host.filterText = "alpha";
47
+
48
+ const select = host.querySelector("select");
49
+ const opt = document.createElement("option");
50
+ opt.value = "5";
51
+ opt.textContent = "Alpha Two";
52
+ select.appendChild(opt);
53
+
54
+ await waitFor(
55
+ () => host.querySelectorAll(".pwc-msdl-available [data-value]").length === 5,
56
+ { label: "new option rendered" }
57
+ );
58
+
59
+ equal(lastEvent.detail.matchCount, 2, "matchCount 2 after adding Alpha Two");
60
+ equal(lastEvent.detail.totalCount, 5, "totalCount 5 after rebuild");
61
+
62
+ log("done");
63
+ });
64
+ </script>
65
+ </body>
66
+ </html>
@@ -0,0 +1,80 @@
1
+ # Zone-Transfer — Internals
2
+
3
+ ## Core invariants
4
+
5
+ - The component is **DOM authoritative**: it moves existing nodes. No cloning.
6
+ - “What is a zone / item / handle” is resolved via `closest()` + `this.contains(...)` checks.
7
+ - Cross-component dragging is ignored by design (containment check is the boundary).
8
+
9
+ ## Why `PwcChildrenObserverElement`
10
+
11
+ The component relies on `PwcChildrenObserverElement` (observe mode `tree`) to re-assert required
12
+ attributes when the DOM changes for reasons outside the component:
13
+
14
+ - server-driven re-renders
15
+ - app code inserting/removing items
16
+ - items being moved in from other components
17
+
18
+ The observer work is intentionally small: it only ensures items are draggable and maintains a sane
19
+ roving-tabindex setup for keyboard focus.
20
+
21
+ ## Drag and drop state
22
+
23
+ The component keeps only the minimum transient state in `this._drag`:
24
+
25
+ - `item`: the element being moved
26
+ - `fromZone`: origin zone (element reference)
27
+ - `overMethod`: last computed method (`before` vs `append`), used for event metadata only
28
+
29
+ Everything else (zone membership, ordering) is derived from the current DOM when needed.
30
+
31
+ ## Placeholder strategy
32
+
33
+ The placeholder is a single lightweight `<div aria-hidden="true">` stored as `this._placeholder`.
34
+
35
+ - Created lazily on first drag of an item
36
+ - Height is derived once from the dragged item’s bounding box
37
+ - Repositioned on `dragover`
38
+ - Removed on `drop` and `dragend`
39
+
40
+ Rationale:
41
+ - no duplicated interactive state
42
+ - stable layout feedback without copying node trees
43
+ - minimal mutation surface
44
+
45
+ ## Insertion point algorithm
46
+
47
+ During `dragover`, the component computes a “before” element by comparing the pointer Y position
48
+ to each candidate item’s vertical midpoint (excluding the dragged item and the placeholder).
49
+
50
+ - first item whose midpoint is below the pointer becomes `beforeEl`
51
+ - otherwise we append at the end
52
+
53
+ This keeps ordering intuitive and avoids expensive heuristics.
54
+
55
+ ## Keyboard model
56
+
57
+ Keyboard behavior is intentionally conservative:
58
+
59
+ - Focus movement is roving-tabindex within the current zone.
60
+ - Reorder within a zone is a pure DOM reorder (insert before/after neighbor).
61
+ - Moving across zones only happens if zones declare hotkeys; otherwise there is no “teleport” key.
62
+
63
+ Implementation notes:
64
+ - The handler bails out early for typing contexts (`input`, `textarea`, `select`, `button`, `[contenteditable]`).
65
+ - The keyboard path uses the same `_emitChange(...)` payload generation as drag-and-drop, so consumers
66
+ see one consistent event shape.
67
+
68
+ ## Error handling philosophy
69
+
70
+ No silent swallowing of unexpected errors:
71
+ - DOM operations are expected to work on well-formed markup.
72
+ - If a consumer supplies invalid markup (missing zones, etc.), the component should fail loudly in dev.
73
+
74
+ The only “soft failures” are early returns when resolution fails (e.g., drag start on non-item).
75
+
76
+ ## Testing notes
77
+
78
+ - DnD tests stub `dataTransfer` because it is not reliably constructible across browsers.
79
+ - Keyboard tests should avoid duplicating what the basic DnD test already asserts; focus on:
80
+ roving focus + reorder + hotkey zone moves.
@@ -0,0 +1,166 @@
1
+ # `<pwc-zone-transfer>`
2
+
3
+ Minimal zone-based drag & drop plus sorting between containers ("zones").
4
+
5
+ This component is **server-first** and **markup-driven**:
6
+ - no Shadow DOM
7
+ - no external drag-and-drop library
8
+ - uses the native HTML5 DnD events
9
+
10
+ It is intentionally small: it moves elements between zones and emits a single change event.
11
+
12
+ ---
13
+
14
+ ## Basic usage
15
+
16
+ Zones can be declared either via tag name or via data attributes.
17
+
18
+ ### Custom elements (recommended)
19
+
20
+ ```html
21
+ <pwc-zone-transfer>
22
+ <pwc-zone-transfer-zone name="available">
23
+ <pwc-zone-transfer-item>Alice</pwc-zone-transfer-item>
24
+ <pwc-zone-transfer-item>Bob</pwc-zone-transfer-item>
25
+ </pwc-zone-transfer-zone>
26
+
27
+ <pwc-zone-transfer-zone name="selected"></pwc-zone-transfer-zone>
28
+ </pwc-zone-transfer>
29
+ ```
30
+
31
+ ### Data attributes (works with any markup)
32
+
33
+ ```html
34
+ <pwc-zone-transfer>
35
+ <div data-pwc-zone="available">
36
+ <div data-pwc-item>Alice</div>
37
+ <div data-pwc-item>Bob</div>
38
+ </div>
39
+
40
+ <div data-pwc-zone="selected"></div>
41
+ </pwc-zone-transfer>
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Hooks (selectors)
47
+
48
+ Defaults (can be overridden by subclassing and changing the static selectors):
49
+
50
+ - Zones: `pwc-zone-transfer-zone, [data-pwc-zone]`
51
+ - Items: `pwc-zone-transfer-item, [data-pwc-item]`
52
+ - Handle: `pwc-zone-transfer-handle, [data-pwc-handle]`
53
+
54
+ ---
55
+
56
+ ## Data contracts
57
+
58
+ ### Zone name
59
+
60
+ One of:
61
+ - `<pwc-zone-transfer-zone name="selected">`
62
+ - `data-pwc-zone="selected"`
63
+
64
+ ### Item id (optional)
65
+
66
+ If present, it is included in the change event payload.
67
+
68
+ Supported forms:
69
+ - `data-pwc-item="123"`
70
+ - `id="123"`
71
+ - (for `<pwc-zone-transfer-item>`) `data-id="123"`
72
+
73
+ If none of these are present, `itemId` in the event payload will be an empty string.
74
+
75
+ ---
76
+
77
+ ## Optional handle
78
+
79
+ If an item contains a handle element (matches the handle selector), dragging is only allowed
80
+ when the drag starts on that handle.
81
+
82
+ Example:
83
+
84
+ ```html
85
+ <div data-pwc-item>
86
+ <span data-pwc-handle>⠿</span>
87
+ Alice
88
+ </div>
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Keyboard
94
+
95
+ - ArrowUp / ArrowDown: move focus within the current zone
96
+ - Ctrl+ArrowUp / Ctrl+ArrowDown (Cmd on macOS): reorder the focused item within the zone
97
+ - Optional zone hotkeys: move the focused item to a specific zone
98
+
99
+ ### Zone hotkeys (optional)
100
+
101
+ Assign a key to a zone:
102
+
103
+ ```html
104
+ <pwc-zone-transfer-zone name="selected" data-pwc-zone-key="2"></pwc-zone-transfer-zone>
105
+ ```
106
+
107
+ Pressing that key moves the focused item into that zone.
108
+
109
+ Note:
110
+ - Keyboard-based zone moves are only active if **at least one** zone defines `data-pwc-zone-key`.
111
+
112
+ ---
113
+
114
+ ## Events
115
+
116
+ ### `pwc-zone-transfer:change`
117
+
118
+ Fired after:
119
+ - drag & drop
120
+ - keyboard reorder
121
+ - keyboard move to another zone
122
+
123
+ ```js
124
+ el.addEventListener("pwc-zone-transfer:change", (e) => {
125
+ console.log(e.detail);
126
+ });
127
+ ```
128
+
129
+ Payload (`e.detail`):
130
+
131
+ - `itemId`: string (may be empty)
132
+ - `fromZone`: string
133
+ - `toZone`: string
134
+ - `index`: number (position in the target zone after the move)
135
+ - `method`: `"before"` or `"append"`
136
+
137
+ ---
138
+
139
+ ## Styling
140
+
141
+ This component only adds two CSS hooks:
142
+
143
+ - `.pwc-zone-transfer-dragging` on the dragged item
144
+ - `.pwc-zone-transfer-placeholder` for the drop placeholder element
145
+
146
+ A minimal baseline:
147
+
148
+ ```css
149
+ pwc-zone-transfer .pwc-zone-transfer-placeholder {
150
+ box-sizing: border-box;
151
+ border: 1px dashed currentColor;
152
+ border-radius: 4px;
153
+ opacity: 0.35;
154
+ }
155
+ ```
156
+
157
+ Notes:
158
+ - The placeholder is a lightweight `<div aria-hidden="true">` with a height matching the dragged item.
159
+ - The component does **not** clone items.
160
+
161
+ ---
162
+
163
+ ## Limitations
164
+
165
+ - Native HTML5 DnD works best with mouse/trackpad. Touch support varies by browser.
166
+ - No "copy" mode: items are moved, not duplicated.
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>zone-transfer</title>
6
+ <link rel="stylesheet" href="../../static/index.css">
7
+ <script type="module" src="https://cdn.jsdelivr.net/npm/zero-md@3?register"></script>
8
+ </head>
9
+ <body>
10
+ <div class="pwc-page-layout">
11
+ <main class="pwc-page-main">
12
+ <zero-md src="./README.md"></zero-md>
13
+ </main>
14
+ <nav class="pwc-page-sidebar">
15
+ <h3>Links</h3>
16
+ <ul>
17
+ <li><a href="./test/basic.html" data-test-page>Test: basic</a></li>
18
+ <li><a href="./test/keyboard.html" data-test-page>Test: keyboard</a></li>
19
+ <li><a href="./INTERNALS.md">INTERNALS.md</a></li>
20
+ </ul>
21
+ </nav>
22
+ </div>
23
+ </body>
24
+ </html>
@@ -0,0 +1,9 @@
1
+ import { PwcZoneTransfer, define } from "./zone-transfer.js";
2
+ import cssText from "./zone-transfer.css";
3
+
4
+ export function register() {
5
+ PwcZoneTransfer.registerCss(cssText);
6
+ define();
7
+ }
8
+
9
+ register();