katalyst-kpop 3.0.0.beta.7 → 3.0.0.beta.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/builds/katalyst/kpop.esm.js +200 -63
- data/app/assets/builds/katalyst/kpop.js +200 -63
- data/app/assets/builds/katalyst/kpop.min.js +1 -1
- data/app/assets/builds/katalyst/kpop.min.js.map +1 -1
- data/app/javascript/kpop/controllers/frame_controller.js +68 -50
- data/app/javascript/kpop/modals/content_modal.js +22 -5
- data/app/javascript/kpop/modals/frame_modal.js +77 -7
- data/app/javascript/kpop/modals/modal.js +9 -1
- data/app/javascript/kpop/modals/stream_modal.js +25 -0
- data/lib/katalyst/kpop/version.rb +1 -1
- metadata +1 -1
@@ -15,22 +15,19 @@ export default class Kpop__FrameController extends Controller {
|
|
15
15
|
this.debug("connect", this.element.src);
|
16
16
|
|
17
17
|
this.element.kpop = this;
|
18
|
-
installNavigationInterception(this.element, this.element.delegate);
|
19
18
|
|
20
|
-
//
|
19
|
+
// allow our code to intercept frame navigation requests before dom changes
|
20
|
+
installNavigationInterception(this);
|
21
|
+
|
21
22
|
if (this.element.src && this.element.complete) {
|
22
23
|
this.debug("new frame modal", this.element.src);
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
FrameModal.connect(this, this.element);
|
25
|
+
} else if (this.modalElements.length > 0) {
|
26
|
+
this.debug("new content modal", window.location.pathname);
|
27
|
+
ContentModal.connect(this, this.element);
|
26
28
|
} else {
|
27
|
-
|
28
|
-
|
29
|
-
);
|
30
|
-
if (element) {
|
31
|
-
this.debug("new content modal", window.location.pathname);
|
32
|
-
this.open(new ContentModal(this.element.id), { animate: false });
|
33
|
-
}
|
29
|
+
this.debug("no modal");
|
30
|
+
this.clear();
|
34
31
|
}
|
35
32
|
}
|
36
33
|
|
@@ -66,6 +63,8 @@ export default class Kpop__FrameController extends Controller {
|
|
66
63
|
return false;
|
67
64
|
}
|
68
65
|
|
66
|
+
await this.dismissing;
|
67
|
+
|
69
68
|
return (this.opening ||= this.#nextFrame(() =>
|
70
69
|
this.#open(modal, { animate })
|
71
70
|
));
|
@@ -77,46 +76,45 @@ export default class Kpop__FrameController extends Controller {
|
|
77
76
|
return false;
|
78
77
|
}
|
79
78
|
|
79
|
+
await this.opening;
|
80
|
+
|
80
81
|
return (this.dismissing ||= this.#nextFrame(() =>
|
81
82
|
this.#dismiss({ animate, reason })
|
82
83
|
));
|
83
84
|
}
|
84
85
|
|
85
|
-
|
86
|
+
async clear() {
|
87
|
+
// clear the src from the frame (if any)
|
88
|
+
this.element.src = "";
|
86
89
|
|
87
|
-
|
88
|
-
this.
|
89
|
-
}
|
90
|
+
// remove any open modal(s)
|
91
|
+
this.modalElements.forEach((element) => element.remove());
|
90
92
|
|
91
|
-
|
92
|
-
this.
|
93
|
+
// mark the modal as hidden (will hide scrim on connect)
|
94
|
+
this.openValue = false;
|
93
95
|
|
94
|
-
//
|
95
|
-
|
96
|
-
|
97
|
-
if (this.element.hasAttribute("busy")) {
|
98
|
-
this.debug("clearing src to cancel turbo request");
|
99
|
-
this.element.src = "";
|
96
|
+
// close the scrim, if connected
|
97
|
+
if (this.scrimConnected) {
|
98
|
+
return this.scrimOutlet.hide({ animate: false });
|
100
99
|
}
|
101
100
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
}
|
101
|
+
// unset modal
|
102
|
+
this.modal = null;
|
103
|
+
}
|
106
104
|
|
107
|
-
|
108
|
-
console.warn("kpop: frame src doesn't match window", this.element.src, window.location.href, location);
|
109
|
-
// clear src so that turbo doesn't cache the frame in a loading state
|
110
|
-
this.element.delegate.ignoringChangesToAttribute("src", (() => {
|
111
|
-
this.element.src = "";
|
112
|
-
this.element.delegate.complete = false;
|
113
|
-
}));
|
114
|
-
}
|
105
|
+
// EVENTS
|
115
106
|
|
116
|
-
|
117
|
-
|
107
|
+
popstate(event) {
|
108
|
+
this.modal?.popstate(this, event);
|
118
109
|
}
|
119
110
|
|
111
|
+
/**
|
112
|
+
* Incoming frame render, dismiss the current modal (if any) first.
|
113
|
+
*
|
114
|
+
* We're starting the actual visit
|
115
|
+
*
|
116
|
+
* @param event turbo:before-render
|
117
|
+
*/
|
120
118
|
beforeFrameRender(event) {
|
121
119
|
this.debug("before-frame-render", event.detail.newFrame.baseURI);
|
122
120
|
|
@@ -159,15 +157,25 @@ export default class Kpop__FrameController extends Controller {
|
|
159
157
|
frameLoad(event) {
|
160
158
|
this.debug("frame-load");
|
161
159
|
|
162
|
-
|
163
|
-
|
164
|
-
|
160
|
+
const modal = new FrameModal(this.element.id, this.element.src);
|
161
|
+
|
162
|
+
window.addEventListener(
|
163
|
+
"turbo:visit",
|
164
|
+
(e) => {
|
165
|
+
this.open(modal, { animate: true });
|
166
|
+
},
|
167
|
+
{ once: true }
|
168
|
+
);
|
165
169
|
}
|
166
170
|
|
167
171
|
get isOpen() {
|
168
172
|
return this.openValue && !this.dismissing;
|
169
173
|
}
|
170
174
|
|
175
|
+
get modalElements() {
|
176
|
+
return this.element.querySelectorAll("[data-controller*='kpop--modal']");
|
177
|
+
}
|
178
|
+
|
171
179
|
async #open(modal, { animate = true } = {}) {
|
172
180
|
this.debug("open-start", { animate });
|
173
181
|
|
@@ -226,14 +234,24 @@ export default class Kpop__FrameController extends Controller {
|
|
226
234
|
*
|
227
235
|
* See Turbo issue: https://github.com/hotwired/turbo/issues/1055
|
228
236
|
*
|
229
|
-
* @param
|
237
|
+
* @param controller FrameController
|
230
238
|
*/
|
231
|
-
function installNavigationInterception(
|
232
|
-
|
233
|
-
controller.
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
+
function installNavigationInterception(controller) {
|
240
|
+
const TurboFrameController =
|
241
|
+
controller.element.delegate.constructor.prototype;
|
242
|
+
|
243
|
+
if (TurboFrameController._navigateFrame) return;
|
244
|
+
|
245
|
+
TurboFrameController._navigateFrame = TurboFrameController.navigateFrame;
|
246
|
+
TurboFrameController.navigateFrame = function (element, url, submitter) {
|
247
|
+
const frame = this.findFrameElement(element, submitter);
|
248
|
+
|
249
|
+
if (frame.kpop) {
|
250
|
+
FrameModal.visit(url, frame.kpop, frame, () => {
|
251
|
+
TurboFrameController._navigateFrame.call(this, element, url, submitter);
|
252
|
+
});
|
253
|
+
} else {
|
254
|
+
TurboFrameController._navigateFrame.call(this, element, url, submitter);
|
255
|
+
}
|
256
|
+
};
|
239
257
|
}
|
@@ -3,13 +3,31 @@ import { Turbo } from "@hotwired/turbo-rails";
|
|
3
3
|
import { Modal } from "./modal";
|
4
4
|
|
5
5
|
export class ContentModal extends Modal {
|
6
|
+
static connect(frame, element) {
|
7
|
+
frame.open(new ContentModal(element.id), { animate: false });
|
8
|
+
}
|
9
|
+
|
6
10
|
constructor(id, src = null) {
|
7
11
|
super(id);
|
8
12
|
|
9
13
|
if (src) this.src = src;
|
10
14
|
}
|
11
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
|
+
*/
|
12
28
|
async dismiss() {
|
29
|
+
const fallbackLocation = this.fallbackLocationValue;
|
30
|
+
|
13
31
|
await super.dismiss();
|
14
32
|
|
15
33
|
if (this.visitStarted) {
|
@@ -21,12 +39,11 @@ export class ContentModal extends Modal {
|
|
21
39
|
return;
|
22
40
|
}
|
23
41
|
|
24
|
-
|
25
|
-
this.debug("turbo-visit", this.fallbackLocationValue);
|
26
|
-
Turbo.visit(this.fallbackLocationValue);
|
27
|
-
});
|
42
|
+
this.frameElement.innerHTML = "";
|
28
43
|
|
29
|
-
|
44
|
+
if (fallbackLocation) {
|
45
|
+
window.history.replaceState(window.history.state, "", fallbackLocation);
|
46
|
+
}
|
30
47
|
}
|
31
48
|
|
32
49
|
beforeVisit(frame, e) {
|
@@ -3,11 +3,81 @@ import { Turbo } from "@hotwired/turbo-rails";
|
|
3
3
|
import { Modal } from "./modal";
|
4
4
|
|
5
5
|
export class FrameModal extends Modal {
|
6
|
+
/**
|
7
|
+
* When the FrameController detects a frame element on connect, it runs this
|
8
|
+
* method to santity check the frame src and restore the modal state.
|
9
|
+
*
|
10
|
+
* @param frame FrameController
|
11
|
+
* @param element TurboFrame element
|
12
|
+
*/
|
13
|
+
static connect(frame, element) {
|
14
|
+
const modal = new FrameModal(element.id, element.src);
|
15
|
+
|
16
|
+
// state reconciliation for turbo restore of invalid frames
|
17
|
+
if (modal.isCurrentLocation) {
|
18
|
+
// restoration visit
|
19
|
+
this.debug("restore", element.src);
|
20
|
+
return frame.open(modal, { animate: false });
|
21
|
+
} else {
|
22
|
+
console.warn(
|
23
|
+
"kpop: restored frame src doesn't match window href",
|
24
|
+
modal.src,
|
25
|
+
window.location.href
|
26
|
+
);
|
27
|
+
return frame.clear();
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
/**
|
32
|
+
* When a user clicks a kpop link, turbo intercepts the click and calls
|
33
|
+
* navigateFrame on the turbo frame controller before setting the TurboFrame
|
34
|
+
* element's src attribute. KPOP intercepts this call and calls this method
|
35
|
+
* first so we cancel problematic navigations that might cache invalid states.
|
36
|
+
*
|
37
|
+
* @param location URL requested by turbo
|
38
|
+
* @param frame FrameController
|
39
|
+
* @param element TurboFrame element
|
40
|
+
* @param resolve continuation chain
|
41
|
+
*/
|
42
|
+
static visit(location, frame, element, resolve) {
|
43
|
+
// Ensure that turbo doesn't cache the frame in a loading state by cancelling
|
44
|
+
// the current request (if any) by clearing the src.
|
45
|
+
// Known issue: this won't work if the frame was previously rendering a useful src.
|
46
|
+
if (element.hasAttribute("busy")) {
|
47
|
+
this.debug("clearing src to cancel turbo request");
|
48
|
+
element.src = "";
|
49
|
+
}
|
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
|
+
this.debug("navigate to", location);
|
67
|
+
resolve();
|
68
|
+
}
|
69
|
+
|
6
70
|
constructor(id, src) {
|
7
71
|
super(id);
|
8
72
|
this.src = src;
|
9
73
|
}
|
10
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
|
+
*/
|
11
81
|
async dismiss() {
|
12
82
|
await super.dismiss();
|
13
83
|
|
@@ -20,6 +90,13 @@ export class FrameModal extends Modal {
|
|
20
90
|
// no specific close action required, this is turbo's responsibility
|
21
91
|
}
|
22
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
|
+
*/
|
23
100
|
beforeVisit(frame, e) {
|
24
101
|
super.beforeVisit(frame, e);
|
25
102
|
|
@@ -31,11 +108,4 @@ export class FrameModal extends Modal {
|
|
31
108
|
this.debug("before-visit-end");
|
32
109
|
});
|
33
110
|
}
|
34
|
-
|
35
|
-
popstate(frame, e) {
|
36
|
-
super.popstate(frame, e);
|
37
|
-
|
38
|
-
// Turbo will restore modal state, but we need to reset the scrim
|
39
|
-
frame.scrimOutlet.hide({ animate: false });
|
40
|
-
}
|
41
111
|
}
|
@@ -45,6 +45,10 @@ export class Modal {
|
|
45
45
|
return document.getElementById(this.id);
|
46
46
|
}
|
47
47
|
|
48
|
+
get controller() {
|
49
|
+
return this.frameElement?.kpop;
|
50
|
+
}
|
51
|
+
|
48
52
|
get modalElement() {
|
49
53
|
return this.frameElement?.querySelector("[data-controller*='kpop--modal']");
|
50
54
|
}
|
@@ -54,7 +58,7 @@ export class Modal {
|
|
54
58
|
}
|
55
59
|
|
56
60
|
get fallbackLocationValue() {
|
57
|
-
return this.modalElement?.dataset["kpop-ModalFallbackLocationValue"]
|
61
|
+
return this.modalElement?.dataset["kpop-ModalFallbackLocationValue"];
|
58
62
|
}
|
59
63
|
|
60
64
|
get isCurrentLocation() {
|
@@ -63,6 +67,10 @@ export class Modal {
|
|
63
67
|
);
|
64
68
|
}
|
65
69
|
|
70
|
+
static debug(event, ...args) {
|
71
|
+
if (DEBUG) console.debug(`${this.name}:${event}`, ...args);
|
72
|
+
}
|
73
|
+
|
66
74
|
debug(event, ...args) {
|
67
75
|
if (DEBUG) console.debug(`${this.constructor.name}:${event}`, ...args);
|
68
76
|
}
|
@@ -9,12 +9,24 @@ export class StreamModal extends Modal {
|
|
9
9
|
this.action = action;
|
10
10
|
}
|
11
11
|
|
12
|
+
/**
|
13
|
+
* When the modal opens, push a state event for the current location so that
|
14
|
+
* the user can dismiss the modal by navigating back.
|
15
|
+
*
|
16
|
+
* @returns {Promise<void>}
|
17
|
+
*/
|
12
18
|
async open() {
|
13
19
|
await super.open();
|
14
20
|
|
15
21
|
window.history.pushState({ kpop: true, id: this.id }, "", window.location);
|
16
22
|
}
|
17
23
|
|
24
|
+
/**
|
25
|
+
* On dismiss, pop the state event that was pushed when the modal opened,
|
26
|
+
* then clear any modals from the turbo frame element.
|
27
|
+
*
|
28
|
+
* @returns {Promise<void>}
|
29
|
+
*/
|
18
30
|
async dismiss() {
|
19
31
|
await super.dismiss();
|
20
32
|
|
@@ -25,6 +37,13 @@ export class StreamModal extends Modal {
|
|
25
37
|
this.frameElement.innerHTML = "";
|
26
38
|
}
|
27
39
|
|
40
|
+
/**
|
41
|
+
* On navigation from inside the modal, dismiss the modal first so that the
|
42
|
+
* modal does not appear in the history stack.
|
43
|
+
*
|
44
|
+
* @param frame TurboFrame element
|
45
|
+
* @param e Turbo navigation event
|
46
|
+
*/
|
28
47
|
beforeVisit(frame, e) {
|
29
48
|
super.beforeVisit(frame, e);
|
30
49
|
|
@@ -37,6 +56,12 @@ export class StreamModal extends Modal {
|
|
37
56
|
});
|
38
57
|
}
|
39
58
|
|
59
|
+
/**
|
60
|
+
* If the user pops state, dismiss the modal.
|
61
|
+
*
|
62
|
+
* @param frame FrameController
|
63
|
+
* @param e history event
|
64
|
+
*/
|
40
65
|
popstate(frame, e) {
|
41
66
|
super.popstate(frame, e);
|
42
67
|
|