@tillsc/progressive-web-components 0.1.0 → 0.1.1
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/package.json +3 -4
- 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.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/dialog-opener/test/index.html +19 -0
- package/src/index-bs5.js +3 -0
- package/src/index.js +3 -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.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/modal-dialog/test/index.html +19 -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.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/multiselect-dual-list/test/index.html +21 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineOnce } from "../../core/utils.js";
|
|
2
|
+
import { BaseDialogOpener } from "../base.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <pwc-dialog-opener-bs5>
|
|
6
|
+
*
|
|
7
|
+
* Uses <pwc-modal-dialog-bs5> as the Bootstrap modal socket.
|
|
8
|
+
*
|
|
9
|
+
* Requirements:
|
|
10
|
+
* - Bootstrap 5 CSS + JS present (globalThis.bootstrap.Modal)
|
|
11
|
+
* - pwc-modal-dialog-bs5 defined (either imported/bundled or already on page)
|
|
12
|
+
*/
|
|
13
|
+
export class PwcDialogOpenerBs5 extends BaseDialogOpener {
|
|
14
|
+
findOrCreateDialog(src) {
|
|
15
|
+
const tag = "pwc-modal-dialog-bs5";
|
|
16
|
+
|
|
17
|
+
// Prefer one modal socket per opener instance.
|
|
18
|
+
if (!this.dialog) {
|
|
19
|
+
// Use existing child socket if provided, otherwise create one.
|
|
20
|
+
this.dialog = this.querySelector(tag) || document.createElement(tag);
|
|
21
|
+
|
|
22
|
+
// If caller didn't place it into the DOM, keep it associated with this component.
|
|
23
|
+
// ModalDialogBase will auto-append itself to <body> on open() if not connected.
|
|
24
|
+
if (!this.dialog.isConnected) {
|
|
25
|
+
this.appendChild(this.dialog);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Open modal and get access to the body/footer containers.
|
|
30
|
+
this.dialog.open({
|
|
31
|
+
title: this.getAttribute("title") || "",
|
|
32
|
+
size: this.getAttribute("size") || "lg",
|
|
33
|
+
closeText: this.getAttribute("close") || "Close",
|
|
34
|
+
showClose: false,
|
|
35
|
+
backdrop: true,
|
|
36
|
+
keyboard: true,
|
|
37
|
+
focus: true
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const closeText = this.getAttribute("close") || "Close";
|
|
41
|
+
this.dialog.footerEl.innerHTML = `
|
|
42
|
+
<div class="pwc-dialog-opener-actions">
|
|
43
|
+
<button type="button" class="btn btn-secondary" data-pwc-action="close" aria-label="${closeText}">
|
|
44
|
+
${closeText}
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const body = this.dialog.bodyEl;
|
|
50
|
+
body.replaceChildren(this.createIFrame(src));
|
|
51
|
+
|
|
52
|
+
// BaseDialogOpener expects this.modal.show()/hide()
|
|
53
|
+
// Map those to the modal-dialog component API.
|
|
54
|
+
this.modal = {
|
|
55
|
+
show: () => {
|
|
56
|
+
// already shown by open(); no-op for compatibility
|
|
57
|
+
},
|
|
58
|
+
hide: () => this.dialog.close()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_moveOutSelector() {
|
|
63
|
+
let selector = super._moveOutSelector();
|
|
64
|
+
if (selector === "primary") {
|
|
65
|
+
selector = ".btn-primary[type=submit]";
|
|
66
|
+
}
|
|
67
|
+
return selector
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function define() {
|
|
72
|
+
defineOnce("pwc-dialog-opener-bs5", PwcDialogOpenerBs5);
|
|
73
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* Footer actions container (used by move-out) */
|
|
2
|
+
.pwc-dialog-opener-footer {
|
|
3
|
+
display: flex;
|
|
4
|
+
justify-content: flex-end;
|
|
5
|
+
gap: 8px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Close button */
|
|
9
|
+
.pwc-dialog-opener-close {
|
|
10
|
+
appearance: none;
|
|
11
|
+
border: 1px solid rgba(0, 0, 0, 0.25);
|
|
12
|
+
background: transparent;
|
|
13
|
+
font: inherit;
|
|
14
|
+
padding: 6px 12px;
|
|
15
|
+
border-radius: 4px;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.pwc-dialog-opener-close:hover {
|
|
20
|
+
background: rgba(0, 0, 0, 0.06);
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineOnce } from "../core/utils.js";
|
|
2
|
+
import { BaseDialogOpener } from "./base.js";
|
|
3
|
+
|
|
4
|
+
export class PwcDialogOpener extends BaseDialogOpener {
|
|
5
|
+
findOrCreateDialog(src) {
|
|
6
|
+
if (!this.modalDialog) {
|
|
7
|
+
this.modalDialog = document.createElement("pwc-modal-dialog");
|
|
8
|
+
document.body.appendChild(this.modalDialog);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const closeText = this.getAttribute("close") || "Close";
|
|
12
|
+
this.modalDialog.open({
|
|
13
|
+
closeText,
|
|
14
|
+
showClose: false
|
|
15
|
+
});
|
|
16
|
+
this.modalDialog.footerEl.innerHTML = `
|
|
17
|
+
<div class="pwc-dialog-opener-actions pwc-dialog-opener-footer">
|
|
18
|
+
<button type="button" class="pwc-dialog-opener-close" data-pwc-action="close" aria-label="${closeText}">
|
|
19
|
+
${closeText}
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
`;
|
|
23
|
+
const iframe = this.createIFrame(src);
|
|
24
|
+
this.modalDialog.bodyEl.replaceChildren(iframe);
|
|
25
|
+
|
|
26
|
+
// Contract for BaseDialogOpener.enhanceIFrame():
|
|
27
|
+
// it queries this.dialog for "iframe".
|
|
28
|
+
this.dialog = this.modalDialog.ui.rootEl;
|
|
29
|
+
|
|
30
|
+
// Contract for BaseDialogOpener.open():
|
|
31
|
+
// it calls this.modal.show()/hide().
|
|
32
|
+
this.modal = {
|
|
33
|
+
show: () => {}, // modal-dialog is already shown by open()
|
|
34
|
+
hide: () => this.modalDialog.close()
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function define() {
|
|
40
|
+
defineOnce("pwc-dialog-opener", PwcDialogOpener);
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PwcDialogOpener, define } from "./dialog-opener.js";
|
|
2
|
+
import cssText from "./dialog-opener.css";
|
|
3
|
+
|
|
4
|
+
// Ensure the modal-dialog is registered first.
|
|
5
|
+
import "../modal-dialog/index.js";
|
|
6
|
+
|
|
7
|
+
export function register() {
|
|
8
|
+
PwcDialogOpener.registerCss(cssText);
|
|
9
|
+
define();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
register();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<!-- src/dialog-opener/test/click-opens-dialog.html -->
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html>
|
|
4
|
+
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8" />
|
|
7
|
+
<title>dialog-opener: click opens dialog</title>
|
|
8
|
+
<script type="module" src="../../../dist/dialog-opener.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body>
|
|
12
|
+
<pwc-dialog-opener id="dialog-opener-demo" move-out="submit" local-reload="with-scripts replace-url">
|
|
13
|
+
<a href="./iframe-target.html">Open</a><br>
|
|
14
|
+
Hello <span id="first_name"></span> <span id="last_name"></span>
|
|
15
|
+
<input type="hidden" name="default_for_iframe_target">
|
|
16
|
+
<script type="module">
|
|
17
|
+
const url = new URL(window.location.href);
|
|
18
|
+
const firstName = url.searchParams.get("first_name") || "";
|
|
19
|
+
const lastName = url.searchParams.get("last_name") || "";
|
|
20
|
+
document.getElementById("first_name").textContent = firstName
|
|
21
|
+
document.getElementById("last_name").textContent = lastName
|
|
22
|
+
document.querySelector("input[name=default_for_iframe_target]").value = JSON.stringify({firstName , lastName});
|
|
23
|
+
</script>
|
|
24
|
+
</pwc-dialog-opener>
|
|
25
|
+
|
|
26
|
+
<script type="module">
|
|
27
|
+
import { run } from "../../../test/static/harness.js";
|
|
28
|
+
|
|
29
|
+
const u = new URL(window.location.href);
|
|
30
|
+
if (u.searchParams.has("commit")) {
|
|
31
|
+
// IFrame subission mode
|
|
32
|
+
if (u.searchParams.has("commit") && !u.searchParams.has("dialog_finished_with")) {
|
|
33
|
+
// Ensure the iframe URL contains the marker param your component expects.
|
|
34
|
+
// This runs inside the iframe page.
|
|
35
|
+
// This would usually be done by the server handling the form submission,
|
|
36
|
+
// but we can simulate it here for testing.
|
|
37
|
+
u.searchParams.set("dialog_finished_with", "ok");
|
|
38
|
+
window.location.replace(u.toString());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
run(async ({ assert, equal, waitFor, nextTick, log }) => {
|
|
43
|
+
log("checking element definition");
|
|
44
|
+
assert(customElements.get("pwc-dialog-opener"), "pwc-dialog-opener not defined");
|
|
45
|
+
|
|
46
|
+
const el = document.querySelector("#dialog-opener-demo");
|
|
47
|
+
assert(el, "missing pwc-dialog-opener");
|
|
48
|
+
|
|
49
|
+
const link = el.querySelector("a");
|
|
50
|
+
assert(link, "missing link");
|
|
51
|
+
|
|
52
|
+
log("clicking link");
|
|
53
|
+
link.dispatchEvent(
|
|
54
|
+
new MouseEvent("click", { bubbles: true, cancelable: true, composed: true })
|
|
55
|
+
);
|
|
56
|
+
await nextTick();
|
|
57
|
+
|
|
58
|
+
log("waiting for dialog to exist");
|
|
59
|
+
await waitFor(() => Boolean(document.querySelector("dialog.pwc-modal-dialog")), {
|
|
60
|
+
message: "dialog not created"
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const dlg = document.querySelector("dialog.pwc-modal-dialog");
|
|
64
|
+
assert(dlg, "dialog missing");
|
|
65
|
+
|
|
66
|
+
log("waiting for dialog to be open");
|
|
67
|
+
await waitFor(() => dlg.open === true, { message: "dialog did not open" });
|
|
68
|
+
|
|
69
|
+
log("checking iframe exists");
|
|
70
|
+
const iframe = dlg.querySelector("iframe");
|
|
71
|
+
assert(iframe, "iframe not created");
|
|
72
|
+
|
|
73
|
+
log("waiting for iframe load");
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
return iframe.contentDocument && iframe.contentDocument.readyState === "complete";
|
|
76
|
+
}, { timeoutMs: 10_000, message: "iframe did not load" });
|
|
77
|
+
|
|
78
|
+
log("checking iframe src has _layout=false");
|
|
79
|
+
{
|
|
80
|
+
const raw = iframe.getAttribute("src") || "";
|
|
81
|
+
const u = new URL(raw, window.location.href);
|
|
82
|
+
assert(u.pathname.endsWith("/src/dialog-opener/test/iframe-target.html"), `unexpected iframe pathname: ${u.pathname}`);
|
|
83
|
+
assert(u.searchParams.get("_layout") === "false", `missing or wrong _layout param: ${u.search}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
log("waiting for moved-out OK button");
|
|
87
|
+
await waitFor(() => {
|
|
88
|
+
const btn = dlg.querySelector(".pwc-dialog-opener-actions button[type=submit]");
|
|
89
|
+
return Boolean(btn);
|
|
90
|
+
}, { timeoutMs: 10_000, message: "moved-out submit button not found" });
|
|
91
|
+
|
|
92
|
+
const okBtn = dlg.querySelector(".pwc-dialog-opener-actions button[type=submit]");
|
|
93
|
+
assert(okBtn, "moved-out submit button missing");
|
|
94
|
+
|
|
95
|
+
log("clicking moved-out OK button");
|
|
96
|
+
okBtn.dispatchEvent(
|
|
97
|
+
new MouseEvent("click", { bubbles: true, cancelable: true, composed: true })
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
log("waiting for dialog to close after redirect");
|
|
101
|
+
await waitFor(() => dlg.open === false, { timeoutMs: 10_000, message: "dialog did not close after OK" });
|
|
102
|
+
|
|
103
|
+
equal(document.querySelectorAll("pwc-modal-dialog").length, 1, "modal-dialog element should persist for reuse");
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
</body>
|
|
108
|
+
|
|
109
|
+
</html>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<title>dialog-opener-bs5: basic</title>
|
|
7
|
+
|
|
8
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body>
|
|
12
|
+
<pwc-dialog-opener-bs5 id="opener" local-reload="with-scripts replace-url">
|
|
13
|
+
<a href="./bs5-iframe-target.html">Open dialog</a>
|
|
14
|
+
Hello <span id="first_name"></span> <span id="last_name"></span>
|
|
15
|
+
<input type="hidden" name="default_for_iframe_target">
|
|
16
|
+
<script type="module">
|
|
17
|
+
const url = new URL(window.location.href);
|
|
18
|
+
const firstName = url.searchParams.get("first_name") || "";
|
|
19
|
+
const lastName = url.searchParams.get("last_name") || "";
|
|
20
|
+
document.getElementById("first_name").textContent = firstName
|
|
21
|
+
document.getElementById("last_name").textContent = lastName
|
|
22
|
+
document.querySelector("input[name=default_for_iframe_target]").value = JSON.stringify({ firstName, lastName });
|
|
23
|
+
</script>
|
|
24
|
+
</pwc-dialog-opener-bs5>
|
|
25
|
+
|
|
26
|
+
<!-- component bundle -->
|
|
27
|
+
<script type="module" src="../../../dist/dialog-opener-bs5.js"></script>
|
|
28
|
+
|
|
29
|
+
<!-- bootstrap js -->
|
|
30
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
31
|
+
|
|
32
|
+
<script type="module">
|
|
33
|
+
import { run } from "../../../test/static/harness.js";
|
|
34
|
+
|
|
35
|
+
const u = new URL(window.location.href);
|
|
36
|
+
if (u.searchParams.has("commit")) {
|
|
37
|
+
// IFrame subission mode
|
|
38
|
+
if (u.searchParams.has("commit") && !u.searchParams.has("dialog_finished_with")) {
|
|
39
|
+
// Ensure the iframe URL contains the marker param your component expects.
|
|
40
|
+
// This runs inside the iframe page.
|
|
41
|
+
// This would usually be done by the server handling the form submission,
|
|
42
|
+
// but we can simulate it here for testing.
|
|
43
|
+
u.searchParams.set("dialog_finished_with", "ok");
|
|
44
|
+
window.location.replace(u.toString());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
|
|
49
|
+
run(async ({ assert, waitFor, log }) => {
|
|
50
|
+
log("checking custom element definition");
|
|
51
|
+
assert(
|
|
52
|
+
customElements.get("pwc-dialog-opener-bs5"),
|
|
53
|
+
"pwc-dialog-opener-bs5 not defined"
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const opener = document.getElementById("opener");
|
|
57
|
+
assert(opener, "missing dialog opener element");
|
|
58
|
+
|
|
59
|
+
const link = opener.querySelector("a");
|
|
60
|
+
assert(link, "missing link");
|
|
61
|
+
|
|
62
|
+
log("clicking opener link");
|
|
63
|
+
link.click();
|
|
64
|
+
|
|
65
|
+
log("waiting for bootstrap modal to appear");
|
|
66
|
+
await waitFor(() => document.querySelector(".modal.show"), {
|
|
67
|
+
message: "modal not shown"
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const modal = document.querySelector(".modal.show");
|
|
71
|
+
assert(modal, "modal element missing");
|
|
72
|
+
|
|
73
|
+
log("checking iframe existence");
|
|
74
|
+
const iframe = modal.querySelector("iframe");
|
|
75
|
+
assert(iframe, "iframe not created");
|
|
76
|
+
|
|
77
|
+
log("waiting for iframe to load");
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
return iframe.contentDocument && iframe.contentDocument.readyState === "complete";
|
|
80
|
+
}, { message: "iframe did not load" });
|
|
81
|
+
|
|
82
|
+
log("finding modal close button");
|
|
83
|
+
const closeButton = modal.querySelector("[data-pwc-action='close']");
|
|
84
|
+
assert(closeButton, "close button not found on modal");
|
|
85
|
+
|
|
86
|
+
log("clicking close button");
|
|
87
|
+
closeButton.click();
|
|
88
|
+
|
|
89
|
+
log("waiting for modal to close");
|
|
90
|
+
await waitFor(() => !modal.classList.contains("show"), {
|
|
91
|
+
message: "modal did not close after clicking close button"
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
log("done");
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
</body>
|
|
99
|
+
|
|
100
|
+
</html>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<title>dialog-opener-bs5 iframe target</title>
|
|
7
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
|
+
</head>
|
|
9
|
+
|
|
10
|
+
<body>
|
|
11
|
+
<h1>iframe target</h1>
|
|
12
|
+
<form method="get" action="./bs5-basic.html" class="container py-3">
|
|
13
|
+
<div class="mb-3">
|
|
14
|
+
<label for="first_name" class="form-label">First name</label>
|
|
15
|
+
<input id="first_name" name="first_name" value="John" class="form-control" />
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="mb-3">
|
|
19
|
+
<label for="last_name" class="form-label">Last name</label>
|
|
20
|
+
<input id="last_name" name="last_name" value="Doe" class="form-control" />
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<button type="submit" name="commit" value="ok" class="btn btn-primary">OK</button>
|
|
24
|
+
</form>
|
|
25
|
+
|
|
26
|
+
<script type="module">
|
|
27
|
+
const url = new URL(window.location.href);
|
|
28
|
+
const defaultData = url.searchParams.get("default");
|
|
29
|
+
if (defaultData) {
|
|
30
|
+
try {
|
|
31
|
+
const data = JSON.parse(defaultData);
|
|
32
|
+
document.querySelector("input[name=first_name]").value = data.firstName;
|
|
33
|
+
document.querySelector("input[name=last_name]").value = data.lastName;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error("Failed to parse default data:", e);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
</body>
|
|
40
|
+
|
|
41
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>dialog-opener iframe target</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1>iframe target</h1>
|
|
9
|
+
<form method="get" action="./basic.html">
|
|
10
|
+
<label>First name: </label><input name="first_name" value="John" /></label><br />
|
|
11
|
+
<label>Last name: </label><input name="last_name" value="Doe" /></label><br />
|
|
12
|
+
<button type="submit" name="commit" value="ok">OK</button>
|
|
13
|
+
</form>
|
|
14
|
+
<script type="module">
|
|
15
|
+
const url = new URL(window.location.href);
|
|
16
|
+
const defaultData = url.searchParams.get("default");
|
|
17
|
+
if (defaultData) {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(defaultData);
|
|
20
|
+
document.querySelector("input[name=first_name]").value = data.firstName;
|
|
21
|
+
document.querySelector("input[name=last_name]").value = data.lastName;
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.error("Failed to parse default data:", e);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>dialog-opener</title>
|
|
6
|
+
<link rel="stylesheet" href="../../../test/static/index.css">
|
|
7
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/zero-md@3?register"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<zero-md src="../README.md"></zero-md>
|
|
11
|
+
|
|
12
|
+
<h2>Weitere Links</h2>
|
|
13
|
+
<ul>
|
|
14
|
+
<li><a href="./basic.html" data-test-page>Test: basic</a></li>
|
|
15
|
+
<li><a href="./bs5-basic.html" data-test-page>Test: bs5-basic</a></li>
|
|
16
|
+
<li><a href="../INTERNALS.md">INTERNALS.md</a></li>
|
|
17
|
+
</ul>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
package/src/index-bs5.js
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Modal-Dialog — Internals
|
|
2
|
+
|
|
3
|
+
## Architecture
|
|
4
|
+
|
|
5
|
+
`ModalDialogBase` (`base.js`) provides orchestration for opening, closing, and stacking modals.
|
|
6
|
+
It defines a **subclass contract** — a set of hooks that variants must implement:
|
|
7
|
+
|
|
8
|
+
| Hook | Responsibility |
|
|
9
|
+
|------|---------------|
|
|
10
|
+
| `_render(ctx)` | Build DOM, return `{ rootEl, bodyEl, headerEl, footerEl, teardown? }` |
|
|
11
|
+
| `_getOpenSibling()` | Find an already-open modal (for stacking) |
|
|
12
|
+
| `_suspend(el)` / `_restore(el)` | Hide/show a sibling modal during stacking |
|
|
13
|
+
| `_show(ui, options)` / `_hide(ui)` | Actually show/hide the modal |
|
|
14
|
+
| `_armFinalClose(ui, onFinalClose)` | Wire up the "real close" callback |
|
|
15
|
+
|
|
16
|
+
The vanilla variant uses native `<dialog>` + `showModal()`.
|
|
17
|
+
The BS5 variant uses `bootstrap.Modal`.
|
|
18
|
+
|
|
19
|
+
## Open lifecycle
|
|
20
|
+
|
|
21
|
+
1. If the element is not in the DOM, it auto-appends to `<body>` (and auto-removes on close)
|
|
22
|
+
2. Any previous UI is torn down
|
|
23
|
+
3. `_render()` builds fresh DOM and returns the `ui` object
|
|
24
|
+
4. `_getOpenSibling()` checks for a currently open modal
|
|
25
|
+
5. If a sibling exists → `_suspend()` hides it (marked as `closeReason=suspend`)
|
|
26
|
+
6. `_armFinalClose()` registers the close callback
|
|
27
|
+
7. `_show()` makes the modal visible
|
|
28
|
+
|
|
29
|
+
## Stacking
|
|
30
|
+
|
|
31
|
+
When a second modal opens while the first is visible:
|
|
32
|
+
|
|
33
|
+
- The first modal is suspended (hidden but not destroyed)
|
|
34
|
+
- Its `dataset.closeReason` is set to `"suspend"` so close handlers know to ignore it
|
|
35
|
+
- When the second modal closes, `_onFinalClose()` restores the first via `_restore()`
|
|
36
|
+
- The restore happens in a `queueMicrotask` to avoid event ordering issues
|
|
37
|
+
|
|
38
|
+
## Close detection
|
|
39
|
+
|
|
40
|
+
The tricky part is distinguishing a **final close** (user dismissed the dialog) from a
|
|
41
|
+
**suspend close** (another modal is temporarily hiding this one).
|
|
42
|
+
|
|
43
|
+
- Vanilla: listens to the `<dialog>` `close` event, checks `dataset.closeReason`
|
|
44
|
+
- BS5: listens to `hidden.bs.modal`, same check
|
|
45
|
+
|
|
46
|
+
Only a final close triggers teardown and parent restoration.
|
|
47
|
+
|
|
48
|
+
## Close triggers
|
|
49
|
+
|
|
50
|
+
Handled in `ModalDialogBase.handleEvent()`:
|
|
51
|
+
|
|
52
|
+
- Click on `[data-pwc-action="close"]` → `close()`
|
|
53
|
+
- Click directly on `ui.rootEl` (backdrop) → `close()`
|
|
54
|
+
|
|
55
|
+
`close()` sets `dataset.closeReason = "final"` and calls `_hide()`.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# `<pwc-modal-dialog>`
|
|
2
|
+
|
|
3
|
+
Minimal modal dialog web component.
|
|
4
|
+
|
|
5
|
+
`<pwc-modal-dialog>` provides a small, explicit API to open modal dialogs from JavaScript.
|
|
6
|
+
It is designed as a low-level building block and does **not** manage navigation, iframes,
|
|
7
|
+
or application flow on its own.
|
|
8
|
+
|
|
9
|
+
The component does **not** use Shadow DOM and relies on regular DOM structure and CSS hooks.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Basic usage
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<pwc-modal-dialog></pwc-modal-dialog>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
const dialog = document.querySelector("pwc-modal-dialog");
|
|
21
|
+
|
|
22
|
+
dialog.open({ title: "Example" });
|
|
23
|
+
dialog.bodyEl.innerHTML = "<p>Hello world</p>";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Calling `open()` renders and shows the dialog. Content is populated **after** opening.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## JavaScript API
|
|
31
|
+
|
|
32
|
+
### `open(options)`
|
|
33
|
+
|
|
34
|
+
Opens the dialog.
|
|
35
|
+
|
|
36
|
+
Supported options:
|
|
37
|
+
- `title` – dialog title text
|
|
38
|
+
- `size` – size token (implementation-specific)
|
|
39
|
+
- `closeText` – accessible label for close actions
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
dialog.open({
|
|
43
|
+
title: "Edit item",
|
|
44
|
+
size: "lg",
|
|
45
|
+
closeText: "Cancel"
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `close()`
|
|
50
|
+
|
|
51
|
+
Closes the dialog programmatically.
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
dialog.close();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Content accessors
|
|
60
|
+
|
|
61
|
+
After `open()` the following properties are available:
|
|
62
|
+
|
|
63
|
+
- `bodyEl` – main content container
|
|
64
|
+
- `headerEl` – header container
|
|
65
|
+
- `footerEl` – footer container
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
dialog.bodyEl.append(form);
|
|
69
|
+
dialog.footerEl.append(button);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Accessing these before `open()` throws an error.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Close behavior
|
|
77
|
+
|
|
78
|
+
The dialog closes when:
|
|
79
|
+
|
|
80
|
+
- `close()` is called
|
|
81
|
+
- an element with `data-pwc-action="close"` is clicked
|
|
82
|
+
- the backdrop is clicked (implementation dependent)
|
|
83
|
+
|
|
84
|
+
Example close button:
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<button data-pwc-action="close">Close</button>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Stacking behavior
|
|
93
|
+
|
|
94
|
+
If a dialog is opened while another dialog is already open:
|
|
95
|
+
|
|
96
|
+
- the parent dialog is temporarily suspended
|
|
97
|
+
- the child dialog is shown
|
|
98
|
+
- when the child closes, the parent is restored
|
|
99
|
+
|
|
100
|
+
This works for both vanilla and Bootstrap variants.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Styling (vanilla)
|
|
105
|
+
|
|
106
|
+
The vanilla implementation uses the native `<dialog>` element.
|
|
107
|
+
|
|
108
|
+
Key CSS hooks:
|
|
109
|
+
|
|
110
|
+
- `pwc-modal-dialog`
|
|
111
|
+
- `.pwc-modal-dialog-surface`
|
|
112
|
+
- `.pwc-modal-dialog-header`
|
|
113
|
+
- `.pwc-modal-dialog-body`
|
|
114
|
+
- `.pwc-modal-dialog-footer`
|
|
115
|
+
|
|
116
|
+
Custom properties are defined on the host element:
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
pwc-modal-dialog {
|
|
120
|
+
--pwc-modal-width: 640px;
|
|
121
|
+
--pwc-modal-padding: 1rem;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Bootstrap 5 variant
|
|
128
|
+
|
|
129
|
+
A Bootstrap 5 based implementation is available:
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<pwc-modal-dialog-bs5></pwc-modal-dialog-bs5>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Notes:
|
|
136
|
+
- Same JavaScript API as the vanilla component
|
|
137
|
+
- Uses Bootstrap modal markup and classes
|
|
138
|
+
- No additional close button is injected
|
|
139
|
+
- Requires Bootstrap 5 JavaScript and CSS to be present globally
|