katalyst-kpop 3.0.0.beta.6 → 3.0.0.beta.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea8b93a5914923b3ba6db7318e0887f3851bfc5774ae6e6790b543900da8d579
4
- data.tar.gz: 336b3aeb96fcd5cc8293365fc2fc45609ec9334167048a93a7ac3d254ea82e36
3
+ metadata.gz: c29776e86c6d5f591b5f5398987da2487a6d8eba09f44da2988852c4b594502b
4
+ data.tar.gz: '004809bbf80602c942b5d4074062d5793c3f210f07f1d1a97a844e544ef60038'
5
5
  SHA512:
6
- metadata.gz: bc928df7baf831a99ca7ef267270cebfae10170c18e91b99863ed830ab330c05807bce81f1a9b82384db4450fb8039c18dfb1cb6929f0c2475e026b6a9a650e7
7
- data.tar.gz: ed9d5a3ab73686931a9494107680b048a5ab316d5e47f4afc315c203d5995505263f26c65a5e6abf62065e50c9d656c83795302d9975dd81c24f3bd91b9e2c83
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
- return this.pop("turbo:load", () => {
89
- this.debug("turbo-visit", this.fallbackLocationValue);
90
- Turbo.visit(this.fallbackLocationValue);
91
- });
113
+ this.frameElement.innerHTML = "";
92
114
 
93
- // no specific close action required, this is turbo's responsibility
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);
162
255
 
163
- // restoration visit
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
- this.open(new FrameModal(this.element.id, this.element.src), {
167
- animate: false,
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
- const element = this.element.querySelector(
171
- "[data-controller*='kpop--modal']"
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,31 +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
- // EVENTS
323
+ async clear() {
324
+ // clear the src from the frame (if any)
325
+ this.element.src = "";
229
326
 
230
- popstate(event) {
231
- this.modal?.popstate(this, event);
232
- }
327
+ // remove any open modal(s)
328
+ this.modalElements.forEach((element) => element.remove());
233
329
 
234
- navigateFrame(element, location) {
235
- this.debug("navigate-frame", this.element.src, location);
330
+ // mark the modal as hidden (will hide scrim on connect)
331
+ this.openValue = false;
236
332
 
237
- // Ensure that turbo doesn't cache the frame in a loading state by cancelling
238
- // the current request (if any) by clearing the src.
239
- // Known issue: this won't work if the frame was previously rendering a useful src.
240
- if (this.element.hasAttribute("busy")) {
241
- this.element.src = "";
333
+ // close the scrim, if connected
334
+ if (this.scrimConnected) {
335
+ return this.scrimOutlet.hide({ animate: false });
242
336
  }
243
337
 
244
- // Delay turbo's navigateFrame until next tick to let the src change settle.
245
- return Promise.resolve();
338
+ // unset modal
339
+ this.modal = null;
246
340
  }
247
341
 
342
+ // EVENTS
343
+
344
+ popstate(event) {
345
+ this.modal?.popstate(this, event);
346
+ }
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
+ */
248
355
  beforeFrameRender(event) {
249
356
  this.debug("before-frame-render", event.detail.newFrame.baseURI);
250
357
 
@@ -287,15 +394,25 @@ class Kpop__FrameController extends Controller {
287
394
  frameLoad(event) {
288
395
  this.debug("frame-load");
289
396
 
290
- return this.open(new FrameModal(this.element.id, this.element.src), {
291
- animate: true,
292
- });
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
+ );
293
406
  }
294
407
 
295
408
  get isOpen() {
296
409
  return this.openValue && !this.dismissing;
297
410
  }
298
411
 
412
+ get modalElements() {
413
+ return this.element.querySelectorAll("[data-controller*='kpop--modal']");
414
+ }
415
+
299
416
  async #open(modal, { animate = true } = {}) {
300
417
  this.debug("open-start", { animate });
301
418
 
@@ -352,20 +469,26 @@ class Kpop__FrameController extends Controller {
352
469
  *
353
470
  * See Turbo issue: https://github.com/hotwired/turbo/issues/1055
354
471
  *
355
- * @param frameElement turbo-frame element
472
+ * @param controller FrameController
356
473
  */
357
- function installNavigationInterception(frameElement) {
358
- if (frameElement.delegate._navigateFrame === undefined) {
359
- frameElement.delegate._navigateFrame = frameElement.delegate.navigateFrame;
360
- frameElement.delegate.navigateFrame = async (element, location) => {
361
- await frameElement.kpop?.navigateFrame(element, location);
362
- return frameElement.delegate._navigateFrame.call(
363
- frameElement.delegate,
364
- element,
365
- location
366
- );
367
- };
368
- }
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
+ };
369
492
  }
370
493
 
371
494
  class Kpop__ModalController extends Controller {
@@ -550,12 +673,24 @@ class StreamModal extends Modal {
550
673
  this.action = action;
551
674
  }
552
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
+ */
553
682
  async open() {
554
683
  await super.open();
555
684
 
556
685
  window.history.pushState({ kpop: true, id: this.id }, "", window.location);
557
686
  }
558
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
+ */
559
694
  async dismiss() {
560
695
  await super.dismiss();
561
696
 
@@ -566,6 +701,13 @@ class StreamModal extends Modal {
566
701
  this.frameElement.innerHTML = "";
567
702
  }
568
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
+ */
569
711
  beforeVisit(frame, e) {
570
712
  super.beforeVisit(frame, e);
571
713
 
@@ -578,6 +720,12 @@ class StreamModal extends Modal {
578
720
  });
579
721
  }
580
722
 
723
+ /**
724
+ * If the user pops state, dismiss the modal.
725
+ *
726
+ * @param frame FrameController
727
+ * @param e history event
728
+ */
581
729
  popstate(frame, e) {
582
730
  super.popstate(frame, e);
583
731