katalyst-kpop 2.0.9 → 3.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -43
  3. data/app/assets/builds/katalyst/kpop.esm.js +599 -0
  4. data/app/assets/builds/katalyst/kpop.js +479 -519
  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 +45 -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 +15 -0
  25. data/app/javascript/kpop/controllers/close_controller.js +9 -0
  26. data/app/javascript/kpop/controllers/frame_controller.js +189 -0
  27. data/app/javascript/kpop/controllers/modal_controller.js +30 -0
  28. data/app/javascript/kpop/controllers/redirect_controller.js +22 -0
  29. data/app/{assets/javascripts → javascript/kpop}/controllers/scrim_controller.js +76 -72
  30. data/app/javascript/kpop/debug.js +3 -0
  31. data/app/javascript/kpop/modals/content_modal.js +46 -0
  32. data/app/javascript/kpop/modals/frame_modal.js +41 -0
  33. data/app/javascript/kpop/modals/modal.js +69 -0
  34. data/app/javascript/kpop/modals/stream_modal.js +49 -0
  35. data/app/javascript/kpop/modals/stream_renderer.js +15 -0
  36. data/app/views/layouts/kpop.html.erb +1 -1
  37. data/config/importmap.rb +1 -4
  38. data/lib/katalyst/kpop/engine.rb +13 -12
  39. data/lib/katalyst/kpop/matchers/base.rb +18 -0
  40. data/lib/katalyst/kpop/matchers/capybara_matcher.rb +46 -0
  41. data/lib/katalyst/kpop/matchers/capybara_parser.rb +17 -0
  42. data/lib/katalyst/kpop/matchers/chained_matcher.rb +40 -0
  43. data/lib/katalyst/kpop/matchers/frame_matcher.rb +16 -0
  44. data/lib/katalyst/kpop/matchers/modal_matcher.rb +20 -0
  45. data/lib/katalyst/kpop/matchers/redirect_finder.rb +16 -0
  46. data/lib/katalyst/kpop/matchers/redirect_matcher.rb +28 -0
  47. data/lib/katalyst/kpop/matchers/response_matcher.rb +33 -0
  48. data/lib/katalyst/kpop/matchers/stream_matcher.rb +16 -0
  49. data/lib/katalyst/kpop/matchers/title_finder.rb +16 -0
  50. data/lib/katalyst/kpop/matchers/title_matcher.rb +28 -0
  51. data/lib/katalyst/kpop/matchers.rb +79 -0
  52. data/lib/katalyst/kpop/turbo.rb +56 -0
  53. data/lib/katalyst/kpop/version.rb +1 -1
  54. data/lib/katalyst/kpop.rb +4 -0
  55. metadata +90 -15
  56. data/app/assets/builds/katalyst/kpop.css +0 -117
  57. data/app/assets/javascripts/controllers/kpop_controller.js +0 -72
  58. data/app/assets/javascripts/katalyst/kpop.js +0 -9
  59. data/app/assets/stylesheets/katalyst/kpop/_index.scss +0 -2
  60. data/app/assets/stylesheets/katalyst/kpop/_kpop.scss +0 -133
  61. data/app/helpers/kpop/modal.rb +0 -98
  62. data/app/helpers/scrim_helper.rb +0 -13
@@ -0,0 +1,9 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class Kpop__CloseController extends Controller {
4
+ static outlets = ["kpop--frame"];
5
+
6
+ kpopFrameOutletConnected(frame) {
7
+ frame.dismiss();
8
+ }
9
+ }
@@ -0,0 +1,189 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import { Turbo } from "@hotwired/turbo-rails";
3
+
4
+ import DEBUG from "../debug";
5
+ import { ContentModal } from "../modals/content_modal";
6
+ import { FrameModal } from "../modals/frame_modal";
7
+ import { StreamModal } from "../modals/stream_modal";
8
+ import { StreamRenderer } from "../modals/stream_renderer";
9
+
10
+ Turbo.StreamActions.kpop_open = function () {
11
+ const frame = () => {
12
+ return this.targetElements[0];
13
+ };
14
+ const animate = !frame?.kpop?.openValue;
15
+
16
+ frame()
17
+ .kpop.dismiss({ animate, reason: "before-turbo-stream" })
18
+ .then(() => {
19
+ new StreamRenderer(frame(), this).render();
20
+ frame().kpop.open(new StreamModal(this.target, this), { animate });
21
+ });
22
+ };
23
+
24
+ export default class Kpop__FrameController extends Controller {
25
+ static outlets = ["scrim"];
26
+ static targets = ["modal"];
27
+ static values = {
28
+ open: Boolean,
29
+ };
30
+
31
+ connect() {
32
+ this.debug("connect", this.element.src);
33
+
34
+ this.element.kpop = this;
35
+
36
+ // restoration visit
37
+ if (this.element.src && this.element.complete) {
38
+ this.debug("new frame modal", this.element.src);
39
+ this.open(new FrameModal(this.element.id, this.element.src), {
40
+ animate: false,
41
+ });
42
+ } else {
43
+ const element = this.element.querySelector(
44
+ "[data-controller*='kpop--modal']"
45
+ );
46
+ if (element) {
47
+ this.debug("new content modal", window.location.pathname);
48
+ this.open(new ContentModal(this.element.id), { animate: false });
49
+ }
50
+ }
51
+ }
52
+
53
+ disconnect() {
54
+ this.debug("disconnect");
55
+
56
+ delete this.element.kpop;
57
+ delete this.modal;
58
+ }
59
+
60
+ scrimOutletConnected(scrim) {
61
+ this.debug("scrim-connected");
62
+
63
+ this.scrimConnected = true;
64
+
65
+ if (this.openValue) scrim.show({ animate: false });
66
+ }
67
+
68
+ openValueChanged(open) {
69
+ this.debug("open-changed", open);
70
+
71
+ this.element.parentElement.style.display = open ? "flex" : "none";
72
+ }
73
+
74
+ async open(modal, { animate = true } = {}) {
75
+ if (this.isOpen) {
76
+ this.debug("skip open as already open");
77
+ return false;
78
+ }
79
+
80
+ this.opening ||= this.#nextAnimationFrame(() => {
81
+ return this.#open(modal, { animate });
82
+ });
83
+
84
+ return this.opening;
85
+ }
86
+
87
+ async dismiss({ animate = true, reason = "" } = {}) {
88
+ if (!this.isOpen) {
89
+ this.debug("skip dismiss as already closed");
90
+ return false;
91
+ }
92
+
93
+ this.dismissing ||= this.#nextAnimationFrame(() => {
94
+ return new Promise((resolve) => {
95
+ this.#dismiss({ animate, reason }).then(() => {
96
+ return this.#nextAnimationFrame(resolve);
97
+ });
98
+ });
99
+ });
100
+
101
+ return this.dismissing;
102
+ }
103
+
104
+ // EVENTS
105
+
106
+ popstate(event) {
107
+ this.modal?.popstate(this, event);
108
+ }
109
+
110
+ beforeFrameRender(event) {
111
+ this.debug("before-frame-render", event.detail.newFrame.baseURI);
112
+
113
+ event.preventDefault();
114
+
115
+ this.dismiss({ animate: true, reason: "before-frame-render" }).then(() => {
116
+ event.detail.resume();
117
+ });
118
+ }
119
+
120
+ beforeVisit(e) {
121
+ this.debug("before-visit", e.detail.url);
122
+
123
+ // ignore visits to the current frame, these fire when the frame navigates
124
+ if (e.detail.url === this.element.src) return;
125
+
126
+ // ignore unless we're open
127
+ if (!this.isOpen) return;
128
+
129
+ this.modal.beforeVisit(this, e);
130
+ }
131
+
132
+ frameLoad(event) {
133
+ this.debug("frame-load");
134
+
135
+ return this.open(new FrameModal(this.element.id, this.element.src), {
136
+ animate: true,
137
+ });
138
+ }
139
+
140
+ get isOpen() {
141
+ return this.openValue && !this.dismissing;
142
+ }
143
+
144
+ async #open(modal, { animate = true } = {}) {
145
+ this.debug("open-start", { animate });
146
+
147
+ const scrim = this.scrimConnected && this.scrimOutlet;
148
+
149
+ this.modal = modal;
150
+ this.openValue = true;
151
+
152
+ modal.open({ animate });
153
+ scrim?.show({ animate });
154
+
155
+ delete this.opening;
156
+
157
+ this.debug("open-end");
158
+ }
159
+
160
+ async #dismiss({ animate = true, reason = "" } = {}) {
161
+ this.debug("dismiss-start", { animate, reason });
162
+
163
+ if (!this.modal) {
164
+ console.warn("modal missing on dismiss");
165
+ if (DEBUG) debugger;
166
+ }
167
+
168
+ await this.scrimOutlet.hide({ animate });
169
+ await this.modal?.dismiss();
170
+
171
+ this.openValue = false;
172
+ this.modal = null;
173
+ delete this.dismissing;
174
+
175
+ this.debug("dismiss-end");
176
+ }
177
+
178
+ #nextAnimationFrame(callback) {
179
+ return new Promise((resolve) => {
180
+ window.requestAnimationFrame(() => {
181
+ resolve(callback());
182
+ });
183
+ });
184
+ }
185
+
186
+ debug(event, ...args) {
187
+ if (DEBUG) console.debug(`FrameController:${event}`, ...args);
188
+ }
189
+ }
@@ -0,0 +1,30 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ import DEBUG from "../debug";
4
+
5
+ export default class Kpop__ModalController extends Controller {
6
+ static values = {
7
+ fallback_location: String,
8
+ layout: String,
9
+ };
10
+
11
+ connect() {
12
+ this.debug("connect");
13
+
14
+ if (this.layoutValue) {
15
+ document.querySelector("#kpop").classList.toggle(this.layoutValue, true);
16
+ }
17
+ }
18
+
19
+ disconnect() {
20
+ this.debug("disconnect");
21
+
22
+ if (this.layoutValue) {
23
+ document.querySelector("#kpop").classList.toggle(this.layoutValue, false);
24
+ }
25
+ }
26
+
27
+ debug(event, ...args) {
28
+ if (DEBUG) console.debug(`ModalController:${event}`, ...args);
29
+ }
30
+ }
@@ -0,0 +1,22 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import { Turbo } from "@hotwired/turbo-rails";
3
+
4
+ export default class Kpop__RedirectController extends Controller {
5
+ static outlets = ["kpop--frame"];
6
+ static values = {
7
+ path: String,
8
+ target: String,
9
+ };
10
+
11
+ kpopFrameOutletConnected(frame) {
12
+ if (this.targetValue === frame.element.id) {
13
+ frame.dismiss().then(() => {
14
+ document.getElementById(this.targetValue).src = this.pathValue;
15
+ });
16
+ } else {
17
+ Turbo.visit(this.pathValue, { action: "replace" });
18
+ }
19
+
20
+ this.element.remove();
21
+ }
22
+ }
@@ -1,6 +1,6 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
2
 
3
- const DEBUG = false;
3
+ import DEBUG from "../debug";
4
4
 
5
5
  /**
6
6
  * Scrim controller wraps an element that creates a whole page layer.
@@ -8,8 +8,7 @@ const DEBUG = false;
8
8
  *
9
9
  * If the Scrim element receives a click event, it automatically triggers "scrim:hide".
10
10
  *
11
- * You can show and hide the scrim programmatically by sending "scrim:request:show" and "scrim:request:hide" events to
12
- * the window or by calling the provided methods.
11
+ * You can show and hide the scrim programmatically by calling show/hide on the controller, e.g. using an outlet.
13
12
  *
14
13
  * If you need to respond to the scrim showing or hiding you should subscribe to "scrim:show" and "scrim:hide".
15
14
  */
@@ -20,109 +19,113 @@ export default class ScrimController extends Controller {
20
19
  zIndex: Number,
21
20
  };
22
21
 
23
- /**
24
- * Show the scrim element. Returns true if successful.
25
- */
26
- static showScrim({
27
- dismiss = true,
28
- zIndex = undefined,
29
- top = undefined,
30
- } = {}) {
31
- return window.dispatchEvent(
32
- new CustomEvent("scrim:request:show", {
33
- cancelable: true,
34
- detail: { captive: !dismiss, zIndex: zIndex, top: top },
35
- })
36
- );
37
- }
38
-
39
- /**
40
- * Hide the scrim element. Returns true if successful.
41
- */
42
- static hideScrim() {
43
- return window.dispatchEvent(
44
- new CustomEvent("scrim:request:hide", { cancelable: true })
45
- );
46
- }
47
-
48
22
  connect() {
23
+ if (DEBUG) console.debug("scrim:connect");
24
+
49
25
  this.defaultZIndexValue = this.zIndexValue;
50
26
  this.defaultCaptiveValue = this.captiveValue;
27
+
28
+ this.element.scrim = this;
51
29
  }
52
30
 
53
- show(request) {
54
- if (DEBUG) console.debug("request show scrim");
31
+ disconnect() {
32
+ if (DEBUG) console.debug("scrim:disconnect");
55
33
 
56
- // hide the scrim before opening the new one if it's already open
57
- if (this.openValue) this.hide(request);
34
+ delete this.element.scrim;
35
+ }
58
36
 
59
- // if the scrim is still open, abort
60
- if (this.openValue) return;
37
+ async show({
38
+ captive = this.defaultCaptiveValue,
39
+ zIndex = this.defaultZIndexValue,
40
+ top = window.scrollY,
41
+ animate = true,
42
+ } = {}) {
43
+ if (DEBUG) console.debug("scrim:before-show");
61
44
 
62
- // update internal state to break event cycles
45
+ // hide the scrim before opening the new one if it's already open
46
+ if (this.openValue) {
47
+ await this.hide({ animate });
48
+ }
49
+
50
+ // update internal state
63
51
  this.openValue = true;
64
52
 
65
53
  // notify listeners of pending request
66
- const event = this.dispatch("show", { bubbles: true, cancelable: true });
54
+ this.dispatch("show", { bubbles: true });
67
55
 
68
- // if notification was cancelled, update request and abort
69
- if (event.defaultPrevented) {
70
- this.openValue = false;
71
- request.preventDefault();
72
- return;
73
- }
56
+ if (DEBUG) console.debug("scrim:show-start");
57
+
58
+ // update state, perform style updates
59
+ this.#show(captive, zIndex, top);
74
60
 
75
- if (DEBUG) console.debug("show scrim");
61
+ if (animate) {
62
+ // animate opening
63
+ // this will trigger an animationEnd event via CSS that completes the open
64
+ this.element.dataset.showAnimating = "";
76
65
 
77
- // perform show updates
78
- this.#show(request.detail);
79
- }
66
+ await new Promise((resolve) => {
67
+ this.element.addEventListener("animationend", () => resolve(), {
68
+ once: true,
69
+ });
70
+ });
71
+
72
+ delete this.element.dataset.showAnimating;
73
+ }
80
74
 
81
- hide(request) {
82
- if (!this.openValue) return;
75
+ if (DEBUG) console.debug("scrim:show-end");
76
+ }
83
77
 
84
- if (DEBUG) console.debug("request hide scrim");
78
+ async hide({ animate = true } = {}) {
79
+ if (!this.openValue || this.element.dataset.hideAnimating) return;
85
80
 
86
- // update internal state to break event cycles
87
- this.openValue = false;
81
+ if (DEBUG) console.debug("scrim:before-hide");
88
82
 
89
83
  // notify listeners of pending request
90
- const event = this.dispatch("hide", { bubbles: true, cancelable: true });
84
+ this.dispatch("hide", { bubbles: true });
91
85
 
92
- // if notification was cancelled, update request and abort
93
- if (event.defaultPrevented) {
94
- this.openValue = true;
95
- request.preventDefault();
96
- return;
97
- }
86
+ if (DEBUG) console.debug("scrim:hide-start");
98
87
 
99
- if (DEBUG) console.debug("hide scrim");
88
+ if (animate) {
89
+ // set animation state
90
+ // this will trigger an animationEnd event via CSS that completes the hide
91
+ this.element.dataset.hideAnimating = "";
92
+
93
+ await new Promise((resolve) => {
94
+ this.element.addEventListener("animationend", () => resolve(), {
95
+ once: true,
96
+ });
97
+ });
98
+
99
+ delete this.element.dataset.hideAnimating;
100
+ }
100
101
 
101
- // update state, perform style updates
102
102
  this.#hide();
103
+
104
+ this.openValue = false;
105
+
106
+ if (DEBUG) console.debug("scrim:hide-end");
103
107
  }
104
108
 
105
109
  dismiss(event) {
106
- if (!this.captiveValue) this.hide(event);
107
- }
110
+ if (DEBUG) console.debug("scrim:dismiss");
108
111
 
109
- escape(event) {
110
- if (event.key === "Escape" && !this.captiveValue && !event.defaultPrevented)
111
- this.hide(event);
112
+ if (!this.captiveValue) this.dispatch("dismiss", { bubbles: true });
112
113
  }
113
114
 
114
- disconnect() {
115
- super.disconnect();
115
+ escape(event) {
116
+ if (
117
+ event.key === "Escape" &&
118
+ !this.captiveValue &&
119
+ !event.defaultPrevented
120
+ ) {
121
+ this.dispatch("dismiss", { bubbles: true });
122
+ }
116
123
  }
117
124
 
118
125
  /**
119
126
  * Clips body to viewport size and sets the z-index
120
127
  */
121
- #show({
122
- captive = this.defaultCaptiveValue,
123
- zIndex = this.defaultZIndexValue,
124
- top = window.scrollY,
125
- }) {
128
+ #show(captive, zIndex, top) {
126
129
  this.captiveValue = captive;
127
130
  this.zIndexValue = zIndex;
128
131
  this.scrollY = top;
@@ -145,6 +148,7 @@ export default class ScrimController extends Controller {
145
148
  resetStyle(this.element, "z-index", null);
146
149
  resetStyle(document.body, "position", null);
147
150
  resetStyle(document.body, "top", null);
151
+
148
152
  window.scrollTo({ left: 0, top: this.scrollY, behavior: "instant" });
149
153
 
150
154
  delete this.scrollY;
@@ -0,0 +1,3 @@
1
+ const DEBUG = false;
2
+
3
+ export default DEBUG;
@@ -0,0 +1,46 @@
1
+ import { Turbo } from "@hotwired/turbo-rails";
2
+
3
+ import { Modal } from "./modal";
4
+
5
+ export class ContentModal extends Modal {
6
+ constructor(id, src = null) {
7
+ super(id);
8
+
9
+ if (src) this.src = src;
10
+ }
11
+
12
+ async dismiss() {
13
+ await super.dismiss();
14
+
15
+ if (this.visitStarted) {
16
+ this.debug("skipping dismiss, visit started");
17
+ return;
18
+ }
19
+ if (!this.isCurrentLocation) {
20
+ this.debug("skipping dismiss, not current location");
21
+ return;
22
+ }
23
+
24
+ return this.pop("turbo:load", () => {
25
+ this.debug("turbo-visit", this.fallbackLocationValue);
26
+ Turbo.visit(this.fallbackLocationValue, { action: "replace" });
27
+ });
28
+
29
+ // no specific close action required, this is turbo's responsibility
30
+ }
31
+
32
+ beforeVisit(frame, e) {
33
+ super.beforeVisit(frame, e);
34
+
35
+ this.visitStarted = true;
36
+
37
+ frame.scrimOutlet.hide({ animate: false });
38
+ }
39
+
40
+ get src() {
41
+ return new URL(
42
+ this.currentLocationValue.toString(),
43
+ document.baseURI
44
+ ).toString();
45
+ }
46
+ }
@@ -0,0 +1,41 @@
1
+ import { Turbo } from "@hotwired/turbo-rails";
2
+
3
+ import { Modal } from "./modal";
4
+
5
+ export class FrameModal extends Modal {
6
+ constructor(id, src) {
7
+ super(id);
8
+ this.src = src;
9
+ }
10
+
11
+ async dismiss() {
12
+ await super.dismiss();
13
+
14
+ if (!this.isCurrentLocation) {
15
+ this.debug("skipping dismiss, not current location");
16
+ }
17
+
18
+ await this.pop("turbo:load", () => window.history.back());
19
+
20
+ // no specific close action required, this is turbo's responsibility
21
+ }
22
+
23
+ beforeVisit(frame, e) {
24
+ super.beforeVisit(frame, e);
25
+
26
+ e.preventDefault();
27
+
28
+ frame.dismiss({ animate: false }).then(() => {
29
+ Turbo.visit(e.detail.url);
30
+
31
+ this.debug("before-visit-end");
32
+ });
33
+ }
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
+ }
@@ -0,0 +1,69 @@
1
+ import { Turbo } from "@hotwired/turbo-rails";
2
+
3
+ import DEBUG from "../debug";
4
+
5
+ export class Modal {
6
+ constructor(id) {
7
+ this.id = id;
8
+ }
9
+
10
+ async open() {
11
+ this.debug("open");
12
+ }
13
+
14
+ async dismiss() {
15
+ this.debug(`dismiss`);
16
+ }
17
+
18
+ beforeVisit(frame, e) {
19
+ this.debug(`before-visit`, e.detail.url);
20
+ }
21
+
22
+ popstate(frame, e) {
23
+ this.debug(`popstate`, e.state);
24
+ }
25
+
26
+ async pop(event, callback) {
27
+ this.debug(`pop`);
28
+
29
+ const promise = new Promise((resolve) => {
30
+ window.addEventListener(
31
+ event,
32
+ () => {
33
+ resolve();
34
+ },
35
+ { once: true }
36
+ );
37
+ });
38
+
39
+ callback();
40
+
41
+ return promise;
42
+ }
43
+
44
+ get frameElement() {
45
+ return document.getElementById(this.id);
46
+ }
47
+
48
+ get modalElement() {
49
+ return this.frameElement?.querySelector("[data-controller*='kpop--modal']");
50
+ }
51
+
52
+ get currentLocationValue() {
53
+ return this.modalElement?.dataset["kpop-ModalCurrentLocationValue"] || "/";
54
+ }
55
+
56
+ get fallbackLocationValue() {
57
+ return this.modalElement?.dataset["kpop-ModalFallbackLocationValue"] || "/";
58
+ }
59
+
60
+ get isCurrentLocation() {
61
+ return (
62
+ window.history.state?.turbo && Turbo.session.location.href === this.src
63
+ );
64
+ }
65
+
66
+ debug(event, ...args) {
67
+ if (DEBUG) console.debug(`${this.constructor.name}:${event}`, ...args);
68
+ }
69
+ }
@@ -0,0 +1,49 @@
1
+ import { Turbo } from "@hotwired/turbo-rails";
2
+
3
+ import { Modal } from "./modal";
4
+
5
+ export class StreamModal extends Modal {
6
+ constructor(id, action) {
7
+ super(id);
8
+
9
+ this.action = action;
10
+ }
11
+
12
+ async open() {
13
+ await super.open();
14
+
15
+ window.history.pushState({ kpop: true, id: this.id }, "", window.location);
16
+ }
17
+
18
+ async dismiss() {
19
+ await super.dismiss();
20
+
21
+ if (this.isCurrentLocation) {
22
+ await this.pop("popstate", () => window.history.back());
23
+ }
24
+
25
+ this.frameElement.innerHTML = "";
26
+ }
27
+
28
+ beforeVisit(frame, e) {
29
+ super.beforeVisit(frame, e);
30
+
31
+ e.preventDefault();
32
+
33
+ frame.dismiss({ animate: false }).then(() => {
34
+ Turbo.visit(e.detail.url);
35
+
36
+ this.debug("before-visit-end");
37
+ });
38
+ }
39
+
40
+ popstate(frame, e) {
41
+ super.popstate(frame, e);
42
+
43
+ frame.dismiss({ animate: true, reason: "popstate" });
44
+ }
45
+
46
+ get isCurrentLocation() {
47
+ return window.history.state?.kpop && window.history.state?.id === this.id;
48
+ }
49
+ }
@@ -0,0 +1,15 @@
1
+ import DEBUG from "../debug";
2
+
3
+ export class StreamRenderer {
4
+ constructor(frame, action) {
5
+ this.frame = frame;
6
+ this.action = action;
7
+ }
8
+
9
+ render() {
10
+ if (DEBUG) console.debug("stream-renderer:render");
11
+ this.frame.src = "";
12
+ this.frame.innerHTML = "";
13
+ this.frame.append(this.action.templateContent);
14
+ }
15
+ }
@@ -1,4 +1,4 @@
1
1
  <%# controller layout, for use with turbo responses %>
2
- <%= kpop_frame_tag do %>
2
+ <%= render Kpop::FrameComponent.new do %>
3
3
  <%= yield %>
4
4
  <% end %>