@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.
- package/README.md +5 -2
- package/dist/all-bs5.js +212 -0
- package/dist/all.js +212 -0
- package/dist/zone-transfer.js +315 -0
- package/package.json +5 -5
- package/src/core/pwc-children-observer-element.js +53 -0
- package/src/core/pwc-element.js +76 -0
- package/src/core/pwc-sentinel-init-element.js +56 -0
- package/src/core/pwc-simple-init-element.js +25 -0
- package/src/core/utils.js +9 -0
- package/src/dialog-opener/INTERNALS.md +47 -0
- package/src/dialog-opener/README.md +145 -0
- package/src/dialog-opener/base.js +226 -0
- package/src/dialog-opener/bs5/dialog-opener.js +73 -0
- package/src/dialog-opener/bs5/index.js +10 -0
- package/src/dialog-opener/dialog-opener.css +21 -0
- package/src/dialog-opener/dialog-opener.js +41 -0
- package/src/dialog-opener/index.html +24 -0
- package/src/dialog-opener/index.js +12 -0
- package/src/dialog-opener/test/basic.html +109 -0
- package/src/dialog-opener/test/bs5-basic.html +100 -0
- package/src/dialog-opener/test/bs5-iframe-target.html +41 -0
- package/src/dialog-opener/test/iframe-target.html +28 -0
- package/src/index-bs5.js +5 -0
- package/src/index.js +4 -0
- package/src/modal-dialog/INTERNALS.md +55 -0
- package/src/modal-dialog/README.md +139 -0
- package/src/modal-dialog/base.js +117 -0
- package/src/modal-dialog/bs5/index.js +7 -0
- package/src/modal-dialog/bs5/modal-dialog.js +109 -0
- package/src/modal-dialog/index.html +24 -0
- package/src/modal-dialog/index.js +9 -0
- package/src/modal-dialog/modal-dialog.css +103 -0
- package/src/modal-dialog/modal-dialog.js +97 -0
- package/src/modal-dialog/test/basic.html +84 -0
- package/src/modal-dialog/test/bs5-basic.html +123 -0
- package/src/multiselect-dual-list/INTERNALS.md +101 -0
- package/src/multiselect-dual-list/README.md +191 -0
- package/src/multiselect-dual-list/base.js +215 -0
- package/src/multiselect-dual-list/bs5/index.js +10 -0
- package/src/multiselect-dual-list/bs5/multiselect-dual-list.js +103 -0
- package/src/multiselect-dual-list/index.html +26 -0
- package/src/multiselect-dual-list/index.js +9 -0
- package/src/multiselect-dual-list/multiselect-dual-list.css +123 -0
- package/src/multiselect-dual-list/multiselect-dual-list.js +100 -0
- package/src/multiselect-dual-list/test/basic.html +115 -0
- package/src/multiselect-dual-list/test/bs5-basic.html +106 -0
- package/src/multiselect-dual-list/test/dynamic-options.html +70 -0
- package/src/multiselect-dual-list/test/filter-api.html +66 -0
- package/src/zone-transfer/INTERNALS.md +80 -0
- package/src/zone-transfer/README.md +166 -0
- package/src/zone-transfer/index.html +24 -0
- package/src/zone-transfer/index.js +9 -0
- package/src/zone-transfer/test/basic.html +78 -0
- package/src/zone-transfer/test/keyboard.html +111 -0
- package/src/zone-transfer/zone-transfer.css +12 -0
- 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>
|