@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,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.0",
3
+ "version": "0.1.2",
4
4
  "description": "Server-first web components.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
- "files": [
8
- "dist"
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()`.