modal_stack 0.1.1
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 +7 -0
- data/CHANGELOG.md +37 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +748 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/modal_stack.js +756 -0
- data/app/assets/stylesheets/modal_stack/bootstrap.css +232 -0
- data/app/assets/stylesheets/modal_stack/tailwind.css +303 -0
- data/app/assets/stylesheets/modal_stack/vanilla.css +219 -0
- data/app/javascript/modal_stack/controllers/modal_stack_controller.js +149 -0
- data/app/javascript/modal_stack/controllers/modal_stack_link_controller.js +34 -0
- data/app/javascript/modal_stack/index.js +15 -0
- data/app/javascript/modal_stack/install.js +15 -0
- data/app/javascript/modal_stack/orchestrator.js +98 -0
- data/app/javascript/modal_stack/orchestrator.test.js +260 -0
- data/app/javascript/modal_stack/runtime.js +217 -0
- data/app/javascript/modal_stack/runtime.test.js +134 -0
- data/app/javascript/modal_stack/state.js +315 -0
- data/app/javascript/modal_stack/state.test.js +508 -0
- data/app/views/layouts/modal.html.erb +6 -0
- data/lib/generators/modal_stack/install/install_generator.rb +224 -0
- data/lib/generators/modal_stack/install/templates/initializer.rb +57 -0
- data/lib/modal_stack/capybara/minitest.rb +9 -0
- data/lib/modal_stack/capybara/rspec.rb +9 -0
- data/lib/modal_stack/capybara.rb +85 -0
- data/lib/modal_stack/configuration.rb +90 -0
- data/lib/modal_stack/controller_extensions.rb +73 -0
- data/lib/modal_stack/engine.rb +44 -0
- data/lib/modal_stack/helpers/modal_link_helper.rb +65 -0
- data/lib/modal_stack/helpers/modal_stack_assets_helper.rb +45 -0
- data/lib/modal_stack/helpers/modal_stack_container_helper.rb +36 -0
- data/lib/modal_stack/initializer_version_check.rb +33 -0
- data/lib/modal_stack/turbo_streams_extension.rb +73 -0
- data/lib/modal_stack/version.rb +5 -0
- data/lib/modal_stack.rb +36 -0
- metadata +130 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* modal_stack — Vanilla preset
|
|
3
|
+
*
|
|
4
|
+
* Pure CSS, no framework dependency. Override any of the
|
|
5
|
+
* --modal-stack-* tokens on :root to retheme.
|
|
6
|
+
*
|
|
7
|
+
* Variants: [data-variant="modal" | "drawer" | "bottom_sheet" | "confirmation"]
|
|
8
|
+
* Drawer side: [data-side="left" | "right"]
|
|
9
|
+
* Sizes: [data-modal-stack-size="sm" | "md" | "lg" | "xl"]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
--modal-stack-z-base: 1000;
|
|
14
|
+
--modal-stack-duration: 220ms;
|
|
15
|
+
--modal-stack-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
|
16
|
+
--modal-stack-radius: 8px;
|
|
17
|
+
--modal-stack-bg: #ffffff;
|
|
18
|
+
--modal-stack-fg: #1a1a1a;
|
|
19
|
+
--modal-stack-shadow: 0 24px 60px -16px rgba(0, 0, 0, 0.32);
|
|
20
|
+
--modal-stack-backdrop: rgba(0, 0, 0, 0.5);
|
|
21
|
+
--modal-stack-backdrop-blur: 0;
|
|
22
|
+
--modal-stack-panel-padding: 20px;
|
|
23
|
+
--modal-stack-size-sm: 320px;
|
|
24
|
+
--modal-stack-size-md: 480px;
|
|
25
|
+
--modal-stack-size-lg: 720px;
|
|
26
|
+
--modal-stack-size-xl: 960px;
|
|
27
|
+
--modal-stack-drawer-width: 400px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
body[data-modal-stack-locked] {
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
padding-right: var(--modal-stack-scrollbar-width, 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#modal-stack-root {
|
|
36
|
+
position: fixed;
|
|
37
|
+
inset: 0;
|
|
38
|
+
width: 100vw;
|
|
39
|
+
height: 100vh;
|
|
40
|
+
margin: 0;
|
|
41
|
+
padding: 0;
|
|
42
|
+
border: 0;
|
|
43
|
+
background: transparent;
|
|
44
|
+
max-width: none;
|
|
45
|
+
max-height: none;
|
|
46
|
+
overflow: visible;
|
|
47
|
+
z-index: var(--modal-stack-z-base);
|
|
48
|
+
opacity: 0;
|
|
49
|
+
transition:
|
|
50
|
+
opacity var(--modal-stack-duration) var(--modal-stack-ease),
|
|
51
|
+
overlay var(--modal-stack-duration) var(--modal-stack-ease) allow-discrete,
|
|
52
|
+
display var(--modal-stack-duration) var(--modal-stack-ease) allow-discrete;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#modal-stack-root[open] { opacity: 1; }
|
|
56
|
+
|
|
57
|
+
@starting-style {
|
|
58
|
+
#modal-stack-root[open] { opacity: 0; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#modal-stack-root::backdrop {
|
|
62
|
+
background: rgba(0, 0, 0, 0);
|
|
63
|
+
backdrop-filter: blur(0);
|
|
64
|
+
transition:
|
|
65
|
+
background var(--modal-stack-duration) var(--modal-stack-ease),
|
|
66
|
+
backdrop-filter var(--modal-stack-duration) var(--modal-stack-ease),
|
|
67
|
+
overlay var(--modal-stack-duration) var(--modal-stack-ease) allow-discrete,
|
|
68
|
+
display var(--modal-stack-duration) var(--modal-stack-ease) allow-discrete;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#modal-stack-root[open]::backdrop {
|
|
72
|
+
background: var(--modal-stack-backdrop);
|
|
73
|
+
backdrop-filter: blur(var(--modal-stack-backdrop-blur));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@starting-style {
|
|
77
|
+
#modal-stack-root[open]::backdrop {
|
|
78
|
+
background: rgba(0, 0, 0, 0);
|
|
79
|
+
backdrop-filter: blur(0);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[data-modal-stack-target="layer"] {
|
|
84
|
+
position: absolute;
|
|
85
|
+
top: 50%;
|
|
86
|
+
left: 50%;
|
|
87
|
+
width: var(--modal-stack-size-md);
|
|
88
|
+
max-width: 92vw;
|
|
89
|
+
max-height: min(85vh, 720px);
|
|
90
|
+
overflow-y: auto;
|
|
91
|
+
background: var(--modal-stack-bg);
|
|
92
|
+
color: var(--modal-stack-fg);
|
|
93
|
+
border-radius: var(--modal-stack-radius);
|
|
94
|
+
box-shadow: var(--modal-stack-shadow);
|
|
95
|
+
padding: 0;
|
|
96
|
+
transform: translate(-50%, -50%);
|
|
97
|
+
transition:
|
|
98
|
+
transform var(--modal-stack-duration) var(--modal-stack-ease),
|
|
99
|
+
opacity var(--modal-stack-duration) var(--modal-stack-ease),
|
|
100
|
+
filter var(--modal-stack-duration) var(--modal-stack-ease);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@starting-style {
|
|
104
|
+
[data-modal-stack-target="layer"] {
|
|
105
|
+
opacity: 0;
|
|
106
|
+
transform: translate(-50%, -44%) scale(0.97);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
[data-modal-stack-target="layer"][data-leaving] {
|
|
111
|
+
opacity: 0;
|
|
112
|
+
transform: translate(-50%, -56%) scale(0.97);
|
|
113
|
+
pointer-events: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
[data-modal-stack-target="layer"][inert] {
|
|
117
|
+
opacity: 0.5;
|
|
118
|
+
filter: blur(0.5px);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
[data-modal-stack-target="layer"][data-modal-stack-size="sm"] { width: var(--modal-stack-size-sm); }
|
|
122
|
+
[data-modal-stack-target="layer"][data-modal-stack-size="md"] { width: var(--modal-stack-size-md); }
|
|
123
|
+
[data-modal-stack-target="layer"][data-modal-stack-size="lg"] { width: var(--modal-stack-size-lg); }
|
|
124
|
+
[data-modal-stack-target="layer"][data-modal-stack-size="xl"] { width: var(--modal-stack-size-xl); }
|
|
125
|
+
|
|
126
|
+
[data-modal-stack-target="layer"][data-depth="2"] {
|
|
127
|
+
transform: translate(-50%, -53%) scale(1.015);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
[data-modal-stack-target="layer"][data-depth="3"] {
|
|
131
|
+
transform: translate(-50%, -56%) scale(1.03);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
[data-modal-stack-target="layer"][data-depth="4"] {
|
|
135
|
+
transform: translate(-50%, -59%) scale(1.045);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[data-modal-stack-target="layer"][data-variant="drawer"] {
|
|
139
|
+
top: 0;
|
|
140
|
+
height: 100vh;
|
|
141
|
+
max-height: none;
|
|
142
|
+
width: var(--modal-stack-drawer-width);
|
|
143
|
+
max-width: 90vw;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="right"] {
|
|
147
|
+
left: auto;
|
|
148
|
+
right: 0;
|
|
149
|
+
transform: translateX(0);
|
|
150
|
+
border-radius: var(--modal-stack-radius) 0 0 var(--modal-stack-radius);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="left"] {
|
|
154
|
+
left: 0;
|
|
155
|
+
transform: translateX(0);
|
|
156
|
+
border-radius: 0 var(--modal-stack-radius) var(--modal-stack-radius) 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@starting-style {
|
|
160
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="right"] {
|
|
161
|
+
opacity: 0;
|
|
162
|
+
transform: translateX(100%);
|
|
163
|
+
}
|
|
164
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="left"] {
|
|
165
|
+
opacity: 0;
|
|
166
|
+
transform: translateX(-100%);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="right"][data-leaving] {
|
|
171
|
+
opacity: 0;
|
|
172
|
+
transform: translateX(100%);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
[data-modal-stack-target="layer"][data-variant="drawer"][data-side="left"][data-leaving] {
|
|
176
|
+
opacity: 0;
|
|
177
|
+
transform: translateX(-100%);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
[data-modal-stack-target="layer"][data-variant="bottom_sheet"] {
|
|
181
|
+
top: auto;
|
|
182
|
+
bottom: 0;
|
|
183
|
+
left: 0;
|
|
184
|
+
width: 100vw;
|
|
185
|
+
max-width: none;
|
|
186
|
+
transform: translate(0, 0);
|
|
187
|
+
border-radius: var(--modal-stack-radius) var(--modal-stack-radius) 0 0;
|
|
188
|
+
max-height: 90vh;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@starting-style {
|
|
192
|
+
[data-modal-stack-target="layer"][data-variant="bottom_sheet"] {
|
|
193
|
+
opacity: 0;
|
|
194
|
+
transform: translateY(100%);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
[data-modal-stack-target="layer"][data-variant="bottom_sheet"][data-leaving] {
|
|
199
|
+
opacity: 0;
|
|
200
|
+
transform: translateY(100%);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
[data-modal-stack-target="layer"][data-variant="confirmation"] {
|
|
204
|
+
width: var(--modal-stack-size-sm);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.modal-stack__panel {
|
|
208
|
+
padding: var(--modal-stack-panel-padding);
|
|
209
|
+
display: grid;
|
|
210
|
+
gap: 12px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@media (prefers-reduced-motion: reduce) {
|
|
214
|
+
#modal-stack-root,
|
|
215
|
+
#modal-stack-root::backdrop,
|
|
216
|
+
[data-modal-stack-target="layer"] {
|
|
217
|
+
transition-duration: 1ms !important;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
import { Orchestrator } from "../orchestrator.js";
|
|
3
|
+
import { BrowserRuntime } from "../runtime.js";
|
|
4
|
+
|
|
5
|
+
export class ModalStackController extends Controller {
|
|
6
|
+
static values = {
|
|
7
|
+
stackId: String,
|
|
8
|
+
baseUrl: String,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
const stackId = this.stackIdValue || generateLayerId();
|
|
13
|
+
const baseUrl = this.baseUrlValue || window.location.href;
|
|
14
|
+
|
|
15
|
+
this.runtime = new BrowserRuntime({ dialog: this.element });
|
|
16
|
+
const snapshot = this.runtime.readSnapshot();
|
|
17
|
+
|
|
18
|
+
this.orchestrator = new Orchestrator({
|
|
19
|
+
runtime: this.runtime,
|
|
20
|
+
stackId,
|
|
21
|
+
baseUrl,
|
|
22
|
+
restoreFrom: snapshot,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
this._onPopstate = (event) =>
|
|
26
|
+
this.orchestrator.onPopstate({
|
|
27
|
+
historyState: event.state,
|
|
28
|
+
locationHref: window.location.href,
|
|
29
|
+
});
|
|
30
|
+
window.addEventListener("popstate", this._onPopstate);
|
|
31
|
+
|
|
32
|
+
this._onCancel = (event) => {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
const top = this.#topLayer();
|
|
35
|
+
if (!top || top.dismissible === false) return;
|
|
36
|
+
this.orchestrator.pop();
|
|
37
|
+
};
|
|
38
|
+
this.element.addEventListener("cancel", this._onCancel);
|
|
39
|
+
|
|
40
|
+
this._onBackdropClick = (event) => {
|
|
41
|
+
if (event.target !== this.element) return;
|
|
42
|
+
const top = this.#topLayer();
|
|
43
|
+
if (!top || top.dismissible === false) return;
|
|
44
|
+
this.orchestrator.pop();
|
|
45
|
+
};
|
|
46
|
+
this.element.addEventListener("click", this._onBackdropClick);
|
|
47
|
+
|
|
48
|
+
this.#registerStreamActions();
|
|
49
|
+
this.element.dispatchEvent(
|
|
50
|
+
new CustomEvent("modal_stack:ready", { bubbles: true, detail: { stackId } }),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
disconnect() {
|
|
55
|
+
window.removeEventListener("popstate", this._onPopstate);
|
|
56
|
+
this.element.removeEventListener("cancel", this._onCancel);
|
|
57
|
+
this.element.removeEventListener("click", this._onBackdropClick);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
push(layer, opts) {
|
|
61
|
+
return this.orchestrator.push(layer, opts);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pop() {
|
|
65
|
+
return this.orchestrator.pop();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
replaceTop(patch, opts) {
|
|
69
|
+
return this.orchestrator.replaceTop(patch, opts);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
closeAll() {
|
|
73
|
+
return this.orchestrator.closeAll();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#topLayer() {
|
|
77
|
+
const layers = this.orchestrator.layers;
|
|
78
|
+
return layers[layers.length - 1] ?? null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#registerStreamActions() {
|
|
82
|
+
const Turbo = globalThis.Turbo;
|
|
83
|
+
if (!Turbo) {
|
|
84
|
+
console.warn(
|
|
85
|
+
"[modal_stack] Turbo is not loaded; modal_push/pop/replace stream actions are disabled. " +
|
|
86
|
+
"Ensure turbo-rails (or @hotwired/turbo) loads before modal_stack.",
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const StreamActions = Turbo.StreamActions || (Turbo.StreamActions = {});
|
|
91
|
+
const orchestrator = this.orchestrator;
|
|
92
|
+
|
|
93
|
+
StreamActions.modal_push = function modalPush() {
|
|
94
|
+
orchestrator.push(layerFromStreamElement(this), {
|
|
95
|
+
fragment: this.templateContent.cloneNode(true),
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
StreamActions.modal_pop = function modalPop() {
|
|
100
|
+
orchestrator.pop();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
StreamActions.modal_replace = function modalReplace() {
|
|
104
|
+
orchestrator.replaceTop(layerPatchFromStreamElement(this), {
|
|
105
|
+
fragment: this.templateContent.cloneNode(true),
|
|
106
|
+
historyMode: this.dataset.historyMode || "replace",
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
StreamActions.modal_close_all = function modalCloseAll() {
|
|
111
|
+
orchestrator.closeAll();
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function layerFromStreamElement(el) {
|
|
117
|
+
return {
|
|
118
|
+
id: el.dataset.layerId || generateLayerId(),
|
|
119
|
+
url: el.dataset.url || window.location.href,
|
|
120
|
+
variant: el.dataset.variant || "modal",
|
|
121
|
+
side: el.dataset.side,
|
|
122
|
+
size: el.dataset.size,
|
|
123
|
+
width: el.dataset.width,
|
|
124
|
+
height: el.dataset.height,
|
|
125
|
+
dismissible: el.dataset.dismissible !== "false",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function layerPatchFromStreamElement(el) {
|
|
130
|
+
const patch = {};
|
|
131
|
+
if (el.dataset.layerId) patch.id = el.dataset.layerId;
|
|
132
|
+
if (el.dataset.url) patch.url = el.dataset.url;
|
|
133
|
+
if (el.dataset.variant) patch.variant = el.dataset.variant;
|
|
134
|
+
if (el.dataset.side) patch.side = el.dataset.side;
|
|
135
|
+
if (el.dataset.size) patch.size = el.dataset.size;
|
|
136
|
+
if (el.dataset.width) patch.width = el.dataset.width;
|
|
137
|
+
if (el.dataset.height) patch.height = el.dataset.height;
|
|
138
|
+
if (el.dataset.dismissible != null) {
|
|
139
|
+
patch.dismissible = el.dataset.dismissible !== "false";
|
|
140
|
+
}
|
|
141
|
+
return patch;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function generateLayerId() {
|
|
145
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
146
|
+
return crypto.randomUUID();
|
|
147
|
+
}
|
|
148
|
+
return `ms-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
149
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
export class ModalStackLinkController extends Controller {
|
|
4
|
+
open(event) {
|
|
5
|
+
const stack = document.querySelector('[data-controller~="modal-stack"]');
|
|
6
|
+
if (!stack) return;
|
|
7
|
+
|
|
8
|
+
const controller = this.application.getControllerForElementAndIdentifier(
|
|
9
|
+
stack,
|
|
10
|
+
"modal-stack",
|
|
11
|
+
);
|
|
12
|
+
if (!controller) return;
|
|
13
|
+
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
const ds = this.element.dataset;
|
|
16
|
+
controller.push({
|
|
17
|
+
id: generateLayerId(),
|
|
18
|
+
url: this.element.href,
|
|
19
|
+
variant: ds.modalStackLinkVariant || "modal",
|
|
20
|
+
side: ds.modalStackLinkSide,
|
|
21
|
+
size: ds.modalStackLinkSize,
|
|
22
|
+
width: ds.modalStackLinkWidth,
|
|
23
|
+
height: ds.modalStackLinkHeight,
|
|
24
|
+
dismissible: ds.modalStackLinkDismissible !== "false",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function generateLayerId() {
|
|
30
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
31
|
+
return crypto.randomUUID();
|
|
32
|
+
}
|
|
33
|
+
return `ms-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
VARIANTS,
|
|
3
|
+
closeAll,
|
|
4
|
+
createStack,
|
|
5
|
+
handlePopstate,
|
|
6
|
+
pop,
|
|
7
|
+
push,
|
|
8
|
+
replaceTop,
|
|
9
|
+
restore,
|
|
10
|
+
snapshot,
|
|
11
|
+
topLayer,
|
|
12
|
+
} from "./state.js";
|
|
13
|
+
|
|
14
|
+
export { Orchestrator } from "./orchestrator.js";
|
|
15
|
+
export { BrowserRuntime, FRAGMENT_HEADER, SNAPSHOT_KEY } from "./runtime.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ModalStackController } from "./controllers/modal_stack_controller.js";
|
|
2
|
+
import { ModalStackLinkController } from "./controllers/modal_stack_link_controller.js";
|
|
3
|
+
|
|
4
|
+
export function install(application) {
|
|
5
|
+
if (!application || typeof application.register !== "function") {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"modal_stack: install(application) requires a Stimulus Application instance",
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
application.register("modal-stack", ModalStackController);
|
|
11
|
+
application.register("modal-stack-link", ModalStackLinkController);
|
|
12
|
+
return application;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { ModalStackController, ModalStackLinkController };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeAll,
|
|
3
|
+
createStack,
|
|
4
|
+
handlePopstate,
|
|
5
|
+
pop,
|
|
6
|
+
push,
|
|
7
|
+
replaceTop,
|
|
8
|
+
restore,
|
|
9
|
+
snapshot,
|
|
10
|
+
} from "./state.js";
|
|
11
|
+
|
|
12
|
+
export class Orchestrator {
|
|
13
|
+
#expectedPopstates = 0;
|
|
14
|
+
|
|
15
|
+
constructor({ runtime, stackId, baseUrl, restoreFrom = null }) {
|
|
16
|
+
if (!runtime) throw new Error("runtime required");
|
|
17
|
+
this.runtime = runtime;
|
|
18
|
+
this.state = createStack({ stackId, baseUrl });
|
|
19
|
+
|
|
20
|
+
if (restoreFrom) {
|
|
21
|
+
const restored = restore(restoreFrom, { stackId });
|
|
22
|
+
if (restored) this.state = restored;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get layers() {
|
|
27
|
+
return this.state.layers;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get depth() {
|
|
31
|
+
return this.state.layers.length;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async push(layer, { html = null, fragment = null } = {}) {
|
|
35
|
+
if (fragment == null && html == null && layer?.url) {
|
|
36
|
+
fragment = await this.#prefetch(layer.url);
|
|
37
|
+
}
|
|
38
|
+
return this.#dispatch(push(this.state, layer), { html, fragment });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pop() {
|
|
42
|
+
return this.#dispatch(pop(this.state));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async replaceTop(patch, { html = null, fragment = null, ...opts } = {}) {
|
|
46
|
+
if (fragment == null && html == null && patch?.url) {
|
|
47
|
+
fragment = await this.#prefetch(patch.url);
|
|
48
|
+
}
|
|
49
|
+
return this.#dispatch(replaceTop(this.state, patch, opts), { html, fragment });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async #prefetch(url) {
|
|
53
|
+
if (typeof this.runtime.fetchFragment !== "function") return null;
|
|
54
|
+
return this.runtime.fetchFragment(url);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
closeAll() {
|
|
58
|
+
return this.#dispatch(closeAll(this.state));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onPopstate({ historyState, locationHref }) {
|
|
62
|
+
if (this.#expectedPopstates > 0) {
|
|
63
|
+
this.#expectedPopstates -= 1;
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
return this.#dispatch(
|
|
67
|
+
handlePopstate(this.state, { historyState, locationHref }),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async #dispatch({ state, commands }, payload = {}) {
|
|
72
|
+
this.state = state;
|
|
73
|
+
for (const cmd of commands) {
|
|
74
|
+
if (cmd.type === "mountLayer" || cmd.type === "morphTopLayer") {
|
|
75
|
+
if (payload.html != null) cmd.html = payload.html;
|
|
76
|
+
if (payload.fragment != null) cmd.fragment = payload.fragment;
|
|
77
|
+
}
|
|
78
|
+
await this.#execute(cmd);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async #execute(cmd) {
|
|
83
|
+
if (cmd.type === "historyBack") {
|
|
84
|
+
this.#expectedPopstates += 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (cmd.type === "persistSnapshot") {
|
|
88
|
+
await this.runtime.persistSnapshot?.(snapshot(this.state));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const handler = this.runtime[cmd.type];
|
|
93
|
+
if (typeof handler !== "function") {
|
|
94
|
+
throw new Error(`runtime missing handler for "${cmd.type}"`);
|
|
95
|
+
}
|
|
96
|
+
await handler.call(this.runtime, cmd);
|
|
97
|
+
}
|
|
98
|
+
}
|