@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
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.1",
|
|
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",
|
|
@@ -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()`.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# `<pwc-dialog-opener>`
|
|
2
|
+
|
|
3
|
+
Server-first dialog opener web component.
|
|
4
|
+
|
|
5
|
+
`<pwc-dialog-opener>` enhances existing links to open their targets inside a modal dialog.
|
|
6
|
+
It is designed to work with server-rendered HTML and minimal JavaScript.
|
|
7
|
+
|
|
8
|
+
The component does **not** use Shadow DOM and relies on stable, explicit markup hooks.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Basic usage
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<pwc-dialog-opener>
|
|
16
|
+
<a href="/path/to/page">Open</a>
|
|
17
|
+
</pwc-dialog-opener>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Clicking the link opens the target URL inside a dialog using an `<iframe>`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Attributes
|
|
25
|
+
|
|
26
|
+
### `close`
|
|
27
|
+
Text for the close button inside the dialog.
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<pwc-dialog-opener close="Cancel">
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `move-out`
|
|
34
|
+
Moves action buttons from the iframe content into the dialog footer.
|
|
35
|
+
|
|
36
|
+
Supported magic values:
|
|
37
|
+
- `submit`
|
|
38
|
+
- `primary` – **Bootstrap 5 variant only**
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<pwc-dialog-opener move-out="submit">
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `local-reload`
|
|
45
|
+
Enables server-side fragment reload instead of full page navigation when the dialog
|
|
46
|
+
signals completion.
|
|
47
|
+
|
|
48
|
+
Supported tokens (space-separated):
|
|
49
|
+
- `with-scripts` – re-executes inline scripts in the replaced fragment
|
|
50
|
+
- `replace-url` – updates the browser URL via `history.replaceState`
|
|
51
|
+
- `push-url` – updates the browser URL via `history.pushState`
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<pwc-dialog-opener id="editor" local-reload="with-scripts replace-url">
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The element **must have an `id`** for `local-reload` to work.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Dialog completion protocol
|
|
62
|
+
|
|
63
|
+
The iframe content is considered "finished" when its URL contains the query parameter:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
dialog_finished_with=ok
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
When detected:
|
|
70
|
+
- the dialog closes
|
|
71
|
+
- local reload is attempted (if enabled)
|
|
72
|
+
- otherwise a full page navigation is performed
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## `default` query parameter
|
|
77
|
+
|
|
78
|
+
When opening the iframe URL, the component collects values from the **first-level inputs inside**
|
|
79
|
+
`<pwc-dialog-opener>` and appends them as a `default` query parameter.
|
|
80
|
+
|
|
81
|
+
This mechanism is essential for server-driven workflows where the dialog is used as a
|
|
82
|
+
fallback or completion step.
|
|
83
|
+
|
|
84
|
+
A common example is an autocomplete or reference selector:
|
|
85
|
+
- the user types a value
|
|
86
|
+
- no matching target exists yet
|
|
87
|
+
- the dialog opens a “create” form
|
|
88
|
+
- the previously entered value is forwarded as a default
|
|
89
|
+
|
|
90
|
+
Current behavior:
|
|
91
|
+
- reads the values of all `input` elements inside the component
|
|
92
|
+
- ignores empty values
|
|
93
|
+
- concatenates values from multiple inputs into a single `default` query parameter
|
|
94
|
+
|
|
95
|
+
```html
|
|
96
|
+
<pwc-dialog-opener>
|
|
97
|
+
<input name="search" value="bar">
|
|
98
|
+
<a href="/teams/new">Open</a>
|
|
99
|
+
</pwc-dialog-opener>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The iframe will be opened with:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
/teams/new?default=bar&_layout=false
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Styling
|
|
111
|
+
|
|
112
|
+
The vanilla variant uses the native `<dialog>` element.
|
|
113
|
+
|
|
114
|
+
Key CSS hooks:
|
|
115
|
+
- `.pwc-dialog-opener-modal`
|
|
116
|
+
- `.pwc-dialog-opener-body`
|
|
117
|
+
- `.pwc-dialog-opener-footer`
|
|
118
|
+
- `.pwc-dialog-opener-close`
|
|
119
|
+
|
|
120
|
+
Height can be controlled via CSS variables on the host element:
|
|
121
|
+
|
|
122
|
+
```css
|
|
123
|
+
pwc-dialog-opener {
|
|
124
|
+
--pwc-dialog-opener-height: 550px;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Bootstrap 5 variant
|
|
131
|
+
|
|
132
|
+
A Bootstrap 5 styled variant is provided with the same API:
|
|
133
|
+
|
|
134
|
+
```html
|
|
135
|
+
<pwc-dialog-opener-bs5>
|
|
136
|
+
<a href="/path/to/page">Open</a>
|
|
137
|
+
</pwc-dialog-opener-bs5>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Notes:
|
|
141
|
+
- Same attributes and behavior as the vanilla component
|
|
142
|
+
- Uses Bootstrap modal markup and classes
|
|
143
|
+
- Requires Bootstrap 5 JavaScript and CSS to be present
|
|
144
|
+
|
|
145
|
+
---
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { PwcElement } from "../core/pwc-element.js";
|
|
2
|
+
|
|
3
|
+
export class BaseDialogOpener extends PwcElement {
|
|
4
|
+
static events = ["click"];
|
|
5
|
+
|
|
6
|
+
handleEvent(e) {
|
|
7
|
+
if (e.type !== "click") return;
|
|
8
|
+
if (e.defaultPrevented) return;
|
|
9
|
+
|
|
10
|
+
const link = e.target.closest("a");
|
|
11
|
+
if (!link || !this.contains(link)) return;
|
|
12
|
+
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
|
|
15
|
+
if (this.hasAttribute("local-reload") && !this.id) {
|
|
16
|
+
console.warn("<pwc-dialog-opener> has local-reload attribute but no id", this);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const href = link.getAttribute("href");
|
|
20
|
+
if (!href) return;
|
|
21
|
+
|
|
22
|
+
this.open(href);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
open(href) {
|
|
26
|
+
const src = this.prepareIFrameLink(href);
|
|
27
|
+
this.findOrCreateDialog(src);
|
|
28
|
+
this.enhanceIFrame().then(() => this.modal.show());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
prepareIFrameLink(src) {
|
|
32
|
+
const s = new URL(src, document.location.href);
|
|
33
|
+
|
|
34
|
+
const defaultValues = [...this.querySelectorAll("input")]
|
|
35
|
+
.map((input) => {
|
|
36
|
+
if (input.value) return input.value;
|
|
37
|
+
return null;
|
|
38
|
+
})
|
|
39
|
+
.filter((item) => item !== null);
|
|
40
|
+
|
|
41
|
+
if (defaultValues.length > 0) {
|
|
42
|
+
s.searchParams.set("default", defaultValues.join(","));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
s.searchParams.set("_layout", false);
|
|
46
|
+
return s.toString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Variant hook: must set this.dialog and this.modal
|
|
50
|
+
// eslint-disable-next-line no-unused-vars
|
|
51
|
+
findOrCreateDialog(_src) {
|
|
52
|
+
throw new Error("BaseDialogOpener: findOrCreateDialog(src) must be implemented by a variant");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
createIFrame(src) {
|
|
56
|
+
const iframe = document.createElement("iframe");
|
|
57
|
+
iframe.src = src;
|
|
58
|
+
|
|
59
|
+
iframe.style.width = "100%";
|
|
60
|
+
iframe.style.height = getComputedStyle(this).getPropertyValue("--pwc-dialog-opener-height").trim() || "550px";
|
|
61
|
+
iframe.style.display = "none";
|
|
62
|
+
|
|
63
|
+
return iframe;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
enhanceIFrame() {
|
|
67
|
+
this.iframe = this.dialog.querySelector("iframe");
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
this.iframe.addEventListener("load",
|
|
70
|
+
(e) => this.iFrameLoad(e).then(resolve));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async iFrameLoad(_e) {
|
|
75
|
+
let uri;
|
|
76
|
+
try {
|
|
77
|
+
uri = new URL(this.iframe.contentWindow.location);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
throw new Error(`<pwc-dialog-opener> cannot access iframe location (cross-origin?): ${e.message}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (uri.searchParams.has("dialog_finished_with")) {
|
|
83
|
+
this.modal.hide();
|
|
84
|
+
|
|
85
|
+
uri.searchParams.delete("_layout");
|
|
86
|
+
uri.searchParams.set("dummy", Math.floor(Math.random() * 100000));
|
|
87
|
+
|
|
88
|
+
const localReloadWorked = await this.tryLocalReload(uri);
|
|
89
|
+
if (!localReloadWorked) {
|
|
90
|
+
window.location.href = uri.toString();
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.moveElementsToOuterActions();
|
|
96
|
+
this.iframe.style.display = "unset";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async tryLocalReload(newUri) {
|
|
100
|
+
const currentUri = new URL(window.location.href);
|
|
101
|
+
if (
|
|
102
|
+
currentUri.hostname !== newUri.hostname ||
|
|
103
|
+
currentUri.pathname !== newUri.pathname ||
|
|
104
|
+
currentUri.protocol !== newUri.protocol
|
|
105
|
+
) {
|
|
106
|
+
console.log(`<dialog-opener> Warning: local-reload got different base uri (${newUri.toString()}) then window has (${currentUri.toString()}). This might lead to problems, but we'll try it anyway.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (this.hasAttribute("local-reload") && this.id) {
|
|
110
|
+
const localReloadOptionTokens = document.createElement("div").classList;
|
|
111
|
+
if (this.hasAttribute("local-reload")) localReloadOptionTokens.add(...this.getAttribute("local-reload").split(/\s+/));
|
|
112
|
+
const localReloadOptions = {
|
|
113
|
+
replaceUrl: localReloadOptionTokens.contains("replace-url"),
|
|
114
|
+
pushUrl: localReloadOptionTokens.contains("push-url"),
|
|
115
|
+
withScripts: localReloadOptionTokens.contains("with-scripts")
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
newUri.searchParams.set("local_reload", this.id);
|
|
119
|
+
|
|
120
|
+
const res = await fetch(newUri);
|
|
121
|
+
if (res.ok) {
|
|
122
|
+
const html = await res.text();
|
|
123
|
+
const newDocument = new DOMParser().parseFromString(html, "text/html");
|
|
124
|
+
const fragment = newDocument.getElementById(this.id);
|
|
125
|
+
|
|
126
|
+
if (fragment) {
|
|
127
|
+
this.replaceChildren(...fragment.childNodes);
|
|
128
|
+
|
|
129
|
+
// Optional History API update
|
|
130
|
+
if (localReloadOptions.replaceUrl || localReloadOptions.pushUrl) {
|
|
131
|
+
|
|
132
|
+
if (localReloadOptions.pushUrl) {
|
|
133
|
+
history.pushState(null, "", newUri);
|
|
134
|
+
}
|
|
135
|
+
else if (localReloadOptions.replaceUrl) {
|
|
136
|
+
history.replaceState(null, "", newUri);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (localReloadOptions.withScripts) {
|
|
141
|
+
this.executeInlineScripts(this);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.dispatchEvent(
|
|
145
|
+
new CustomEvent("pwc-dialog-opener:local-reload", {
|
|
146
|
+
bubbles: true,
|
|
147
|
+
detail: { url: newUri.toString() }
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
console.log("local-reload not possible, falling back to full reload");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
executeInlineScripts(root) {
|
|
161
|
+
console.log("Executing inline scripts in local-reload fragment", root);
|
|
162
|
+
const scripts = Array.from(root.querySelectorAll("script"));
|
|
163
|
+
|
|
164
|
+
for (const old of scripts) {
|
|
165
|
+
if (old.src) {
|
|
166
|
+
console.warn("Ignoring external script in local-reload fragment:", old.src);
|
|
167
|
+
old.remove();
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Re-create script to execute it
|
|
172
|
+
const s = document.createElement("script");
|
|
173
|
+
// preserve type if present (default is classic)
|
|
174
|
+
if (old.type) s.type = old.type;
|
|
175
|
+
if (old.noModule) s.noModule = true;
|
|
176
|
+
|
|
177
|
+
s.textContent = old.textContent || "";
|
|
178
|
+
|
|
179
|
+
old.replaceWith(s);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
moveElementsToOuterActions() {
|
|
184
|
+
if (!this.getAttribute("move-out")) return;
|
|
185
|
+
|
|
186
|
+
const iframeDoc = this.iframe.contentWindow.document;
|
|
187
|
+
if (!iframeDoc) return;
|
|
188
|
+
|
|
189
|
+
let buttonContainer = this.dialog.querySelector("dialog-opener-buttons");
|
|
190
|
+
if (!buttonContainer) {
|
|
191
|
+
buttonContainer = document.createElement("dialog-opener-buttons");
|
|
192
|
+
this.dialog.querySelector(".pwc-dialog-opener-actions").prepend(buttonContainer);
|
|
193
|
+
} else {
|
|
194
|
+
buttonContainer.innerHTML = "";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const elements = iframeDoc.querySelectorAll(this._moveOutSelector());
|
|
198
|
+
for (let i = 0; i < elements.length; i++) {
|
|
199
|
+
const btn = elements[i];
|
|
200
|
+
|
|
201
|
+
const outerBtn = document.createElement(btn.tagName);
|
|
202
|
+
for (const attr of btn.attributes) {
|
|
203
|
+
outerBtn.setAttribute(attr.name, attr.value);
|
|
204
|
+
}
|
|
205
|
+
outerBtn.innerHTML = btn.innerHTML;
|
|
206
|
+
|
|
207
|
+
outerBtn.addEventListener("click", () => {
|
|
208
|
+
this.iframe.style.display = "none";
|
|
209
|
+
btn.click();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
buttonContainer.append(outerBtn);
|
|
213
|
+
|
|
214
|
+
btn.style.visibility = "hidden";
|
|
215
|
+
btn.style.display = "none";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_moveOutSelector() {
|
|
220
|
+
let selector = this.getAttribute("move-out");
|
|
221
|
+
if (selector === "submit") {
|
|
222
|
+
selector = "button[type=submit], input[type=submit]";
|
|
223
|
+
}
|
|
224
|
+
return selector;
|
|
225
|
+
}
|
|
226
|
+
}
|