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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c29776e86c6d5f591b5f5398987da2487a6d8eba09f44da2988852c4b594502b
|
4
|
+
data.tar.gz: '004809bbf80602c942b5d4074062d5793c3f210f07f1d1a97a844e544ef60038'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4d2466c600eb9c88e68556d08aac862b102052675fa8cdad68aac0033356a52669b0e02f2238b6affff871d98d3410efe22c39bc0bcaeb09a9ae619d728026d
|
7
|
+
data.tar.gz: 613392880bd2d59e6610213cec65b4746766c5c8fa6409ec2dca8e9add724a0423f0abdc5c607fe492465409ab79451558d50a44534ce131345e28d39c5811c5
|
@@ -44,6 +44,10 @@ class Modal {
|
|
44
44
|
return document.getElementById(this.id);
|
45
45
|
}
|
46
46
|
|
47
|
+
get controller() {
|
48
|
+
return this.frameElement?.kpop;
|
49
|
+
}
|
50
|
+
|
47
51
|
get modalElement() {
|
48
52
|
return this.frameElement?.querySelector("[data-controller*='kpop--modal']");
|
49
53
|
}
|
@@ -53,7 +57,7 @@ class Modal {
|
|
53
57
|
}
|
54
58
|
|
55
59
|
get fallbackLocationValue() {
|
56
|
-
return this.modalElement?.dataset["kpop-ModalFallbackLocationValue"]
|
60
|
+
return this.modalElement?.dataset["kpop-ModalFallbackLocationValue"];
|
57
61
|
}
|
58
62
|
|
59
63
|
get isCurrentLocation() {
|
@@ -62,18 +66,39 @@ class Modal {
|
|
62
66
|
);
|
63
67
|
}
|
64
68
|
|
69
|
+
static debug(event, ...args) {
|
70
|
+
}
|
71
|
+
|
65
72
|
debug(event, ...args) {
|
66
73
|
}
|
67
74
|
}
|
68
75
|
|
69
76
|
class ContentModal extends Modal {
|
77
|
+
static connect(frame, element) {
|
78
|
+
frame.open(new ContentModal(element.id), { animate: false });
|
79
|
+
}
|
80
|
+
|
70
81
|
constructor(id, src = null) {
|
71
82
|
super(id);
|
72
83
|
|
73
84
|
if (src) this.src = src;
|
74
85
|
}
|
75
86
|
|
87
|
+
/**
|
88
|
+
* When the modal is dismissed we can't rely on a back navigation to close the
|
89
|
+
* modal as the user may have navigated to a different location. Instead we
|
90
|
+
* remove the content from the dom and replace the current history state with
|
91
|
+
* the fallback location, if set.
|
92
|
+
*
|
93
|
+
* If there is no fallback location, we may be showing a stream modal that was
|
94
|
+
* injected and cached by turbo. In this case, we clear the frame element and
|
95
|
+
* do not change history.
|
96
|
+
*
|
97
|
+
* @returns {Promise<void>}
|
98
|
+
*/
|
76
99
|
async dismiss() {
|
100
|
+
const fallbackLocation = this.fallbackLocationValue;
|
101
|
+
|
77
102
|
await super.dismiss();
|
78
103
|
|
79
104
|
if (this.visitStarted) {
|
@@ -85,12 +110,11 @@ class ContentModal extends Modal {
|
|
85
110
|
return;
|
86
111
|
}
|
87
112
|
|
88
|
-
|
89
|
-
this.debug("turbo-visit", this.fallbackLocationValue);
|
90
|
-
Turbo.visit(this.fallbackLocationValue);
|
91
|
-
});
|
113
|
+
this.frameElement.innerHTML = "";
|
92
114
|
|
93
|
-
|
115
|
+
if (fallbackLocation) {
|
116
|
+
window.history.replaceState(window.history.state, "", fallbackLocation);
|
117
|
+
}
|
94
118
|
}
|
95
119
|
|
96
120
|
beforeVisit(frame, e) {
|
@@ -110,11 +134,81 @@ class ContentModal extends Modal {
|
|
110
134
|
}
|
111
135
|
|
112
136
|
class FrameModal extends Modal {
|
137
|
+
/**
|
138
|
+
* When the FrameController detects a frame element on connect, it runs this
|
139
|
+
* method to santity check the frame src and restore the modal state.
|
140
|
+
*
|
141
|
+
* @param frame FrameController
|
142
|
+
* @param element TurboFrame element
|
143
|
+
*/
|
144
|
+
static connect(frame, element) {
|
145
|
+
const modal = new FrameModal(element.id, element.src);
|
146
|
+
|
147
|
+
// state reconciliation for turbo restore of invalid frames
|
148
|
+
if (modal.isCurrentLocation) {
|
149
|
+
// restoration visit
|
150
|
+
this.debug("restore", element.src);
|
151
|
+
return frame.open(modal, { animate: false });
|
152
|
+
} else {
|
153
|
+
console.warn(
|
154
|
+
"kpop: restored frame src doesn't match window href",
|
155
|
+
modal.src,
|
156
|
+
window.location.href
|
157
|
+
);
|
158
|
+
return frame.clear();
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
/**
|
163
|
+
* When a user clicks a kpop link, turbo intercepts the click and calls
|
164
|
+
* navigateFrame on the turbo frame controller before setting the TurboFrame
|
165
|
+
* element's src attribute. KPOP intercepts this call and calls this method
|
166
|
+
* first so we cancel problematic navigations that might cache invalid states.
|
167
|
+
*
|
168
|
+
* @param location URL requested by turbo
|
169
|
+
* @param frame FrameController
|
170
|
+
* @param element TurboFrame element
|
171
|
+
* @param resolve continuation chain
|
172
|
+
*/
|
173
|
+
static visit(location, frame, element, resolve) {
|
174
|
+
// Ensure that turbo doesn't cache the frame in a loading state by cancelling
|
175
|
+
// the current request (if any) by clearing the src.
|
176
|
+
// Known issue: this won't work if the frame was previously rendering a useful src.
|
177
|
+
if (element.hasAttribute("busy")) {
|
178
|
+
this.debug("clearing src to cancel turbo request");
|
179
|
+
element.src = "";
|
180
|
+
}
|
181
|
+
|
182
|
+
if (element.src === location) {
|
183
|
+
this.debug("skipping navigate as already on location");
|
184
|
+
return;
|
185
|
+
}
|
186
|
+
|
187
|
+
if (element.src && element.src !== window.location.href) {
|
188
|
+
console.warn(
|
189
|
+
"kpop: frame src doesn't match window",
|
190
|
+
element.src,
|
191
|
+
window.location.href,
|
192
|
+
location
|
193
|
+
);
|
194
|
+
frame.clear();
|
195
|
+
}
|
196
|
+
|
197
|
+
this.debug("navigate to", location);
|
198
|
+
resolve();
|
199
|
+
}
|
200
|
+
|
113
201
|
constructor(id, src) {
|
114
202
|
super(id);
|
115
203
|
this.src = src;
|
116
204
|
}
|
117
205
|
|
206
|
+
/**
|
207
|
+
* FrameModals are closed by running pop state and awaiting the turbo:load
|
208
|
+
* event that follows on history restoration.
|
209
|
+
*
|
210
|
+
* @returns {Promise<void>}
|
211
|
+
*/
|
118
212
|
async dismiss() {
|
119
213
|
await super.dismiss();
|
120
214
|
|
@@ -127,6 +221,13 @@ class FrameModal extends Modal {
|
|
127
221
|
// no specific close action required, this is turbo's responsibility
|
128
222
|
}
|
129
223
|
|
224
|
+
/**
|
225
|
+
* When user navigates from inside a Frame modal, dismiss the modal first so
|
226
|
+
* that the modal does not appear in the history stack.
|
227
|
+
*
|
228
|
+
* @param frame FrameController
|
229
|
+
* @param e Turbo navigation event
|
230
|
+
*/
|
130
231
|
beforeVisit(frame, e) {
|
131
232
|
super.beforeVisit(frame, e);
|
132
233
|
|
@@ -138,13 +239,6 @@ class FrameModal extends Modal {
|
|
138
239
|
this.debug("before-visit-end");
|
139
240
|
});
|
140
241
|
}
|
141
|
-
|
142
|
-
popstate(frame, e) {
|
143
|
-
super.popstate(frame, e);
|
144
|
-
|
145
|
-
// Turbo will restore modal state, but we need to reset the scrim
|
146
|
-
frame.scrimOutlet.hide({ animate: false });
|
147
|
-
}
|
148
242
|
}
|
149
243
|
|
150
244
|
class Kpop__FrameController extends Controller {
|
@@ -158,22 +252,19 @@ class Kpop__FrameController extends Controller {
|
|
158
252
|
this.debug("connect", this.element.src);
|
159
253
|
|
160
254
|
this.element.kpop = this;
|
161
|
-
installNavigationInterception(this.element, this.element.delegate);
|
162
255
|
|
163
|
-
//
|
256
|
+
// allow our code to intercept frame navigation requests before dom changes
|
257
|
+
installNavigationInterception(this);
|
258
|
+
|
164
259
|
if (this.element.src && this.element.complete) {
|
165
260
|
this.debug("new frame modal", this.element.src);
|
166
|
-
|
167
|
-
|
168
|
-
|
261
|
+
FrameModal.connect(this, this.element);
|
262
|
+
} else if (this.modalElements.length > 0) {
|
263
|
+
this.debug("new content modal", window.location.pathname);
|
264
|
+
ContentModal.connect(this, this.element);
|
169
265
|
} else {
|
170
|
-
|
171
|
-
|
172
|
-
);
|
173
|
-
if (element) {
|
174
|
-
this.debug("new content modal", window.location.pathname);
|
175
|
-
this.open(new ContentModal(this.element.id), { animate: false });
|
176
|
-
}
|
266
|
+
this.debug("no modal");
|
267
|
+
this.clear();
|
177
268
|
}
|
178
269
|
}
|
179
270
|
|
@@ -209,6 +300,8 @@ class Kpop__FrameController extends Controller {
|
|
209
300
|
return false;
|
210
301
|
}
|
211
302
|
|
303
|
+
await this.dismissing;
|
304
|
+
|
212
305
|
return (this.opening ||= this.#nextFrame(() =>
|
213
306
|
this.#open(modal, { animate })
|
214
307
|
));
|
@@ -220,46 +313,45 @@ class Kpop__FrameController extends Controller {
|
|
220
313
|
return false;
|
221
314
|
}
|
222
315
|
|
316
|
+
await this.opening;
|
317
|
+
|
223
318
|
return (this.dismissing ||= this.#nextFrame(() =>
|
224
319
|
this.#dismiss({ animate, reason })
|
225
320
|
));
|
226
321
|
}
|
227
322
|
|
228
|
-
|
323
|
+
async clear() {
|
324
|
+
// clear the src from the frame (if any)
|
325
|
+
this.element.src = "";
|
229
326
|
|
230
|
-
|
231
|
-
this.
|
232
|
-
}
|
327
|
+
// remove any open modal(s)
|
328
|
+
this.modalElements.forEach((element) => element.remove());
|
233
329
|
|
234
|
-
|
235
|
-
this.
|
330
|
+
// mark the modal as hidden (will hide scrim on connect)
|
331
|
+
this.openValue = false;
|
236
332
|
|
237
|
-
//
|
238
|
-
|
239
|
-
|
240
|
-
if (this.element.hasAttribute("busy")) {
|
241
|
-
this.debug("clearing src to cancel turbo request");
|
242
|
-
this.element.src = "";
|
333
|
+
// close the scrim, if connected
|
334
|
+
if (this.scrimConnected) {
|
335
|
+
return this.scrimOutlet.hide({ animate: false });
|
243
336
|
}
|
244
337
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
}
|
338
|
+
// unset modal
|
339
|
+
this.modal = null;
|
340
|
+
}
|
249
341
|
|
250
|
-
|
251
|
-
console.warn("kpop: frame src doesn't match window", this.element.src, window.location.href, location);
|
252
|
-
// clear src so that turbo doesn't cache the frame in a loading state
|
253
|
-
this.element.delegate.ignoringChangesToAttribute("src", (() => {
|
254
|
-
this.element.src = "";
|
255
|
-
this.element.delegate.complete = false;
|
256
|
-
}));
|
257
|
-
}
|
342
|
+
// EVENTS
|
258
343
|
|
259
|
-
|
260
|
-
|
344
|
+
popstate(event) {
|
345
|
+
this.modal?.popstate(this, event);
|
261
346
|
}
|
262
347
|
|
348
|
+
/**
|
349
|
+
* Incoming frame render, dismiss the current modal (if any) first.
|
350
|
+
*
|
351
|
+
* We're starting the actual visit
|
352
|
+
*
|
353
|
+
* @param event turbo:before-render
|
354
|
+
*/
|
263
355
|
beforeFrameRender(event) {
|
264
356
|
this.debug("before-frame-render", event.detail.newFrame.baseURI);
|
265
357
|
|
@@ -302,15 +394,25 @@ class Kpop__FrameController extends Controller {
|
|
302
394
|
frameLoad(event) {
|
303
395
|
this.debug("frame-load");
|
304
396
|
|
305
|
-
|
306
|
-
|
307
|
-
|
397
|
+
const modal = new FrameModal(this.element.id, this.element.src);
|
398
|
+
|
399
|
+
window.addEventListener(
|
400
|
+
"turbo:visit",
|
401
|
+
(e) => {
|
402
|
+
this.open(modal, { animate: true });
|
403
|
+
},
|
404
|
+
{ once: true }
|
405
|
+
);
|
308
406
|
}
|
309
407
|
|
310
408
|
get isOpen() {
|
311
409
|
return this.openValue && !this.dismissing;
|
312
410
|
}
|
313
411
|
|
412
|
+
get modalElements() {
|
413
|
+
return this.element.querySelectorAll("[data-controller*='kpop--modal']");
|
414
|
+
}
|
415
|
+
|
314
416
|
async #open(modal, { animate = true } = {}) {
|
315
417
|
this.debug("open-start", { animate });
|
316
418
|
|
@@ -367,16 +469,26 @@ class Kpop__FrameController extends Controller {
|
|
367
469
|
*
|
368
470
|
* See Turbo issue: https://github.com/hotwired/turbo/issues/1055
|
369
471
|
*
|
370
|
-
* @param
|
472
|
+
* @param controller FrameController
|
371
473
|
*/
|
372
|
-
function installNavigationInterception(
|
373
|
-
|
374
|
-
controller.
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
474
|
+
function installNavigationInterception(controller) {
|
475
|
+
const TurboFrameController =
|
476
|
+
controller.element.delegate.constructor.prototype;
|
477
|
+
|
478
|
+
if (TurboFrameController._navigateFrame) return;
|
479
|
+
|
480
|
+
TurboFrameController._navigateFrame = TurboFrameController.navigateFrame;
|
481
|
+
TurboFrameController.navigateFrame = function (element, url, submitter) {
|
482
|
+
const frame = this.findFrameElement(element, submitter);
|
483
|
+
|
484
|
+
if (frame.kpop) {
|
485
|
+
FrameModal.visit(url, frame.kpop, frame, () => {
|
486
|
+
TurboFrameController._navigateFrame.call(this, element, url, submitter);
|
487
|
+
});
|
488
|
+
} else {
|
489
|
+
TurboFrameController._navigateFrame.call(this, element, url, submitter);
|
490
|
+
}
|
491
|
+
};
|
380
492
|
}
|
381
493
|
|
382
494
|
class Kpop__ModalController extends Controller {
|
@@ -561,12 +673,24 @@ class StreamModal extends Modal {
|
|
561
673
|
this.action = action;
|
562
674
|
}
|
563
675
|
|
676
|
+
/**
|
677
|
+
* When the modal opens, push a state event for the current location so that
|
678
|
+
* the user can dismiss the modal by navigating back.
|
679
|
+
*
|
680
|
+
* @returns {Promise<void>}
|
681
|
+
*/
|
564
682
|
async open() {
|
565
683
|
await super.open();
|
566
684
|
|
567
685
|
window.history.pushState({ kpop: true, id: this.id }, "", window.location);
|
568
686
|
}
|
569
687
|
|
688
|
+
/**
|
689
|
+
* On dismiss, pop the state event that was pushed when the modal opened,
|
690
|
+
* then clear any modals from the turbo frame element.
|
691
|
+
*
|
692
|
+
* @returns {Promise<void>}
|
693
|
+
*/
|
570
694
|
async dismiss() {
|
571
695
|
await super.dismiss();
|
572
696
|
|
@@ -577,6 +701,13 @@ class StreamModal extends Modal {
|
|
577
701
|
this.frameElement.innerHTML = "";
|
578
702
|
}
|
579
703
|
|
704
|
+
/**
|
705
|
+
* On navigation from inside the modal, dismiss the modal first so that the
|
706
|
+
* modal does not appear in the history stack.
|
707
|
+
*
|
708
|
+
* @param frame TurboFrame element
|
709
|
+
* @param e Turbo navigation event
|
710
|
+
*/
|
580
711
|
beforeVisit(frame, e) {
|
581
712
|
super.beforeVisit(frame, e);
|
582
713
|
|
@@ -589,6 +720,12 @@ class StreamModal extends Modal {
|
|
589
720
|
});
|
590
721
|
}
|
591
722
|
|
723
|
+
/**
|
724
|
+
* If the user pops state, dismiss the modal.
|
725
|
+
*
|
726
|
+
* @param frame FrameController
|
727
|
+
* @param e history event
|
728
|
+
*/
|
592
729
|
popstate(frame, e) {
|
593
730
|
super.popstate(frame, e);
|
594
731
|
|