@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,315 @@
|
|
|
1
|
+
// src/core/pwc-element.js
|
|
2
|
+
var PwcElement = class extends HTMLElement {
|
|
3
|
+
/**
|
|
4
|
+
* List of DOM event types to bind on the host element.
|
|
5
|
+
* Subclasses may override.
|
|
6
|
+
*
|
|
7
|
+
* Example:
|
|
8
|
+
* static events = ["click", "input"];
|
|
9
|
+
*/
|
|
10
|
+
static events = [];
|
|
11
|
+
static registerCss(cssText) {
|
|
12
|
+
const sheet = new CSSStyleSheet();
|
|
13
|
+
sheet.replaceSync(cssText);
|
|
14
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
15
|
+
}
|
|
16
|
+
connectedCallback() {
|
|
17
|
+
if (this._connected) return;
|
|
18
|
+
this._connected = true;
|
|
19
|
+
this._bindEvents();
|
|
20
|
+
}
|
|
21
|
+
disconnectedCallback() {
|
|
22
|
+
if (!this._connected) return;
|
|
23
|
+
this._connected = false;
|
|
24
|
+
this._unbindEvents();
|
|
25
|
+
this.onDisconnect();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Optional cleanup hook for subclasses.
|
|
29
|
+
*/
|
|
30
|
+
onDisconnect() {
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Bind declared events using the handleEvent pattern.
|
|
34
|
+
*/
|
|
35
|
+
_bindEvents() {
|
|
36
|
+
const events = this.constructor.events ?? [];
|
|
37
|
+
for (const type of events) {
|
|
38
|
+
this.addEventListener(type, this);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Unbind all previously declared events.
|
|
43
|
+
*/
|
|
44
|
+
_unbindEvents() {
|
|
45
|
+
const events = this.constructor.events ?? [];
|
|
46
|
+
for (const type of events) {
|
|
47
|
+
this.removeEventListener(type, this);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Default event handler.
|
|
52
|
+
* Subclasses are expected to override this method
|
|
53
|
+
* and route events as needed.
|
|
54
|
+
*/
|
|
55
|
+
handleEvent(_event) {
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/core/pwc-children-observer-element.js
|
|
60
|
+
var PwcChildrenObserverElement = class extends PwcElement {
|
|
61
|
+
static observeMode = "children";
|
|
62
|
+
// "children" | "tree"
|
|
63
|
+
connectedCallback() {
|
|
64
|
+
if (this._connected) return;
|
|
65
|
+
super.connectedCallback();
|
|
66
|
+
this._startChildrenObserver();
|
|
67
|
+
}
|
|
68
|
+
disconnectedCallback() {
|
|
69
|
+
this._stopChildrenObserver();
|
|
70
|
+
super.disconnectedCallback();
|
|
71
|
+
}
|
|
72
|
+
onChildrenChanged(_mutations) {
|
|
73
|
+
}
|
|
74
|
+
/** Run fn() without triggering onChildrenChanged for the resulting DOM mutations. */
|
|
75
|
+
_withoutChildrenChangedNotification(fn) {
|
|
76
|
+
fn();
|
|
77
|
+
this._childrenObserver?.takeRecords();
|
|
78
|
+
}
|
|
79
|
+
_startChildrenObserver() {
|
|
80
|
+
const mode = this.constructor.observeMode || "children";
|
|
81
|
+
const subtree = mode === "tree";
|
|
82
|
+
this._childrenObserver = new MutationObserver((mutations) => {
|
|
83
|
+
if (!this._connected) return;
|
|
84
|
+
this.onChildrenChanged(mutations);
|
|
85
|
+
});
|
|
86
|
+
this._childrenObserver.observe(this, { childList: true, subtree });
|
|
87
|
+
this.onChildrenChanged([]);
|
|
88
|
+
}
|
|
89
|
+
_stopChildrenObserver() {
|
|
90
|
+
if (!this._childrenObserver) return;
|
|
91
|
+
this._childrenObserver.disconnect();
|
|
92
|
+
this._childrenObserver = null;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/core/utils.js
|
|
97
|
+
function defineOnce(name, classDef) {
|
|
98
|
+
if (customElements.get(name)) return;
|
|
99
|
+
customElements.define(name, classDef);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/zone-transfer/zone-transfer.js
|
|
103
|
+
var PwcZoneTransfer = class extends PwcChildrenObserverElement {
|
|
104
|
+
static events = ["dragstart", "dragover", "drop", "dragend", "keydown"];
|
|
105
|
+
static observeMode = "tree";
|
|
106
|
+
static zoneSelector = "pwc-zone-transfer-zone, [data-pwc-zone]";
|
|
107
|
+
static itemSelector = "pwc-zone-transfer-item, [data-pwc-item]";
|
|
108
|
+
static handleSelector = "pwc-zone-transfer-handle, [data-pwc-handle]";
|
|
109
|
+
onChildrenChanged() {
|
|
110
|
+
const items = this._items();
|
|
111
|
+
for (const item of items) {
|
|
112
|
+
if (!item.hasAttribute("draggable")) item.setAttribute("draggable", "true");
|
|
113
|
+
if (!item.hasAttribute("tabindex")) item.tabIndex = -1;
|
|
114
|
+
if (!item.hasAttribute("role")) item.setAttribute("role", "option");
|
|
115
|
+
}
|
|
116
|
+
for (const zone of this._zones()) {
|
|
117
|
+
if (!zone.hasAttribute("role")) zone.setAttribute("role", "listbox");
|
|
118
|
+
if (!zone.hasAttribute("tabindex")) zone.tabIndex = -1;
|
|
119
|
+
}
|
|
120
|
+
const active = items.find((it) => it.tabIndex === 0) || items[0] || null;
|
|
121
|
+
for (const it of items) it.tabIndex = it === active ? 0 : -1;
|
|
122
|
+
}
|
|
123
|
+
handleEvent(e) {
|
|
124
|
+
if (e.type === "dragstart") return this._onDragStart(e);
|
|
125
|
+
if (e.type === "dragover") return this._onDragOver(e);
|
|
126
|
+
if (e.type === "drop") return this._onDrop(e);
|
|
127
|
+
if (e.type === "dragend") return this._onDragEnd();
|
|
128
|
+
if (e.type === "keydown") return this._onKeyDown(e);
|
|
129
|
+
}
|
|
130
|
+
_zones() {
|
|
131
|
+
return Array.from(this.querySelectorAll(this.constructor.zoneSelector));
|
|
132
|
+
}
|
|
133
|
+
_items(zoneEl) {
|
|
134
|
+
return Array.from((zoneEl || this).querySelectorAll(this.constructor.itemSelector));
|
|
135
|
+
}
|
|
136
|
+
_onDragStart(e) {
|
|
137
|
+
const item = this._closestItem(e.target);
|
|
138
|
+
if (!item) return;
|
|
139
|
+
if (item.querySelector(this.constructor.handleSelector) && !this._closestHandle(e.target)) {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const zone = this._closestZone(item);
|
|
144
|
+
if (!zone) return;
|
|
145
|
+
this._drag = { item, fromZone: zone };
|
|
146
|
+
if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
|
|
147
|
+
item.classList.add("pwc-zone-transfer-dragging");
|
|
148
|
+
this._ensurePlaceholder(item);
|
|
149
|
+
}
|
|
150
|
+
_onDragOver(e) {
|
|
151
|
+
if (!this._drag?.item) return;
|
|
152
|
+
const zone = this._closestZone(e.target);
|
|
153
|
+
if (!zone) return;
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
156
|
+
this._ensurePlaceholder(this._drag.item);
|
|
157
|
+
const beforeEl = this._beforeFromPointer(zone, e, this._drag.item);
|
|
158
|
+
this._movePlaceholder(zone, beforeEl);
|
|
159
|
+
this._drag.overZone = zone;
|
|
160
|
+
this._drag.overMethod = beforeEl ? "before" : "append";
|
|
161
|
+
}
|
|
162
|
+
_onDrop(e) {
|
|
163
|
+
if (!this._drag?.item) return;
|
|
164
|
+
const zone = this._closestZone(e.target);
|
|
165
|
+
if (!zone) return;
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
this._applyMove(this._drag.item, this._drag.fromZone, zone, this._drag.overMethod || "append");
|
|
168
|
+
this._clearPlaceholder();
|
|
169
|
+
}
|
|
170
|
+
_onDragEnd() {
|
|
171
|
+
if (this._drag?.item) this._drag.item.classList.remove("pwc-zone-transfer-dragging");
|
|
172
|
+
this._drag = null;
|
|
173
|
+
this._clearPlaceholder();
|
|
174
|
+
}
|
|
175
|
+
_onKeyDown(e) {
|
|
176
|
+
if (e.target?.closest?.("input,textarea,select,button,[contenteditable]")) return;
|
|
177
|
+
const item = this._closestItem(e.target);
|
|
178
|
+
if (!item) return;
|
|
179
|
+
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
const dir = e.key === "ArrowDown" ? 1 : -1;
|
|
182
|
+
if (e.ctrlKey || e.metaKey) this._keyboardReorder(item, dir);
|
|
183
|
+
else this._focusSibling(item, dir);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const zone = this._zoneByHotkey(e.key);
|
|
187
|
+
if (!zone) return;
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
this._keyboardMoveToZone(item, zone);
|
|
190
|
+
}
|
|
191
|
+
_keyboardReorder(item, dir) {
|
|
192
|
+
const zone = this._closestZone(item);
|
|
193
|
+
if (!zone) return;
|
|
194
|
+
const items = this._items(zone);
|
|
195
|
+
const i = items.indexOf(item);
|
|
196
|
+
if (i < 0) return;
|
|
197
|
+
const j = i + dir;
|
|
198
|
+
if (j < 0 || j >= items.length) return;
|
|
199
|
+
zone.insertBefore(item, dir > 0 ? items[j].nextElementSibling : items[j]);
|
|
200
|
+
for (const it of this._items()) it.tabIndex = it === item ? 0 : -1;
|
|
201
|
+
item.focus();
|
|
202
|
+
this._emitChange(item, zone, zone, this._indexInZone(item, zone), "before");
|
|
203
|
+
}
|
|
204
|
+
_keyboardMoveToZone(item, zone) {
|
|
205
|
+
const fromZone = this._closestZone(item);
|
|
206
|
+
if (!fromZone || fromZone === zone) return;
|
|
207
|
+
zone.appendChild(item);
|
|
208
|
+
for (const it of this._items()) it.tabIndex = it === item ? 0 : -1;
|
|
209
|
+
item.focus();
|
|
210
|
+
this._emitChange(item, fromZone, zone, this._indexInZone(item, zone), "append");
|
|
211
|
+
}
|
|
212
|
+
_zoneByHotkey(key) {
|
|
213
|
+
const zones = this._zones();
|
|
214
|
+
if (!zones.some((z) => z.hasAttribute("data-pwc-zone-key"))) return null;
|
|
215
|
+
return zones.find((z) => z.getAttribute("data-pwc-zone-key") === key) || null;
|
|
216
|
+
}
|
|
217
|
+
_emitChange(item, fromZone, toZone, index, method) {
|
|
218
|
+
this.dispatchEvent(
|
|
219
|
+
new CustomEvent("pwc-zone-transfer:change", {
|
|
220
|
+
bubbles: true,
|
|
221
|
+
detail: {
|
|
222
|
+
itemId: this._itemId(item),
|
|
223
|
+
fromZone: this._zoneName(fromZone),
|
|
224
|
+
toZone: this._zoneName(toZone),
|
|
225
|
+
index,
|
|
226
|
+
method
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
_applyMove(item, fromZone, toZone, method) {
|
|
232
|
+
if (this._placeholder?.parentNode === toZone) toZone.insertBefore(item, this._placeholder);
|
|
233
|
+
else toZone.appendChild(item);
|
|
234
|
+
for (const it of this._items()) it.tabIndex = it === item ? 0 : -1;
|
|
235
|
+
this._emitChange(item, fromZone, toZone, this._indexInZone(item, toZone), method);
|
|
236
|
+
}
|
|
237
|
+
_focusSibling(item, dir) {
|
|
238
|
+
const zone = this._closestZone(item);
|
|
239
|
+
if (!zone) return;
|
|
240
|
+
const items = this._items(zone);
|
|
241
|
+
const i = items.indexOf(item);
|
|
242
|
+
if (i < 0) return;
|
|
243
|
+
const next = items[i + dir];
|
|
244
|
+
if (!next) return;
|
|
245
|
+
item.tabIndex = -1;
|
|
246
|
+
next.tabIndex = 0;
|
|
247
|
+
next.focus();
|
|
248
|
+
}
|
|
249
|
+
_ensurePlaceholder(itemEl) {
|
|
250
|
+
if (this._placeholder?.isConnected) return;
|
|
251
|
+
this._placeholder = document.createElement("div");
|
|
252
|
+
this._placeholder.className = "pwc-zone-transfer-placeholder";
|
|
253
|
+
this._placeholder.setAttribute("aria-hidden", "true");
|
|
254
|
+
this._placeholder.style.height = `${Math.max(8, Math.round(itemEl.getBoundingClientRect().height || 0))}px`;
|
|
255
|
+
}
|
|
256
|
+
_movePlaceholder(zoneEl, beforeEl) {
|
|
257
|
+
if (!this._placeholder) return;
|
|
258
|
+
if (beforeEl && beforeEl.parentNode === zoneEl) zoneEl.insertBefore(this._placeholder, beforeEl);
|
|
259
|
+
else zoneEl.appendChild(this._placeholder);
|
|
260
|
+
}
|
|
261
|
+
_clearPlaceholder() {
|
|
262
|
+
if (this._placeholder?.parentNode) this._placeholder.parentNode.removeChild(this._placeholder);
|
|
263
|
+
}
|
|
264
|
+
_beforeFromPointer(zoneEl, e, draggedItem) {
|
|
265
|
+
const y = e.clientY;
|
|
266
|
+
for (const it of this._items(zoneEl)) {
|
|
267
|
+
if (it === draggedItem || it === this._placeholder) continue;
|
|
268
|
+
const r = it.getBoundingClientRect();
|
|
269
|
+
if (y <= r.top + r.height / 2) return it;
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
_closestZone(node) {
|
|
274
|
+
if (!(node instanceof Element)) return null;
|
|
275
|
+
const zone = node.closest(this.constructor.zoneSelector);
|
|
276
|
+
return zone && this.contains(zone) ? zone : null;
|
|
277
|
+
}
|
|
278
|
+
_closestItem(node) {
|
|
279
|
+
if (!(node instanceof Element)) return null;
|
|
280
|
+
const item = node.closest(this.constructor.itemSelector);
|
|
281
|
+
return item && this.contains(item) ? item : null;
|
|
282
|
+
}
|
|
283
|
+
_closestHandle(node) {
|
|
284
|
+
if (!(node instanceof Element)) return null;
|
|
285
|
+
const handle = node.closest(this.constructor.handleSelector);
|
|
286
|
+
return handle && this.contains(handle) ? handle : null;
|
|
287
|
+
}
|
|
288
|
+
_zoneName(zoneEl) {
|
|
289
|
+
if (!zoneEl) return "";
|
|
290
|
+
return zoneEl.tagName.toLowerCase() === "pwc-zone-transfer-zone" ? zoneEl.getAttribute("name") || "" : zoneEl.getAttribute("data-pwc-zone") || "";
|
|
291
|
+
}
|
|
292
|
+
_itemId(itemEl) {
|
|
293
|
+
if (!itemEl) return "";
|
|
294
|
+
return itemEl.tagName.toLowerCase() === "pwc-zone-transfer-item" ? itemEl.getAttribute("id") || itemEl.getAttribute("data-id") || "" : itemEl.getAttribute("data-pwc-item") || itemEl.getAttribute("id") || "";
|
|
295
|
+
}
|
|
296
|
+
_indexInZone(itemEl, zoneEl) {
|
|
297
|
+
return Math.max(0, this._items(zoneEl).indexOf(itemEl));
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
function define() {
|
|
301
|
+
defineOnce("pwc-zone-transfer", PwcZoneTransfer);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/zone-transfer/zone-transfer.css
|
|
305
|
+
var zone_transfer_default = 'pwc-zone-transfer [draggable="true"] {\n cursor: grab;\n}\n\npwc-zone-transfer .pwc-zone-transfer-dragging {\n cursor: grabbing;\n opacity: 0.6;\n}\n\npwc-zone-transfer .pwc-zone-transfer-placeholder {\n opacity: 0.3;\n}';
|
|
306
|
+
|
|
307
|
+
// src/zone-transfer/index.js
|
|
308
|
+
function register() {
|
|
309
|
+
PwcZoneTransfer.registerCss(zone_transfer_default);
|
|
310
|
+
define();
|
|
311
|
+
}
|
|
312
|
+
register();
|
|
313
|
+
export {
|
|
314
|
+
register
|
|
315
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tillsc/progressive-web-components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Server-first web components.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
],
|
|
7
|
+
"publishConfig": { "access": "public" },
|
|
8
|
+
"files": [ "dist", "src"],
|
|
10
9
|
"exports": {
|
|
11
10
|
"./all": "./dist/all.js",
|
|
12
11
|
"./all-bs5": "./dist/all-bs5.js",
|
|
@@ -15,7 +14,8 @@
|
|
|
15
14
|
"./modal-dialog": "./dist/modal-dialog.js",
|
|
16
15
|
"./modal-dialog-bs5": "./dist/modal-dialog-bs5.js",
|
|
17
16
|
"./multiselect-dual-list": "./dist/multiselect-dual-list.js",
|
|
18
|
-
"./multiselect-dual-list-bs5": "./dist/multiselect-dual-list-bs5.js"
|
|
17
|
+
"./multiselect-dual-list-bs5": "./dist/multiselect-dual-list-bs5.js",
|
|
18
|
+
"./zone-transfer": "./dist/zone-transfer.js"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"clean": "rm -rf dist",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {PwcElement} from "./pwc-element.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Children observer element.
|
|
5
|
+
*
|
|
6
|
+
* Calls onChildrenChanged() on connect and on every subsequent child mutation.
|
|
7
|
+
*
|
|
8
|
+
* Modes (static observeMode):
|
|
9
|
+
* - "children": direct children only
|
|
10
|
+
* - "tree": full subtree
|
|
11
|
+
*/
|
|
12
|
+
export class PwcChildrenObserverElement extends PwcElement {
|
|
13
|
+
static observeMode = "children"; // "children" | "tree"
|
|
14
|
+
|
|
15
|
+
connectedCallback() {
|
|
16
|
+
if (this._connected) return;
|
|
17
|
+
super.connectedCallback();
|
|
18
|
+
this._startChildrenObserver();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
disconnectedCallback() {
|
|
22
|
+
this._stopChildrenObserver();
|
|
23
|
+
super.disconnectedCallback();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
onChildrenChanged(_mutations) {}
|
|
27
|
+
|
|
28
|
+
/** Run fn() without triggering onChildrenChanged for the resulting DOM mutations. */
|
|
29
|
+
_withoutChildrenChangedNotification(fn) {
|
|
30
|
+
fn();
|
|
31
|
+
this._childrenObserver?.takeRecords();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_startChildrenObserver() {
|
|
35
|
+
const mode = this.constructor.observeMode || "children";
|
|
36
|
+
const subtree = mode === "tree";
|
|
37
|
+
|
|
38
|
+
this._childrenObserver = new MutationObserver((mutations) => {
|
|
39
|
+
if (!this._connected) return;
|
|
40
|
+
this.onChildrenChanged(mutations);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this._childrenObserver.observe(this, { childList: true, subtree });
|
|
44
|
+
|
|
45
|
+
this.onChildrenChanged([]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_stopChildrenObserver() {
|
|
49
|
+
if (!this._childrenObserver) return;
|
|
50
|
+
this._childrenObserver.disconnect();
|
|
51
|
+
this._childrenObserver = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for progressive-web-components.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Ensure idempotent lifecycle handling
|
|
6
|
+
* - Declaratively bind and unbind host-level DOM events
|
|
7
|
+
* - Provide a consistent cleanup hook
|
|
8
|
+
*
|
|
9
|
+
* This is intentionally minimal.
|
|
10
|
+
* No rendering, no templating, no magic.
|
|
11
|
+
*/
|
|
12
|
+
export class PwcElement extends HTMLElement {
|
|
13
|
+
/**
|
|
14
|
+
* List of DOM event types to bind on the host element.
|
|
15
|
+
* Subclasses may override.
|
|
16
|
+
*
|
|
17
|
+
* Example:
|
|
18
|
+
* static events = ["click", "input"];
|
|
19
|
+
*/
|
|
20
|
+
static events = [];
|
|
21
|
+
|
|
22
|
+
static registerCss(cssText) {
|
|
23
|
+
const sheet = new CSSStyleSheet();
|
|
24
|
+
sheet.replaceSync(cssText);
|
|
25
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
connectedCallback() {
|
|
29
|
+
if (this._connected) return;
|
|
30
|
+
this._connected = true;
|
|
31
|
+
|
|
32
|
+
this._bindEvents();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
disconnectedCallback() {
|
|
36
|
+
if (!this._connected) return;
|
|
37
|
+
this._connected = false;
|
|
38
|
+
|
|
39
|
+
this._unbindEvents();
|
|
40
|
+
this.onDisconnect();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Optional cleanup hook for subclasses.
|
|
45
|
+
*/
|
|
46
|
+
onDisconnect() {}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Bind declared events using the handleEvent pattern.
|
|
50
|
+
*/
|
|
51
|
+
_bindEvents() {
|
|
52
|
+
const events = this.constructor.events ?? [];
|
|
53
|
+
for (const type of events) {
|
|
54
|
+
this.addEventListener(type, this);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Unbind all previously declared events.
|
|
60
|
+
*/
|
|
61
|
+
_unbindEvents() {
|
|
62
|
+
const events = this.constructor.events ?? [];
|
|
63
|
+
for (const type of events) {
|
|
64
|
+
this.removeEventListener(type, this);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Default event handler.
|
|
70
|
+
* Subclasses are expected to override this method
|
|
71
|
+
* and route events as needed.
|
|
72
|
+
*/
|
|
73
|
+
handleEvent(_event) {
|
|
74
|
+
// intentionally empty
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {PwcElement} from "./pwc-element.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sentinel init element.
|
|
5
|
+
*
|
|
6
|
+
* Calls onConnect() once per connection, when a sentinel appears in the light DOM.
|
|
7
|
+
* Uses a MutationObserver only until ready.
|
|
8
|
+
*
|
|
9
|
+
* Subclasses may override sentinelSelector().
|
|
10
|
+
*/
|
|
11
|
+
export class PwcSentinelInitElement extends PwcElement {
|
|
12
|
+
static sentinelSelector = "pwc-sentinel, [data-pwc-sentinel]";
|
|
13
|
+
|
|
14
|
+
connectedCallback() {
|
|
15
|
+
if (this._connected) return;
|
|
16
|
+
super.connectedCallback();
|
|
17
|
+
|
|
18
|
+
if (this._hasSentinel()) {
|
|
19
|
+
this.onConnect();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this._sentinelObserver = new MutationObserver(() => {
|
|
24
|
+
if (!this._connected) return;
|
|
25
|
+
if (!this._hasSentinel()) return;
|
|
26
|
+
|
|
27
|
+
this._stopSentinelObserver();
|
|
28
|
+
this.onConnect();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// subtree:true so the sentinel can be nested (common with templates/partials)
|
|
32
|
+
this._sentinelObserver.observe(this, { childList: true, subtree: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
disconnectedCallback() {
|
|
36
|
+
this._stopSentinelObserver();
|
|
37
|
+
super.disconnectedCallback();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Hook for subclasses.
|
|
42
|
+
* Called once per connection, when the sentinel is present.
|
|
43
|
+
*/
|
|
44
|
+
onConnect() {}
|
|
45
|
+
|
|
46
|
+
_hasSentinel() {
|
|
47
|
+
const selector = this.constructor.sentinelSelector || PwcSentinelInitElement.sentinelSelector;
|
|
48
|
+
return Boolean(this.querySelector(selector));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_stopSentinelObserver() {
|
|
52
|
+
if (!this._sentinelObserver) return;
|
|
53
|
+
this._sentinelObserver.disconnect();
|
|
54
|
+
this._sentinelObserver = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {PwcElement} from "./pwc-element.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple init element.
|
|
5
|
+
*
|
|
6
|
+
* Calls onConnect() once per connection, deferred to a microtask.
|
|
7
|
+
* Use this when a microtask is sufficient to access server-rendered children.
|
|
8
|
+
*/
|
|
9
|
+
export class PwcSimpleInitElement extends PwcElement {
|
|
10
|
+
connectedCallback() {
|
|
11
|
+
if (this._connected) return;
|
|
12
|
+
super.connectedCallback();
|
|
13
|
+
|
|
14
|
+
queueMicrotask(() => {
|
|
15
|
+
if (!this._connected) return;
|
|
16
|
+
this.onConnect();
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook for subclasses.
|
|
22
|
+
* Called once per connection, after microtask deferral.
|
|
23
|
+
*/
|
|
24
|
+
onConnect() {}
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function ensureId(el, prefix = "pwc") {
|
|
2
|
+
if (!el.id) el.id = `${prefix}-${Math.random().toString(36).slice(2)}`;
|
|
3
|
+
return el.id;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function defineOnce(name, classDef) {
|
|
7
|
+
if (customElements.get(name)) return;
|
|
8
|
+
customElements.define(name, classDef);
|
|
9
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Dialog-Opener — Internals
|
|
2
|
+
|
|
3
|
+
## Architecture
|
|
4
|
+
|
|
5
|
+
- `findOrCreateDialog(src)` — creates/reuses a `<pwc-modal-dialog>` (or `-bs5`), opens it,
|
|
6
|
+
places the iframe, and wires up the `this.dialog` / `this.modal` adapter.
|
|
7
|
+
|
|
8
|
+
The base class never touches DOM rendering directly. Variants own the dialog creation and
|
|
9
|
+
provide a uniform adapter interface (`this.modal.show()` / `this.modal.hide()`).
|
|
10
|
+
|
|
11
|
+
## Flow: link click to dialog
|
|
12
|
+
|
|
13
|
+
1. Click on an `<a>` inside the component is intercepted (`handleEvent`)
|
|
14
|
+
2. `prepareIFrameLink()` builds the iframe URL:
|
|
15
|
+
- collects `input` values as `default` query param
|
|
16
|
+
- appends `_layout=false`
|
|
17
|
+
3. `findOrCreateDialog(src)` (variant hook) creates the modal and iframe
|
|
18
|
+
4. `enhanceIFrame()` waits for the iframe `load` event, then calls `iFrameLoad()`
|
|
19
|
+
5. `iFrameLoad()` checks the iframe URL:
|
|
20
|
+
- If `dialog_finished_with` is present → close dialog, trigger reload or navigation
|
|
21
|
+
- Otherwise → run `moveElementsToOuterActions()`, show iframe
|
|
22
|
+
|
|
23
|
+
## Move-out mechanism
|
|
24
|
+
|
|
25
|
+
When `move-out` is set, buttons are **cloned** from the iframe document into the dialog footer.
|
|
26
|
+
The original buttons are hidden. Clicking a cloned button triggers `click()` on the original
|
|
27
|
+
inside the iframe, then hides the iframe (to show a loading state while the form submits).
|
|
28
|
+
|
|
29
|
+
## Local reload
|
|
30
|
+
|
|
31
|
+
When the dialog completes and `local-reload` is set:
|
|
32
|
+
|
|
33
|
+
1. The completion URL is fetched via `fetch()`
|
|
34
|
+
2. The response HTML is parsed with `DOMParser`
|
|
35
|
+
3. The element matching `this.id` is extracted from the response
|
|
36
|
+
4. Its children replace the current component's children
|
|
37
|
+
5. Optionally: `history.pushState` / `replaceState` updates the URL
|
|
38
|
+
6. Optionally: inline `<script>` tags are re-executed (cloned into new elements)
|
|
39
|
+
7. A `pwc-dialog-opener:local-reload` custom event is dispatched
|
|
40
|
+
|
|
41
|
+
If any step fails, it falls back to full page navigation.
|
|
42
|
+
|
|
43
|
+
## Modal adapter pattern
|
|
44
|
+
|
|
45
|
+
The base class expects `this.modal` with `.show()` and `.hide()`. Since both variants use
|
|
46
|
+
`<pwc-modal-dialog>` (which is already open by the time `findOrCreateDialog` returns),
|
|
47
|
+
`show()` is a no-op and `hide()` delegates to `modalDialog.close()`.
|