katalyst-kpop 3.4.0 → 4.0.0.beta.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.
- checksums.yaml +4 -4
- data/README.md +92 -74
- data/app/assets/builds/katalyst/kpop.esm.js +463 -457
- data/app/assets/builds/katalyst/kpop.js +463 -457
- data/app/assets/builds/katalyst/kpop.min.js +1 -1
- data/app/assets/builds/katalyst/kpop.min.js.map +1 -1
- data/app/assets/stylesheets/katalyst/kpop.css +69 -0
- data/app/components/kpop/frame_component.html.erb +3 -14
- data/app/components/kpop/frame_component.rb +15 -11
- data/app/components/kpop/modal_component.html.erb +7 -6
- data/app/components/kpop/modal_component.rb +9 -32
- data/app/controllers/concerns/katalyst/kpop/frame_request.rb +67 -8
- data/app/javascript/kpop/application.js +68 -7
- data/app/javascript/kpop/controllers/frame_controller.js +96 -66
- data/app/javascript/kpop/modals/content_modal.js +2 -58
- data/app/javascript/kpop/modals/frame_modal.js +19 -76
- data/app/javascript/kpop/modals/modal.js +96 -49
- data/app/javascript/kpop/modals/stream_modal.js +11 -62
- data/app/javascript/kpop/utils/debug.js +22 -0
- data/app/javascript/kpop/utils/link_observer.js +151 -0
- data/app/javascript/kpop/utils/ruleset.js +43 -0
- data/app/javascript/kpop/utils/stream_actions.js +21 -0
- data/app/views/layouts/kpop/frame.html.erb +3 -1
- data/app/views/layouts/kpop/stream.html.erb +3 -0
- data/lib/katalyst/kpop/engine.rb +1 -8
- data/lib/katalyst/kpop/matchers/modal_matcher.rb +1 -1
- data/lib/katalyst/kpop/matchers/src_matcher.rb +33 -0
- data/lib/katalyst/kpop/matchers.rb +11 -40
- metadata +8 -19
- data/app/assets/stylesheets/katalyst/kpop/_frame.scss +0 -90
- data/app/assets/stylesheets/katalyst/kpop/_modal.scss +0 -88
- data/app/assets/stylesheets/katalyst/kpop/_scrim.scss +0 -46
- data/app/assets/stylesheets/katalyst/kpop/_side_panel.scss +0 -64
- data/app/assets/stylesheets/katalyst/kpop/_variables.scss +0 -24
- data/app/assets/stylesheets/katalyst/kpop.scss +0 -6
- data/app/components/kpop/modal/footer_component.rb +0 -21
- data/app/components/kpop/modal/header_component.rb +0 -21
- data/app/components/kpop/modal/title_component.html.erb +0 -6
- data/app/components/kpop/modal/title_component.rb +0 -28
- data/app/components/scrim_component.rb +0 -32
- data/app/helpers/kpop_helper.rb +0 -32
- data/app/javascript/kpop/controllers/modal_controller.js +0 -30
- data/app/javascript/kpop/controllers/scrim_controller.js +0 -159
- data/app/javascript/kpop/debug.js +0 -3
- data/app/javascript/kpop/turbo_actions.js +0 -46
- data/app/javascript/kpop/utils/stream_renderer.js +0 -15
- data/lib/katalyst/kpop/turbo.rb +0 -49
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
|
3
|
-
import DEBUG from "../debug";
|
|
4
3
|
import { ContentModal } from "../modals/content_modal";
|
|
5
4
|
import { FrameModal } from "../modals/frame_modal";
|
|
6
5
|
|
|
6
|
+
import debug from "../utils/debug";
|
|
7
|
+
|
|
7
8
|
export default class Kpop__FrameController extends Controller {
|
|
8
|
-
static outlets = ["scrim"];
|
|
9
|
-
static targets = ["modal"];
|
|
10
9
|
static values = {
|
|
11
10
|
open: Boolean,
|
|
12
11
|
};
|
|
@@ -19,12 +18,14 @@ export default class Kpop__FrameController extends Controller {
|
|
|
19
18
|
// allow our code to intercept frame navigation requests before dom changes
|
|
20
19
|
installNavigationInterception(this);
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
const dialog = this.element.querySelector("dialog");
|
|
22
|
+
|
|
23
|
+
if (this.element.src && dialog) {
|
|
23
24
|
this.debug("new frame modal", this.element.src);
|
|
24
|
-
FrameModal.connect(this, this.element);
|
|
25
|
-
} else if (
|
|
26
|
-
this.debug("new content modal",
|
|
27
|
-
ContentModal.connect(this,
|
|
25
|
+
FrameModal.connect(this, dialog, this.element.src).then(() => {});
|
|
26
|
+
} else if (dialog) {
|
|
27
|
+
this.debug("new content modal", dialog);
|
|
28
|
+
ContentModal.connect(this, dialog);
|
|
28
29
|
} else {
|
|
29
30
|
this.debug("no modal");
|
|
30
31
|
this.clear();
|
|
@@ -38,24 +39,18 @@ export default class Kpop__FrameController extends Controller {
|
|
|
38
39
|
delete this.modal;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
scrimOutletConnected(scrim) {
|
|
42
|
-
this.debug("scrim-connected");
|
|
43
|
-
|
|
44
|
-
this.scrimConnected = true;
|
|
45
|
-
|
|
46
|
-
if (this.openValue) {
|
|
47
|
-
scrim.show({ animate: false });
|
|
48
|
-
} else {
|
|
49
|
-
scrim.hide({ animate: false });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
42
|
openValueChanged(open) {
|
|
54
43
|
this.debug("open-changed", open);
|
|
55
|
-
|
|
56
|
-
this.element.parentElement.style.display = open ? "flex" : "none";
|
|
57
44
|
}
|
|
58
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Animate an attached modal into the foreground. Returns a promise that
|
|
48
|
+
* resolves when the animation is complete.
|
|
49
|
+
*
|
|
50
|
+
* @param modal
|
|
51
|
+
* @param animate
|
|
52
|
+
* @returns {Promise<Boolean>}
|
|
53
|
+
*/
|
|
59
54
|
async open(modal, { animate = true } = {}) {
|
|
60
55
|
if (this.isOpen) {
|
|
61
56
|
this.debug("skip open as already open");
|
|
@@ -65,12 +60,25 @@ export default class Kpop__FrameController extends Controller {
|
|
|
65
60
|
|
|
66
61
|
await this.dismissing;
|
|
67
62
|
|
|
68
|
-
return (this.opening ||=
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
return (this.opening ||= Promise.resolve().then(() => {
|
|
64
|
+
modal.connect();
|
|
65
|
+
return this.#open(modal, { animate });
|
|
66
|
+
}));
|
|
71
67
|
}
|
|
72
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Cause a modal to hide. Returns a promise that will resolve when the
|
|
71
|
+
* animation (if requested) is finished.
|
|
72
|
+
*
|
|
73
|
+
* If the modal is already animating out, returns the existing promise instead.
|
|
74
|
+
*
|
|
75
|
+
* @param {Boolean} animate
|
|
76
|
+
* @param {String} reason
|
|
77
|
+
* @returns {Promise}
|
|
78
|
+
*/
|
|
73
79
|
async dismiss({ animate = true, reason = "" } = {}) {
|
|
80
|
+
this.debug("event:dismiss", reason);
|
|
81
|
+
|
|
74
82
|
if (!this.isOpen) {
|
|
75
83
|
this.debug("skip dismiss as already closed");
|
|
76
84
|
return false;
|
|
@@ -78,36 +86,30 @@ export default class Kpop__FrameController extends Controller {
|
|
|
78
86
|
|
|
79
87
|
await this.opening;
|
|
80
88
|
|
|
81
|
-
return (this.dismissing ||= this.#
|
|
82
|
-
this.#dismiss({ animate, reason }),
|
|
83
|
-
));
|
|
89
|
+
return (this.dismissing ||= this.#dismiss({ animate, reason }));
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Clean up after a modal is finished dismissing.
|
|
94
|
+
*/
|
|
95
|
+
clear({ reason = "" } = {}) {
|
|
96
|
+
this.debug("event:clear", reason);
|
|
97
|
+
|
|
87
98
|
// clear the src from the frame (if any)
|
|
88
99
|
this.element.src = "";
|
|
100
|
+
this.element.innerHTML = "";
|
|
89
101
|
|
|
90
|
-
//
|
|
91
|
-
this.modalElements.forEach((element) => element.remove());
|
|
92
|
-
|
|
93
|
-
// mark the modal as hidden (will hide scrim on connect)
|
|
102
|
+
// mark the modal as hidden
|
|
94
103
|
this.openValue = false;
|
|
95
104
|
|
|
96
|
-
// close the scrim, if connected
|
|
97
|
-
if (this.scrimConnected) {
|
|
98
|
-
return this.scrimOutlet.hide({ animate: false });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
105
|
// unset modal
|
|
102
|
-
this.modal
|
|
106
|
+
if (this.modal) this.modal.disconnect();
|
|
107
|
+
delete this.modal;
|
|
108
|
+
delete this.dismissing;
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
// EVENTS
|
|
106
112
|
|
|
107
|
-
popstate(event) {
|
|
108
|
-
this.modal?.popstate(this, event);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
113
|
/**
|
|
112
114
|
* Incoming frame render, dismiss the current modal (if any) first.
|
|
113
115
|
*
|
|
@@ -151,53 +153,72 @@ export default class Kpop__FrameController extends Controller {
|
|
|
151
153
|
// ignore visits to the current frame, these fire when the frame navigates
|
|
152
154
|
if (e.detail.url === this.element.src) return;
|
|
153
155
|
|
|
156
|
+
const url = new URL(e.detail.url.toString(), document.baseURI);
|
|
157
|
+
if (url.pathname === "/resume_historical_location") {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
return this.dismiss();
|
|
160
|
+
}
|
|
161
|
+
|
|
154
162
|
// ignore unless we're open
|
|
155
163
|
if (!this.isOpen) return;
|
|
156
164
|
|
|
157
165
|
this.modal.beforeVisit(this, e);
|
|
158
166
|
}
|
|
159
167
|
|
|
160
|
-
frameLoad(
|
|
168
|
+
frameLoad(e) {
|
|
161
169
|
this.debug("frame-load");
|
|
162
170
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
window.addEventListener(
|
|
166
|
-
"turbo:visit",
|
|
167
|
-
(e) => {
|
|
168
|
-
this.open(modal, { animate: true });
|
|
169
|
-
},
|
|
170
|
-
{ once: true },
|
|
171
|
+
FrameModal.load(this, e.target.firstElementChild, e.target.src).then(
|
|
172
|
+
() => {},
|
|
171
173
|
);
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Outgoing fetch request. Capture the initiator so we can return focus if it causes a modal to show.
|
|
178
|
+
*/
|
|
179
|
+
beforeFetchRequest() {
|
|
180
|
+
const focusElement = document.activeElement;
|
|
181
|
+
|
|
182
|
+
if (focusElement === document.body) {
|
|
183
|
+
delete this.lastFetchFocusRef;
|
|
184
|
+
} else {
|
|
185
|
+
this.lastFetchFocusRef = new WeakRef(focusElement);
|
|
186
|
+
}
|
|
176
187
|
}
|
|
177
188
|
|
|
178
|
-
get
|
|
179
|
-
return this.
|
|
189
|
+
get isOpen() {
|
|
190
|
+
return this.openValue && !this.dismissing;
|
|
180
191
|
}
|
|
181
192
|
|
|
182
193
|
async #open(modal, { animate = true } = {}) {
|
|
183
194
|
this.debug("open-start", { animate });
|
|
184
195
|
|
|
185
|
-
|
|
196
|
+
this.previousFocusRef =
|
|
197
|
+
document.activeElement === document.body
|
|
198
|
+
? this.lastFetchFocusRef
|
|
199
|
+
: new WeakRef(document.activeElement);
|
|
200
|
+
this.debug("capture focus", this.previousFocusRef?.deref());
|
|
186
201
|
|
|
187
202
|
this.modal = modal;
|
|
188
203
|
this.openValue = true;
|
|
189
204
|
|
|
205
|
+
// Set turbo-frame[src] without causing a load event
|
|
206
|
+
this.element.delegate.sourceURL = this.modal.src;
|
|
207
|
+
|
|
190
208
|
await modal.open({ animate });
|
|
191
|
-
await scrim?.show({ animate });
|
|
192
209
|
|
|
193
210
|
delete this.opening;
|
|
194
211
|
|
|
195
212
|
this.debug("open-end");
|
|
196
213
|
|
|
214
|
+
autofocus(this.modal?.element)?.focus();
|
|
215
|
+
|
|
197
216
|
// Detect https://github.com/hotwired/turbo-rails/issues/580
|
|
198
217
|
if (Turbo.session.view.forceReloaded) {
|
|
199
218
|
console.error("Turbo-Frame response is incompatible with current page");
|
|
200
219
|
}
|
|
220
|
+
|
|
221
|
+
return true;
|
|
201
222
|
}
|
|
202
223
|
|
|
203
224
|
async #dismiss({ animate = true, reason = "" } = {}) {
|
|
@@ -211,15 +232,15 @@ export default class Kpop__FrameController extends Controller {
|
|
|
211
232
|
|
|
212
233
|
if (!this.modal) {
|
|
213
234
|
console.warn("modal missing on dismiss");
|
|
214
|
-
if (DEBUG) debugger;
|
|
215
235
|
}
|
|
216
236
|
|
|
217
|
-
await this.
|
|
218
|
-
await this.modal?.dismiss();
|
|
237
|
+
await this.modal?.dismiss({ animate });
|
|
219
238
|
|
|
220
|
-
this.
|
|
221
|
-
|
|
222
|
-
|
|
239
|
+
this.clear();
|
|
240
|
+
|
|
241
|
+
this.previousFocusRef?.deref()?.focus();
|
|
242
|
+
this.debug("restore focus", this.previousFocusRef?.deref());
|
|
243
|
+
delete this.previousFocusRef;
|
|
223
244
|
|
|
224
245
|
this.debug("dismiss-end");
|
|
225
246
|
}
|
|
@@ -228,8 +249,8 @@ export default class Kpop__FrameController extends Controller {
|
|
|
228
249
|
return new Promise(window.requestAnimationFrame).then(callback);
|
|
229
250
|
}
|
|
230
251
|
|
|
231
|
-
debug(
|
|
232
|
-
|
|
252
|
+
get debug() {
|
|
253
|
+
return debug("FrameController");
|
|
233
254
|
}
|
|
234
255
|
}
|
|
235
256
|
|
|
@@ -276,3 +297,12 @@ function installNavigationInterception(controller) {
|
|
|
276
297
|
}
|
|
277
298
|
};
|
|
278
299
|
}
|
|
300
|
+
|
|
301
|
+
function autofocus(container) {
|
|
302
|
+
if (!container) return null;
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
container.querySelector("[autofocus]") ??
|
|
306
|
+
container.querySelector("button:not([disabled])")
|
|
307
|
+
);
|
|
308
|
+
}
|
|
@@ -1,63 +1,7 @@
|
|
|
1
|
-
import { Turbo } from "@hotwired/turbo-rails";
|
|
2
|
-
|
|
3
1
|
import { Modal } from "./modal";
|
|
4
2
|
|
|
5
3
|
export class ContentModal extends Modal {
|
|
6
|
-
static connect(frame,
|
|
7
|
-
frame.open(new ContentModal(
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
constructor(id, src = null) {
|
|
11
|
-
super(id);
|
|
12
|
-
|
|
13
|
-
if (src) this.src = src;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* When the modal is dismissed we can't rely on a back navigation to close the
|
|
18
|
-
* modal as the user may have navigated to a different location. Instead we
|
|
19
|
-
* remove the content from the dom and replace the current history state with
|
|
20
|
-
* the fallback location, if set.
|
|
21
|
-
*
|
|
22
|
-
* If there is no fallback location, we may be showing a stream modal that was
|
|
23
|
-
* injected and cached by turbo. In this case, we clear the frame element and
|
|
24
|
-
* do not change history.
|
|
25
|
-
*
|
|
26
|
-
* @returns {Promise<void>}
|
|
27
|
-
*/
|
|
28
|
-
async dismiss() {
|
|
29
|
-
const fallbackLocation = this.fallbackLocationValue;
|
|
30
|
-
|
|
31
|
-
await super.dismiss();
|
|
32
|
-
|
|
33
|
-
if (this.visitStarted) {
|
|
34
|
-
this.debug("skipping dismiss, visit started");
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (!this.isCurrentLocation) {
|
|
38
|
-
this.debug("skipping dismiss, not current location");
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
this.frameElement.innerHTML = "";
|
|
43
|
-
|
|
44
|
-
if (fallbackLocation) {
|
|
45
|
-
window.history.replaceState(window.history.state, "", fallbackLocation);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
beforeVisit(frame, e) {
|
|
50
|
-
super.beforeVisit(frame, e);
|
|
51
|
-
|
|
52
|
-
this.visitStarted = true;
|
|
53
|
-
|
|
54
|
-
frame.scrimOutlet.hide({ animate: false });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
get src() {
|
|
58
|
-
return new URL(
|
|
59
|
-
this.currentLocationValue.toString(),
|
|
60
|
-
document.baseURI,
|
|
61
|
-
).toString();
|
|
4
|
+
static connect(frame, dialog) {
|
|
5
|
+
frame.open(new ContentModal(frame, dialog), { animate: false });
|
|
62
6
|
}
|
|
63
7
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Turbo } from "@hotwired/turbo-rails";
|
|
2
|
-
|
|
3
1
|
import { Modal } from "./modal";
|
|
4
2
|
|
|
5
3
|
export class FrameModal extends Modal {
|
|
@@ -7,25 +5,27 @@ export class FrameModal extends Modal {
|
|
|
7
5
|
* When the FrameController detects a frame element on connect, it runs this
|
|
8
6
|
* method to sanity check the frame src and restore the modal state.
|
|
9
7
|
*
|
|
10
|
-
* @param frame
|
|
11
|
-
* @param
|
|
8
|
+
* @param {Kpop__FrameController} frame
|
|
9
|
+
* @param {HTMLDialogElement} dialog
|
|
10
|
+
* @param {String} src
|
|
12
11
|
*/
|
|
13
|
-
static connect(frame,
|
|
14
|
-
|
|
12
|
+
static connect(frame, dialog, src) {
|
|
13
|
+
// restoration visit
|
|
14
|
+
this.debug("restore", src);
|
|
15
|
+
return frame.open(new FrameModal(frame, dialog, src), { animate: false });
|
|
16
|
+
}
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return frame.clear();
|
|
28
|
-
}
|
|
18
|
+
/**
|
|
19
|
+
* When the FrameController detects a frame load event, it runs this
|
|
20
|
+
* method to open the modal.
|
|
21
|
+
*
|
|
22
|
+
* @param {Kpop__FrameController} frame
|
|
23
|
+
* @param {HTMLDialogElement} dialog
|
|
24
|
+
* @param {String} src
|
|
25
|
+
*/
|
|
26
|
+
static load(frame, dialog, src) {
|
|
27
|
+
this.debug("load", src);
|
|
28
|
+
return frame.open(new FrameModal(frame, dialog, src), { animate: true });
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -48,64 +48,7 @@ export class FrameModal extends Modal {
|
|
|
48
48
|
element.src = "";
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
if (element.src === location) {
|
|
52
|
-
this.debug("skipping navigate as already on location");
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (element.src && element.src !== window.location.href) {
|
|
57
|
-
console.warn(
|
|
58
|
-
"kpop: frame src doesn't match window",
|
|
59
|
-
element.src,
|
|
60
|
-
window.location.href,
|
|
61
|
-
location,
|
|
62
|
-
);
|
|
63
|
-
frame.clear();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
51
|
this.debug("navigate to", location);
|
|
67
52
|
resolve();
|
|
68
53
|
}
|
|
69
|
-
|
|
70
|
-
constructor(id, src) {
|
|
71
|
-
super(id);
|
|
72
|
-
this.src = src;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* FrameModals are closed by running pop state and awaiting the turbo:load
|
|
77
|
-
* event that follows on history restoration.
|
|
78
|
-
*
|
|
79
|
-
* @returns {Promise<void>}
|
|
80
|
-
*/
|
|
81
|
-
async dismiss() {
|
|
82
|
-
await super.dismiss();
|
|
83
|
-
|
|
84
|
-
if (!this.isCurrentLocation) {
|
|
85
|
-
this.debug("skipping dismiss, not current location");
|
|
86
|
-
} else {
|
|
87
|
-
await this.pop("turbo:load", () => window.history.back());
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// no specific close action required, this is turbo's responsibility
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* When user navigates from inside a Frame modal, dismiss the modal first so
|
|
95
|
-
* that the modal does not appear in the history stack.
|
|
96
|
-
*
|
|
97
|
-
* @param frame FrameController
|
|
98
|
-
* @param e Turbo navigation event
|
|
99
|
-
*/
|
|
100
|
-
beforeVisit(frame, e) {
|
|
101
|
-
super.beforeVisit(frame, e);
|
|
102
|
-
|
|
103
|
-
e.preventDefault();
|
|
104
|
-
|
|
105
|
-
frame.dismiss({ animate: false }).then(() => {
|
|
106
|
-
Turbo.visit(e.detail.url);
|
|
107
|
-
|
|
108
|
-
this.debug("before-visit-end");
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
54
|
}
|
|
@@ -1,77 +1,124 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import DEBUG from "../debug";
|
|
1
|
+
import debug from "../utils/debug";
|
|
4
2
|
|
|
5
3
|
export class Modal {
|
|
6
|
-
constructor(
|
|
7
|
-
this.
|
|
4
|
+
constructor(frame, dialog, src = null) {
|
|
5
|
+
this.frame = frame;
|
|
6
|
+
this.element = dialog;
|
|
7
|
+
this.uri = new URL(src || dialog.dataset.src, window.location.origin);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
this.
|
|
10
|
+
connect() {
|
|
11
|
+
this.element.addEventListener("cancel", this.cancel);
|
|
12
|
+
this.element.addEventListener("close", this.close);
|
|
13
|
+
this.element.addEventListener("mousedown", this.scrim);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
this.
|
|
16
|
+
disconnect() {
|
|
17
|
+
this.element.removeEventListener("cancel", this.cancel);
|
|
18
|
+
this.element.removeEventListener("close", this.close);
|
|
19
|
+
this.element.removeEventListener("mousedown", this.scrim);
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
this.
|
|
22
|
+
get src() {
|
|
23
|
+
return this.uri.pathname + this.uri.search + this.uri.hash;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
this.debug(
|
|
24
|
-
}
|
|
26
|
+
cancel = (e) => {
|
|
27
|
+
this.debug("event:cancel", e);
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
this.debug(`pop`);
|
|
29
|
+
e.preventDefault();
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
event,
|
|
32
|
-
() => {
|
|
33
|
-
resolve();
|
|
34
|
-
},
|
|
35
|
-
{ once: true },
|
|
36
|
-
);
|
|
37
|
-
});
|
|
31
|
+
this.frame.dismiss({ animate: true, reason: "dialog:cancel" });
|
|
32
|
+
};
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
close = (e) => {
|
|
35
|
+
this.debug("event:close", e);
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
}
|
|
37
|
+
this.frame.clear({ reason: "dialog:close" });
|
|
38
|
+
};
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
scrim = (e) => {
|
|
41
|
+
if (e.target.tagName === "DIALOG") {
|
|
42
|
+
this.debug("event:scrim", e);
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
44
|
+
this.frame.dismiss({ animate: true, reason: "dialog:scrim" });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
48
|
+
async open({ animate = true } = {}) {
|
|
49
|
+
this.debug("open-start", animate);
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
return this.modalElement?.dataset["kpop-ModalCurrentLocationValue"] || "/";
|
|
58
|
-
}
|
|
51
|
+
await animation(this.element, animate, () => this.element.showModal());
|
|
59
52
|
|
|
60
|
-
|
|
61
|
-
return this.modalElement?.dataset["kpop-ModalFallbackLocationValue"];
|
|
53
|
+
this.debug("open-end");
|
|
62
54
|
}
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Modals are closed by animating out the modal then removing the modal
|
|
58
|
+
* element from the wrapping frame.
|
|
59
|
+
*
|
|
60
|
+
* @returns {Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
async dismiss({ animate = true } = {}) {
|
|
63
|
+
this.debug("dismiss-start", animate);
|
|
64
|
+
|
|
65
|
+
await animation(this.element, animate, () =>
|
|
66
|
+
this.element.removeAttribute("open"),
|
|
67
67
|
);
|
|
68
|
+
|
|
69
|
+
this.debug("dismiss-end");
|
|
70
|
+
|
|
71
|
+
this.element.close();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* When user navigates from inside a modal, dismiss the modal first so
|
|
76
|
+
* that the modal does not appear in the history stack.
|
|
77
|
+
*
|
|
78
|
+
* @param frame FrameController
|
|
79
|
+
* @param e Turbo navigation event
|
|
80
|
+
*/
|
|
81
|
+
beforeVisit(frame, e) {
|
|
82
|
+
this.debug(`before-visit`, e.detail.url);
|
|
83
|
+
|
|
84
|
+
this.frame.clear();
|
|
68
85
|
}
|
|
69
86
|
|
|
70
|
-
static debug(
|
|
71
|
-
|
|
87
|
+
static get debug() {
|
|
88
|
+
return debug(this.name);
|
|
72
89
|
}
|
|
73
90
|
|
|
74
|
-
debug(
|
|
75
|
-
|
|
91
|
+
get debug() {
|
|
92
|
+
return debug(this.constructor.name);
|
|
76
93
|
}
|
|
77
94
|
}
|
|
95
|
+
|
|
96
|
+
function animation(el, animate, trigger) {
|
|
97
|
+
if (!animate) return trigger();
|
|
98
|
+
|
|
99
|
+
const duration = animationDuration(el);
|
|
100
|
+
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const resolver = () => {
|
|
103
|
+
el.removeEventListener("animationend", resolver, { once: true });
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
el.toggleAttribute("animate", false);
|
|
106
|
+
resolve();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
el.addEventListener("animationend", resolver, { once: true });
|
|
110
|
+
const timeout = setTimeout(resolver, duration);
|
|
111
|
+
|
|
112
|
+
el.toggleAttribute("animate", animate);
|
|
113
|
+
trigger();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function animationDuration(el, defaultValue = "0.2s") {
|
|
118
|
+
const value =
|
|
119
|
+
getComputedStyle(el).getPropertyValue("--animation-duration") ||
|
|
120
|
+
defaultValue;
|
|
121
|
+
const num = parseFloat(value);
|
|
122
|
+
if (value.endsWith("ms")) return num;
|
|
123
|
+
return num * 1000;
|
|
124
|
+
}
|