katalyst-kpop 3.0.0.beta.7 → 3.0.0.beta.8
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/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
|
|