katalyst-kpop 2.0.9 → 3.0.0.beta.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -43
  3. data/app/assets/builds/katalyst/kpop.esm.js +590 -0
  4. data/app/assets/builds/katalyst/kpop.js +466 -515
  5. data/app/assets/builds/katalyst/kpop.min.js +2 -1
  6. data/app/assets/builds/katalyst/kpop.min.js.map +1 -0
  7. data/app/assets/builds/katalyst/kpop.umd.js +5890 -0
  8. data/app/assets/config/kpop.js +1 -1
  9. data/app/assets/stylesheets/katalyst/kpop/_frame.scss +104 -0
  10. data/app/assets/stylesheets/katalyst/kpop/_modal.scss +95 -0
  11. data/app/assets/stylesheets/katalyst/kpop/_scrim.scss +33 -3
  12. data/app/assets/stylesheets/katalyst/kpop/_side_panel.scss +64 -0
  13. data/app/assets/stylesheets/katalyst/kpop/_variables.scss +25 -0
  14. data/app/assets/stylesheets/katalyst/kpop.scss +6 -1
  15. data/app/components/concerns/kpop/has_html_attributes.rb +78 -0
  16. data/app/components/kpop/frame_component.html.erb +14 -0
  17. data/app/components/kpop/frame_component.rb +46 -0
  18. data/app/components/kpop/modal/title_component.html.erb +6 -0
  19. data/app/components/kpop/modal/title_component.rb +28 -0
  20. data/app/components/kpop/modal_component.html.erb +8 -0
  21. data/app/components/kpop/modal_component.rb +39 -0
  22. data/app/components/scrim_component.rb +32 -0
  23. data/app/helpers/kpop_helper.rb +12 -35
  24. data/app/javascript/kpop/application.js +13 -0
  25. data/app/javascript/kpop/controllers/frame_controller.js +178 -0
  26. data/app/javascript/kpop/controllers/modal_controller.js +30 -0
  27. data/app/{assets/javascripts → javascript/kpop}/controllers/scrim_controller.js +76 -72
  28. data/app/javascript/kpop/debug.js +3 -0
  29. data/app/javascript/kpop/modals/content_modal.js +46 -0
  30. data/app/javascript/kpop/modals/frame_modal.js +41 -0
  31. data/app/javascript/kpop/modals/modal.js +69 -0
  32. data/app/javascript/kpop/modals/stream_modal.js +49 -0
  33. data/app/javascript/kpop/turbo_actions.js +33 -0
  34. data/app/javascript/kpop/utils/stream_renderer.js +15 -0
  35. data/app/views/layouts/kpop.html.erb +1 -1
  36. data/config/importmap.rb +1 -4
  37. data/lib/katalyst/kpop/engine.rb +13 -12
  38. data/lib/katalyst/kpop/matchers/base.rb +18 -0
  39. data/lib/katalyst/kpop/matchers/capybara_matcher.rb +46 -0
  40. data/lib/katalyst/kpop/matchers/capybara_parser.rb +17 -0
  41. data/lib/katalyst/kpop/matchers/chained_matcher.rb +40 -0
  42. data/lib/katalyst/kpop/matchers/frame_matcher.rb +16 -0
  43. data/lib/katalyst/kpop/matchers/modal_matcher.rb +20 -0
  44. data/lib/katalyst/kpop/matchers/redirect_matcher.rb +28 -0
  45. data/lib/katalyst/kpop/matchers/response_matcher.rb +33 -0
  46. data/lib/katalyst/kpop/matchers/stream_matcher.rb +16 -0
  47. data/lib/katalyst/kpop/matchers/title_finder.rb +16 -0
  48. data/lib/katalyst/kpop/matchers/title_matcher.rb +28 -0
  49. data/lib/katalyst/kpop/matchers.rb +79 -0
  50. data/lib/katalyst/kpop/turbo.rb +50 -0
  51. data/lib/katalyst/kpop/version.rb +1 -1
  52. data/lib/katalyst/kpop.rb +4 -0
  53. metadata +88 -15
  54. data/app/assets/builds/katalyst/kpop.css +0 -117
  55. data/app/assets/javascripts/controllers/kpop_controller.js +0 -72
  56. data/app/assets/javascripts/katalyst/kpop.js +0 -9
  57. data/app/assets/stylesheets/katalyst/kpop/_index.scss +0 -2
  58. data/app/assets/stylesheets/katalyst/kpop/_kpop.scss +0 -133
  59. data/app/helpers/kpop/modal.rb +0 -98
  60. data/app/helpers/scrim_helper.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 219158f3706755e88a2275d8ebc3bc20d1b05b0a7d7006d528d0361ae7e987dd
4
- data.tar.gz: ac4cb7baf980d41ecf134b42beae6e1c60ee1ff7266a25173d11bc463353eaaf
3
+ metadata.gz: bdd0521dfd00d4dc6d5c4f6b705d45fdcf0fa06b3a47cd6ec1b271b08b77e64a
4
+ data.tar.gz: 6a5480d57130797edd0e59e0e44710cd931033fcdbf01d286009e2be5bf8a5d6
5
5
  SHA512:
6
- metadata.gz: b94b85643fe6035a735c7827de03f38e03408ca40f184e708bcfd9694686060bac4191e4a5e88dd63b7b7bd6be7f4c5ccd9c7b80bd65ff636371e5e342d8ef47
7
- data.tar.gz: 5afc11d9d560f408c94908d9a64ac504a15d9bbd540c690b31f7ceb3678a0455061b7cf1c973a7154b358a9c7d1d1489eb5fed9ed7e5dbdc68f87e1d22f26157
6
+ metadata.gz: 4a8968e2712ea484dab2c01595e34474db62b94db09336fcae92450c9915c534c62980aa3d8ccee5a92bdd4c5836638c71242e668a71ed57de9715e7e6b78d4e
7
+ data.tar.gz: a7bd8a924421cf8f4fb509cea1893e4187e10fc6476579f598686cb9b4b259a8a16097d5b31b03f4fc805dd2362445f65a1438a74cd2c4c17162197b389848af
data/README.md CHANGED
@@ -17,8 +17,16 @@ kpop supports installation of javascript dependencies with either import maps or
17
17
 
18
18
  ### Stimulus controllers
19
19
 
20
- If you are using asset pipeline and import maps then the stimulus controllers
21
- for modals and scrim will be automatically available without configuration.
20
+ kpop assumes that you are using importmaps to manage javascript dependencies.
21
+
22
+ Add the following to your Stimulus `controllers/index.js`:
23
+
24
+ ```js
25
+ import kpop from "@katalyst/kpop";
26
+ application.load(kpop);
27
+ ```
28
+
29
+ This will ensure that kpop is loaded and registered with Stimulus.
22
30
 
23
31
  ### Stylesheets
24
32
 
@@ -30,43 +38,13 @@ Import stylesheets through using SASS using asset pipeline:
30
38
  @use "katalyst/kpop";
31
39
  ```
32
40
 
33
- You can also load a precompiled version from the gem directly:
34
-
35
- ```erb
36
- <%# app/views/layouts/application.html.erb #>
37
-
38
- <%= stylesheet_link_tag "katalyst/kpop" %>
39
- ```
40
-
41
- ### Yarn
42
-
43
- If you are not using import maps, you can add the yarn package to your project:
44
-
45
- ```bash
46
- $ yarn add "@katalyst-interactive/kpop"
47
- ```
48
-
49
- ### Import kpop styles
50
- ```css
51
- /* application.scss */
52
-
53
- @import "~@katalyst-interactive/kpop";
54
- ```
55
-
56
- ### Import kpop stimulus controllers
57
- ```js
58
- /* application.js */
59
- import kpop from "@katalyst-interactive/kpop"
60
- application.load(kpop)
61
- ```
62
-
63
41
  ## Usage
64
42
 
65
43
  kpop provides helpers to add a basic scrim and modal target frame. These should be placed inside the body:
66
44
  ```html
67
45
  <body>
68
- <%= scrim_tag %>
69
- <%= kpop_frame_tag do %>
46
+ <%= render ScrimComponent.new %>
47
+ <%= render Kpop::FrameComponent.new do %>
70
48
  <%= yield :kpop %>
71
49
  <% end %>
72
50
  </body>
@@ -81,32 +59,28 @@ To show a modal you need to add content to the kpop turbo frame. You can do this
81
59
  You can generate a link that will cause a modal to show using the `kpop_link_to` helper.
82
60
 
83
61
  `kpop_link_to`'s are similar to a `link_to` in rails, but it will navigate to the given URL within the modal turbo
84
- frame. The targeted action will need to generate content in a `kpop_frame_tag`, e.g. using `layout "kpop"`.
62
+ frame. The targeted action will need to generate content in a `Kpop::FrameComponent`, e.g. using `layout "kpop"`.
85
63
 
86
64
  ```html
87
65
  <!-- app/views/homepage/index.html.erb -->
88
- <%= modal_link_to "click to open modal", modal_path("example") %>
66
+ <%= kpop_link_to "click to open modal", modal_path("example") %>
89
67
  ```
90
68
 
91
69
  ```html
92
70
  <!-- app/views/modals/show.html.erb -->
93
- <%= render_kpop(title: "Modal title") do %>
94
- Modal content
71
+ <%= Kpop::Modal(title: "Modal title") do %>
72
+ Modal content
95
73
  <% end %>
96
74
  ```
97
75
 
98
- Note that, because kpop modals render in a turbo frame, if you want to navigate the parent frame you will need to use
99
- `target: "_top"` on your links and forms, or add `target: "_top"` to the `kpop_frame_tag` call in your body.
100
-
101
76
  ## Development
102
77
 
103
78
  Releases need to be distributed to rubygems.org and npmjs.org. To do this, you need to have accounts with both providers
104
79
  and be added as a collaborator to the kpop gem and npm packages.
105
80
 
106
- 1. Update the version in `package.json` and `lib/katalyst/kpop/version.rb`
81
+ 1. Update the version in `lib/katalyst/kpop/version.rb`
107
82
  2. Ensure that `rake` passes (format and tests)
108
83
  3. Tag a release and push to rubygems.org by running `rake release`
109
- 4. Push the new version to npmjs.org by running `yarn publish`
110
84
 
111
85
  ## License
112
86
 
@@ -0,0 +1,590 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+ import { Turbo } from '@hotwired/turbo-rails';
3
+
4
+ class Modal {
5
+ constructor(id) {
6
+ this.id = id;
7
+ }
8
+
9
+ async open() {
10
+ this.debug("open");
11
+ }
12
+
13
+ async dismiss() {
14
+ this.debug(`dismiss`);
15
+ }
16
+
17
+ beforeVisit(frame, e) {
18
+ this.debug(`before-visit`, e.detail.url);
19
+ }
20
+
21
+ popstate(frame, e) {
22
+ this.debug(`popstate`, e.state);
23
+ }
24
+
25
+ async pop(event, callback) {
26
+ this.debug(`pop`);
27
+
28
+ const promise = new Promise((resolve) => {
29
+ window.addEventListener(
30
+ event,
31
+ () => {
32
+ resolve();
33
+ },
34
+ { once: true }
35
+ );
36
+ });
37
+
38
+ callback();
39
+
40
+ return promise;
41
+ }
42
+
43
+ get frameElement() {
44
+ return document.getElementById(this.id);
45
+ }
46
+
47
+ get modalElement() {
48
+ return this.frameElement?.querySelector("[data-controller*='kpop--modal']");
49
+ }
50
+
51
+ get currentLocationValue() {
52
+ return this.modalElement?.dataset["kpop-ModalCurrentLocationValue"] || "/";
53
+ }
54
+
55
+ get fallbackLocationValue() {
56
+ return this.modalElement?.dataset["kpop-ModalFallbackLocationValue"] || "/";
57
+ }
58
+
59
+ get isCurrentLocation() {
60
+ return (
61
+ window.history.state?.turbo && Turbo.session.location.href === this.src
62
+ );
63
+ }
64
+
65
+ debug(event, ...args) {
66
+ }
67
+ }
68
+
69
+ class ContentModal extends Modal {
70
+ constructor(id, src = null) {
71
+ super(id);
72
+
73
+ if (src) this.src = src;
74
+ }
75
+
76
+ async dismiss() {
77
+ await super.dismiss();
78
+
79
+ if (this.visitStarted) {
80
+ this.debug("skipping dismiss, visit started");
81
+ return;
82
+ }
83
+ if (!this.isCurrentLocation) {
84
+ this.debug("skipping dismiss, not current location");
85
+ return;
86
+ }
87
+
88
+ return this.pop("turbo:load", () => {
89
+ this.debug("turbo-visit", this.fallbackLocationValue);
90
+ Turbo.visit(this.fallbackLocationValue, { action: "replace" });
91
+ });
92
+
93
+ // no specific close action required, this is turbo's responsibility
94
+ }
95
+
96
+ beforeVisit(frame, e) {
97
+ super.beforeVisit(frame, e);
98
+
99
+ this.visitStarted = true;
100
+
101
+ frame.scrimOutlet.hide({ animate: false });
102
+ }
103
+
104
+ get src() {
105
+ return new URL(
106
+ this.currentLocationValue.toString(),
107
+ document.baseURI
108
+ ).toString();
109
+ }
110
+ }
111
+
112
+ class FrameModal extends Modal {
113
+ constructor(id, src) {
114
+ super(id);
115
+ this.src = src;
116
+ }
117
+
118
+ async dismiss() {
119
+ await super.dismiss();
120
+
121
+ if (!this.isCurrentLocation) {
122
+ this.debug("skipping dismiss, not current location");
123
+ }
124
+
125
+ await this.pop("turbo:load", () => window.history.back());
126
+
127
+ // no specific close action required, this is turbo's responsibility
128
+ }
129
+
130
+ beforeVisit(frame, e) {
131
+ super.beforeVisit(frame, e);
132
+
133
+ e.preventDefault();
134
+
135
+ frame.dismiss({ animate: false }).then(() => {
136
+ Turbo.visit(e.detail.url);
137
+
138
+ this.debug("before-visit-end");
139
+ });
140
+ }
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
+ }
149
+
150
+ class Kpop__FrameController extends Controller {
151
+ static outlets = ["scrim"];
152
+ static targets = ["modal"];
153
+ static values = {
154
+ open: Boolean,
155
+ };
156
+
157
+ connect() {
158
+ this.debug("connect", this.element.src);
159
+
160
+ this.element.kpop = this;
161
+
162
+ // restoration visit
163
+ if (this.element.src && this.element.complete) {
164
+ this.debug("new frame modal", this.element.src);
165
+ this.open(new FrameModal(this.element.id, this.element.src), {
166
+ animate: false,
167
+ });
168
+ } else {
169
+ const element = this.element.querySelector(
170
+ "[data-controller*='kpop--modal']"
171
+ );
172
+ if (element) {
173
+ this.debug("new content modal", window.location.pathname);
174
+ this.open(new ContentModal(this.element.id), { animate: false });
175
+ }
176
+ }
177
+ }
178
+
179
+ disconnect() {
180
+ this.debug("disconnect");
181
+
182
+ delete this.element.kpop;
183
+ delete this.modal;
184
+ }
185
+
186
+ scrimOutletConnected(scrim) {
187
+ this.debug("scrim-connected");
188
+
189
+ this.scrimConnected = true;
190
+
191
+ if (this.openValue) scrim.show({ animate: false });
192
+ }
193
+
194
+ openValueChanged(open) {
195
+ this.debug("open-changed", open);
196
+
197
+ this.element.parentElement.style.display = open ? "flex" : "none";
198
+ }
199
+
200
+ async open(modal, { animate = true } = {}) {
201
+ if (this.isOpen) {
202
+ this.debug("skip open as already open");
203
+ return false;
204
+ }
205
+
206
+ return (this.opening ||= this.#nextFrame(() =>
207
+ this.#open(modal, { animate })
208
+ ));
209
+ }
210
+
211
+ async dismiss({ animate = true, reason = "" } = {}) {
212
+ if (!this.isOpen) {
213
+ this.debug("skip dismiss as already closed");
214
+ return false;
215
+ }
216
+
217
+ return (this.dismissing ||= this.#nextFrame(() =>
218
+ this.#dismiss({ animate, reason })
219
+ ));
220
+ }
221
+
222
+ // EVENTS
223
+
224
+ popstate(event) {
225
+ this.modal?.popstate(this, event);
226
+ }
227
+
228
+ beforeFrameRender(event) {
229
+ this.debug("before-frame-render", event.detail.newFrame.baseURI);
230
+
231
+ event.preventDefault();
232
+
233
+ this.dismiss({ animate: true, reason: "before-frame-render" }).then(() => {
234
+ this.debug("resume-frame-render", event.detail.newFrame.baseURI);
235
+ event.detail.resume();
236
+ });
237
+ }
238
+
239
+ beforeStreamRender(event) {
240
+ this.debug("before-stream-render", event.detail);
241
+
242
+ const resume = event.detail.render;
243
+
244
+ // Defer rendering until dismiss is complete.
245
+ // Dismiss may change history so we need to wait for it to complete to avoid
246
+ // losing DOM changes on restoration visits.
247
+ event.detail.render = (stream) => {
248
+ (this.dismissing || Promise.resolve()).then(() => {
249
+ this.debug("stream-render", stream);
250
+ resume(stream);
251
+ });
252
+ };
253
+ }
254
+
255
+ beforeVisit(e) {
256
+ this.debug("before-visit", e.detail.url);
257
+
258
+ // ignore visits to the current frame, these fire when the frame navigates
259
+ if (e.detail.url === this.element.src) return;
260
+
261
+ // ignore unless we're open
262
+ if (!this.isOpen) return;
263
+
264
+ this.modal.beforeVisit(this, e);
265
+ }
266
+
267
+ frameLoad(event) {
268
+ this.debug("frame-load");
269
+
270
+ return this.open(new FrameModal(this.element.id, this.element.src), {
271
+ animate: true,
272
+ });
273
+ }
274
+
275
+ get isOpen() {
276
+ return this.openValue && !this.dismissing;
277
+ }
278
+
279
+ async #open(modal, { animate = true } = {}) {
280
+ this.debug("open-start", { animate });
281
+
282
+ const scrim = this.scrimConnected && this.scrimOutlet;
283
+
284
+ this.modal = modal;
285
+ this.openValue = true;
286
+
287
+ await modal.open({ animate });
288
+ await scrim?.show({ animate });
289
+
290
+ delete this.opening;
291
+
292
+ this.debug("open-end");
293
+ }
294
+
295
+ async #dismiss({ animate = true, reason = "" } = {}) {
296
+ this.debug("dismiss-start", { animate, reason });
297
+
298
+ if (!this.modal) {
299
+ console.warn("modal missing on dismiss");
300
+ }
301
+
302
+ await this.scrimOutlet.hide({ animate });
303
+ await this.modal?.dismiss();
304
+
305
+ this.openValue = false;
306
+ this.modal = null;
307
+ delete this.dismissing;
308
+
309
+ this.debug("dismiss-end");
310
+ }
311
+
312
+ async #nextFrame(callback) {
313
+ // return Promise.resolve().then(callback);
314
+ return new Promise(window.requestAnimationFrame).then(callback);
315
+ }
316
+
317
+ debug(event, ...args) {
318
+ }
319
+ }
320
+
321
+ class Kpop__ModalController extends Controller {
322
+ static values = {
323
+ fallback_location: String,
324
+ layout: String,
325
+ };
326
+
327
+ connect() {
328
+ this.debug("connect");
329
+
330
+ if (this.layoutValue) {
331
+ document.querySelector("#kpop").classList.toggle(this.layoutValue, true);
332
+ }
333
+ }
334
+
335
+ disconnect() {
336
+ this.debug("disconnect");
337
+
338
+ if (this.layoutValue) {
339
+ document.querySelector("#kpop").classList.toggle(this.layoutValue, false);
340
+ }
341
+ }
342
+
343
+ debug(event, ...args) {
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Scrim controller wraps an element that creates a whole page layer.
349
+ * It is intended to be used behind a modal or nav drawer.
350
+ *
351
+ * If the Scrim element receives a click event, it automatically triggers "scrim:hide".
352
+ *
353
+ * You can show and hide the scrim programmatically by calling show/hide on the controller, e.g. using an outlet.
354
+ *
355
+ * If you need to respond to the scrim showing or hiding you should subscribe to "scrim:show" and "scrim:hide".
356
+ */
357
+ class ScrimController extends Controller {
358
+ static values = {
359
+ open: Boolean,
360
+ captive: Boolean,
361
+ zIndex: Number,
362
+ };
363
+
364
+ connect() {
365
+
366
+ this.defaultZIndexValue = this.zIndexValue;
367
+ this.defaultCaptiveValue = this.captiveValue;
368
+
369
+ this.element.scrim = this;
370
+ }
371
+
372
+ disconnect() {
373
+
374
+ delete this.element.scrim;
375
+ }
376
+
377
+ async show({
378
+ captive = this.defaultCaptiveValue,
379
+ zIndex = this.defaultZIndexValue,
380
+ top = window.scrollY,
381
+ animate = true,
382
+ } = {}) {
383
+
384
+ // hide the scrim before opening the new one if it's already open
385
+ if (this.openValue) {
386
+ await this.hide({ animate });
387
+ }
388
+
389
+ // update internal state
390
+ this.openValue = true;
391
+
392
+ // notify listeners of pending request
393
+ this.dispatch("show", { bubbles: true });
394
+
395
+ // update state, perform style updates
396
+ this.#show(captive, zIndex, top);
397
+
398
+ if (animate) {
399
+ // animate opening
400
+ // this will trigger an animationEnd event via CSS that completes the open
401
+ this.element.dataset.showAnimating = "";
402
+
403
+ await new Promise((resolve) => {
404
+ this.element.addEventListener("animationend", () => resolve(), {
405
+ once: true,
406
+ });
407
+ });
408
+
409
+ delete this.element.dataset.showAnimating;
410
+ }
411
+ }
412
+
413
+ async hide({ animate = true } = {}) {
414
+ if (!this.openValue || this.element.dataset.hideAnimating) return;
415
+
416
+ // notify listeners of pending request
417
+ this.dispatch("hide", { bubbles: true });
418
+
419
+ if (animate) {
420
+ // set animation state
421
+ // this will trigger an animationEnd event via CSS that completes the hide
422
+ this.element.dataset.hideAnimating = "";
423
+
424
+ await new Promise((resolve) => {
425
+ this.element.addEventListener("animationend", () => resolve(), {
426
+ once: true,
427
+ });
428
+ });
429
+
430
+ delete this.element.dataset.hideAnimating;
431
+ }
432
+
433
+ this.#hide();
434
+
435
+ this.openValue = false;
436
+ }
437
+
438
+ dismiss(event) {
439
+
440
+ if (!this.captiveValue) this.dispatch("dismiss", { bubbles: true });
441
+ }
442
+
443
+ escape(event) {
444
+ if (
445
+ event.key === "Escape" &&
446
+ !this.captiveValue &&
447
+ !event.defaultPrevented
448
+ ) {
449
+ this.dispatch("dismiss", { bubbles: true });
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Clips body to viewport size and sets the z-index
455
+ */
456
+ #show(captive, zIndex, top) {
457
+ this.captiveValue = captive;
458
+ this.zIndexValue = zIndex;
459
+ this.scrollY = top;
460
+
461
+ this.previousPosition = document.body.style.position;
462
+ this.previousTop = document.body.style.top;
463
+
464
+ this.element.style.zIndex = this.zIndexValue;
465
+ document.body.style.top = `-${top}px`;
466
+ document.body.style.position = "fixed";
467
+ }
468
+
469
+ /**
470
+ * Unclips body from viewport size and unsets the z-index
471
+ */
472
+ #hide() {
473
+ this.captiveValue = this.defaultCaptiveValue;
474
+ this.zIndexValue = this.defaultZIndexValue;
475
+
476
+ resetStyle(this.element, "z-index", null);
477
+ resetStyle(document.body, "position", null);
478
+ resetStyle(document.body, "top", null);
479
+
480
+ window.scrollTo({ left: 0, top: this.scrollY, behavior: "instant" });
481
+
482
+ delete this.scrollY;
483
+ delete this.previousPosition;
484
+ delete this.previousTop;
485
+ }
486
+ }
487
+
488
+ function resetStyle(element, property, previousValue) {
489
+ if (previousValue) {
490
+ element.style.setProperty(property, previousValue);
491
+ } else {
492
+ element.style.removeProperty(property);
493
+ }
494
+ }
495
+
496
+ class StreamModal extends Modal {
497
+ constructor(id, action) {
498
+ super(id);
499
+
500
+ this.action = action;
501
+ }
502
+
503
+ async open() {
504
+ await super.open();
505
+
506
+ window.history.pushState({ kpop: true, id: this.id }, "", window.location);
507
+ }
508
+
509
+ async dismiss() {
510
+ await super.dismiss();
511
+
512
+ if (this.isCurrentLocation) {
513
+ await this.pop("popstate", () => window.history.back());
514
+ }
515
+
516
+ this.frameElement.innerHTML = "";
517
+ }
518
+
519
+ beforeVisit(frame, e) {
520
+ super.beforeVisit(frame, e);
521
+
522
+ e.preventDefault();
523
+
524
+ frame.dismiss({ animate: false }).then(() => {
525
+ Turbo.visit(e.detail.url);
526
+
527
+ this.debug("before-visit-end");
528
+ });
529
+ }
530
+
531
+ popstate(frame, e) {
532
+ super.popstate(frame, e);
533
+
534
+ frame.dismiss({ animate: true, reason: "popstate" });
535
+ }
536
+
537
+ get isCurrentLocation() {
538
+ return window.history.state?.kpop && window.history.state?.id === this.id;
539
+ }
540
+ }
541
+
542
+ class StreamRenderer {
543
+ constructor(frame, action) {
544
+ this.frame = frame;
545
+ this.action = action;
546
+ }
547
+
548
+ render() {
549
+ this.frame.src = "";
550
+ this.frame.innerHTML = "";
551
+ this.frame.append(this.action.templateContent);
552
+ }
553
+ }
554
+
555
+ function kpop(action) {
556
+ return action.targetElements[0]?.kpop;
557
+ }
558
+
559
+ Turbo.StreamActions.kpop_open = function () {
560
+ const animate = !kpop(this).openValue;
561
+
562
+ kpop(this)
563
+ ?.dismiss({ animate, reason: "before-turbo-stream" })
564
+ .then(() => {
565
+ new StreamRenderer(this.targetElements[0], this).render();
566
+ kpop(this)?.open(new StreamModal(this.target, this), { animate });
567
+ });
568
+ };
569
+
570
+ Turbo.StreamActions.kpop_dismiss = function () {
571
+ kpop(this)?.dismiss({ reason: "turbo_stream.kpop.dismiss" });
572
+ };
573
+
574
+ Turbo.StreamActions.kpop_redirect_to = function () {
575
+ if (this.dataset.turboFrame === this.target) {
576
+ this.targetElements[0].src = this.getAttribute("href");
577
+ } else {
578
+ Turbo.visit(this.getAttribute("href"), {
579
+ action: this.dataset.turboAction,
580
+ });
581
+ }
582
+ };
583
+
584
+ const Definitions = [
585
+ { identifier: "kpop--frame", controllerConstructor: Kpop__FrameController },
586
+ { identifier: "kpop--modal", controllerConstructor: Kpop__ModalController },
587
+ { identifier: "scrim", controllerConstructor: ScrimController },
588
+ ];
589
+
590
+ export { Definitions as default };