mat_views 0.2.0 → 0.3.0
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/README.md +4 -4
- data/app/assets/images/mat_views/android-chrome-192x192.png +0 -0
- data/app/assets/images/mat_views/android-chrome-512x512.png +0 -0
- data/app/assets/images/mat_views/apple-touch-icon.png +0 -0
- data/app/assets/images/mat_views/favicon-16x16.png +0 -0
- data/app/assets/images/mat_views/favicon-32x32.png +0 -0
- data/app/assets/images/mat_views/favicon-48x48.png +0 -0
- data/app/assets/images/mat_views/favicon.ico +0 -0
- data/app/assets/images/mat_views/favicon.svg +18 -0
- data/app/assets/images/mat_views/logo.svg +18 -0
- data/app/assets/images/mat_views/mask-icon.svg +5 -0
- data/app/assets/stylesheets/mat_views/application.css +323 -12
- data/app/controllers/mat_views/admin/application_controller.rb +135 -0
- data/app/controllers/mat_views/admin/dashboard_controller.rb +32 -0
- data/app/controllers/mat_views/admin/mat_view_definitions_controller.rb +248 -0
- data/app/controllers/mat_views/admin/preferences_controller.rb +91 -0
- data/app/controllers/mat_views/admin/runs_controller.rb +74 -0
- data/app/helpers/mat_views/admin/ui_helper.rb +385 -0
- data/app/javascript/mat_views/application.js +8 -0
- data/app/javascript/mat_views/controllers/application.js +10 -0
- data/app/javascript/mat_views/controllers/details_controller.js +122 -0
- data/app/javascript/mat_views/controllers/drawer_controller.js +252 -0
- data/app/javascript/mat_views/controllers/filter_controller.js +90 -0
- data/app/javascript/mat_views/controllers/flash_controller.js +13 -0
- data/app/javascript/mat_views/controllers/index.js +10 -0
- data/app/javascript/mat_views/controllers/mv_confirm_controller.js +281 -0
- data/app/javascript/mat_views/controllers/submitter_controller.js +15 -0
- data/app/javascript/mat_views/controllers/tabs_controller.js +67 -0
- data/app/javascript/mat_views/controllers/timezone_controller.js +16 -0
- data/app/javascript/mat_views/controllers/tooltip_controller.js +328 -0
- data/app/javascript/mat_views/controllers/turbo_frame_lifecycle_controller.js +49 -0
- data/app/jobs/mat_views/application_job.rb +2 -2
- data/app/jobs/mat_views/create_view_job.rb +9 -8
- data/app/jobs/mat_views/delete_view_job.rb +8 -8
- data/app/jobs/mat_views/refresh_view_job.rb +8 -9
- data/app/models/concerns/mat_views_i18n.rb +139 -0
- data/app/models/mat_views/application_record.rb +1 -0
- data/app/models/mat_views/mat_view_definition.rb +12 -7
- data/app/models/mat_views/mat_view_run.rb +11 -13
- data/app/views/layouts/mat_views/_footer.html.erb +41 -0
- data/app/views/layouts/mat_views/_header.html.erb +25 -0
- data/app/views/layouts/mat_views/admin.html.erb +47 -0
- data/app/views/layouts/mat_views/turbo_frame.html.erb +3 -0
- data/app/views/mat_views/admin/dashboard/index.html.erb +33 -0
- data/app/views/mat_views/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
- data/app/views/mat_views/admin/mat_view_definitions/_table.html.erb +48 -0
- data/app/views/mat_views/admin/mat_view_definitions/empty.html.erb +1 -0
- data/app/views/mat_views/admin/mat_view_definitions/form.html.erb +79 -0
- data/app/views/mat_views/admin/mat_view_definitions/index.html.erb +10 -0
- data/app/views/mat_views/admin/mat_view_definitions/show.html.erb +40 -0
- data/app/views/mat_views/admin/preferences/show.html.erb +50 -0
- data/app/views/mat_views/admin/runs/_table.html.erb +61 -0
- data/app/views/mat_views/admin/runs/index.html.erb +38 -0
- data/app/views/mat_views/admin/runs/show.html.erb +64 -0
- data/app/views/mat_views/admin/ui/_card.html.erb +15 -0
- data/app/views/mat_views/admin/ui/_details.html.erb +10 -0
- data/app/views/mat_views/admin/ui/_flash.html.erb +6 -0
- data/app/views/mat_views/admin/ui/_table.html.erb +8 -0
- data/config/importmap.rb +9 -0
- data/config/locales/en-AU-ocker.yml +187 -0
- data/config/locales/en-AU.yml +187 -0
- data/config/locales/en-BB.yml +187 -0
- data/config/locales/en-BD.yml +187 -0
- data/config/locales/en-BE.yml +187 -0
- data/config/locales/en-BORK.yml +187 -0
- data/config/locales/en-BS.yml +187 -0
- data/config/locales/en-BZ.yml +187 -0
- data/config/locales/en-CA.yml +187 -0
- data/config/locales/en-CM.yml +187 -0
- data/config/locales/en-CY.yml +187 -0
- data/config/locales/en-EG.yml +187 -0
- data/config/locales/en-FJ.yml +187 -0
- data/config/locales/en-GB.yml +187 -0
- data/config/locales/en-GH.yml +187 -0
- data/config/locales/en-GI.yml +187 -0
- data/config/locales/en-GM.yml +187 -0
- data/config/locales/en-GY.yml +187 -0
- data/config/locales/en-HK.yml +187 -0
- data/config/locales/en-IE.yml +187 -0
- data/config/locales/en-IN.yml +187 -0
- data/config/locales/en-JM.yml +187 -0
- data/config/locales/en-KE.yml +187 -0
- data/config/locales/en-LK.yml +187 -0
- data/config/locales/en-LOL.yml +187 -0
- data/config/locales/en-LR.yml +187 -0
- data/config/locales/en-MS.yml +187 -0
- data/config/locales/en-MT.yml +187 -0
- data/config/locales/en-MW.yml +187 -0
- data/config/locales/en-MY.yml +187 -0
- data/config/locales/en-NG.yml +187 -0
- data/config/locales/en-NP.yml +187 -0
- data/config/locales/en-NZ.yml +187 -0
- data/config/locales/en-PG.yml +187 -0
- data/config/locales/en-PH.yml +187 -0
- data/config/locales/en-PK.yml +187 -0
- data/config/locales/en-RW.yml +187 -0
- data/config/locales/en-SCOT.yml +187 -0
- data/config/locales/en-SG.yml +187 -0
- data/config/locales/en-SHAKESPEARE.yml +187 -0
- data/config/locales/en-SL.yml +187 -0
- data/config/locales/en-SS.yml +187 -0
- data/config/locales/en-TH.yml +187 -0
- data/config/locales/en-TT.yml +187 -0
- data/config/locales/en-TZ.yml +187 -0
- data/config/locales/en-UG.yml +187 -0
- data/config/locales/en-US-pirate.yml +187 -0
- data/config/locales/en-US.yml +187 -0
- data/config/locales/en-YODA.yml +187 -0
- data/config/locales/en-ZA.yml +187 -0
- data/config/locales/en-ZW.yml +187 -0
- data/config/locales/en.yml +187 -0
- data/config/routes.rb +27 -3
- data/lib/generators/mat_views/install/templates/create_mat_view_definitions.rb +7 -7
- data/lib/generators/mat_views/install/templates/create_mat_view_runs.rb +5 -5
- data/lib/mat_views/admin/auth_bridge.rb +93 -0
- data/lib/mat_views/admin/default_auth.rb +61 -0
- data/lib/mat_views/configuration.rb +9 -0
- data/lib/mat_views/engine.rb +50 -2
- data/lib/mat_views/helpers/ui_test_ids.rb +43 -0
- data/lib/mat_views/services/base_service.rb +46 -38
- data/lib/mat_views/services/check_matview_exists.rb +76 -0
- data/lib/mat_views/services/concurrent_refresh.rb +9 -6
- data/lib/mat_views/services/create_view.rb +15 -15
- data/lib/mat_views/services/delete_view.rb +8 -11
- data/lib/mat_views/services/regular_refresh.rb +6 -5
- data/lib/mat_views/services/swap_refresh.rb +11 -9
- data/lib/mat_views/version.rb +1 -1
- data/lib/mat_views.rb +10 -4
- data/lib/tasks/helpers.rb +13 -13
- data/lib/tasks/mat_views_tasks.rake +15 -15
- metadata +130 -5
@@ -0,0 +1,252 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { Controller } from "@hotwired/stimulus";
|
8
|
+
|
9
|
+
export default class extends Controller {
|
10
|
+
static targets = ["overlay", "panel", "frame"];
|
11
|
+
static currentTargetStack = [];
|
12
|
+
|
13
|
+
connect() {
|
14
|
+
this.currentTargetStack = [];
|
15
|
+
this._escHandler = (e) => {
|
16
|
+
if (e.key === "Escape") this.close();
|
17
|
+
};
|
18
|
+
if (this.hasFrameTarget) {
|
19
|
+
this._submitEndHandler = (e) => this.onSubmitEnd(e);
|
20
|
+
this._frameLoadHandler = (e) => this.onFrameLoad(e);
|
21
|
+
this.frameTarget.addEventListener(
|
22
|
+
"turbo:submit-end",
|
23
|
+
this._submitEndHandler,
|
24
|
+
);
|
25
|
+
this.frameTarget.addEventListener(
|
26
|
+
"turbo:frame-load",
|
27
|
+
this._frameLoadHandler,
|
28
|
+
);
|
29
|
+
}
|
30
|
+
const qs = new URLSearchParams(window.location.search);
|
31
|
+
const open = qs.get("open");
|
32
|
+
if (open) {
|
33
|
+
this.openDrawerByName(open);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
disconnect() {
|
38
|
+
if (this.hasFrameTarget) {
|
39
|
+
this.frameTarget.removeEventListener(
|
40
|
+
"turbo:submit-end",
|
41
|
+
this._submitEndHandler,
|
42
|
+
);
|
43
|
+
this.frameTarget.removeEventListener(
|
44
|
+
"turbo:frame-load",
|
45
|
+
this._frameLoadHandler,
|
46
|
+
);
|
47
|
+
}
|
48
|
+
document.removeEventListener("keydown", this._escHandler);
|
49
|
+
}
|
50
|
+
|
51
|
+
loadAndShow() {
|
52
|
+
let currentTarget = this.currentTargetStack.at(-1);
|
53
|
+
if (!currentTarget) return;
|
54
|
+
|
55
|
+
const title =
|
56
|
+
currentTarget?.dataset.drawerTitle ||
|
57
|
+
currentTarget?.getAttribute("title") ||
|
58
|
+
currentTarget?.getAttribute("aria-label");
|
59
|
+
const url =
|
60
|
+
currentTarget?.dataset.drawerUrl || currentTarget?.getAttribute("href");
|
61
|
+
if (!url || url === "#" || url === "javascript:void(0);") return;
|
62
|
+
|
63
|
+
const header = this._drawerHeader();
|
64
|
+
header.setAttribute("aria-label", title);
|
65
|
+
const titleEle = header.querySelector("h2");
|
66
|
+
titleEle.textContent = title;
|
67
|
+
this.show();
|
68
|
+
this.load(url);
|
69
|
+
}
|
70
|
+
|
71
|
+
open(event) {
|
72
|
+
if (!this.currentTargetStack) {
|
73
|
+
this.currentTargetStack = [];
|
74
|
+
}
|
75
|
+
if (event) {
|
76
|
+
event.preventDefault();
|
77
|
+
event.stopPropagation();
|
78
|
+
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
|
79
|
+
}
|
80
|
+
this.currentTargetStack.push(event?.currentTarget);
|
81
|
+
this.loadAndShow();
|
82
|
+
}
|
83
|
+
|
84
|
+
show() {
|
85
|
+
const root = this._root();
|
86
|
+
document.addEventListener("keydown", this._escHandler);
|
87
|
+
root?.classList.add("is-open");
|
88
|
+
root?.setAttribute("aria-hidden", "false");
|
89
|
+
}
|
90
|
+
|
91
|
+
close() {
|
92
|
+
this.currentTargetStack.pop();
|
93
|
+
if (this.currentTargetStack.length > 0) {
|
94
|
+
this.loadAndShow();
|
95
|
+
return;
|
96
|
+
}
|
97
|
+
|
98
|
+
const root = this._root();
|
99
|
+
document.activeElement?.blur();
|
100
|
+
document.body.focus();
|
101
|
+
root?.classList.remove("is-open");
|
102
|
+
document.removeEventListener("keydown", this._escHandler);
|
103
|
+
root?.setAttribute("aria-hidden", "true");
|
104
|
+
|
105
|
+
const url = new URL(window.location);
|
106
|
+
url.searchParams.delete("open");
|
107
|
+
history.replaceState({}, "", url);
|
108
|
+
|
109
|
+
this.frameTarget.removeAttribute("src");
|
110
|
+
}
|
111
|
+
|
112
|
+
openDrawerByName(name) {
|
113
|
+
const type = name.split("_")[0];
|
114
|
+
const action = name.split("_")[1];
|
115
|
+
const id = name.split("_")[2];
|
116
|
+
switch (type) {
|
117
|
+
case "definitions":
|
118
|
+
this.openDrawerByDefinition(action, id);
|
119
|
+
break;
|
120
|
+
case "runs":
|
121
|
+
this.openDrawerByRun(action, id);
|
122
|
+
break;
|
123
|
+
case "preferences":
|
124
|
+
this.openDrawerByPreferences(action);
|
125
|
+
break;
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
openDrawerByRun(action, id) {
|
130
|
+
if (action == "view") {
|
131
|
+
this.show();
|
132
|
+
this.load(window.MatViewsRoutes.runsPath + `/${id}?frame_id=mv-drawer`);
|
133
|
+
}
|
134
|
+
}
|
135
|
+
|
136
|
+
openDrawerByPreferences(action) {
|
137
|
+
if (action == "edit") {
|
138
|
+
this.show();
|
139
|
+
this.load(window.MatViewsRoutes.preferencesPath + `?frame_id=mv-drawer`);
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
openDrawerByDefinition(action, id) {
|
144
|
+
switch (action) {
|
145
|
+
case "new":
|
146
|
+
this.show();
|
147
|
+
this.load(
|
148
|
+
window.MatViewsRoutes.definitionsPath + "/new?frame_id=mv-drawer",
|
149
|
+
);
|
150
|
+
break;
|
151
|
+
case "view":
|
152
|
+
this.show();
|
153
|
+
this.load(
|
154
|
+
window.MatViewsRoutes.definitionsPath + `/${id}?frame_id=mv-drawer`,
|
155
|
+
);
|
156
|
+
break;
|
157
|
+
case "edit":
|
158
|
+
this.show();
|
159
|
+
this.load(
|
160
|
+
window.MatViewsRoutes.definitionsPath +
|
161
|
+
`/${id}/edit?frame_id=mv-drawer`,
|
162
|
+
);
|
163
|
+
break;
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
refresh() {
|
168
|
+
if (this.frameTarget?.src) {
|
169
|
+
this.load(this.frameTarget.src);
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
load(url) {
|
174
|
+
const frame = this.frameTarget;
|
175
|
+
if (!frame) return;
|
176
|
+
|
177
|
+
const u = new URL(url, window.location.href);
|
178
|
+
frame.src = u.toString();
|
179
|
+
}
|
180
|
+
|
181
|
+
// ── Events ───────────────────────────────────────────────────────
|
182
|
+
|
183
|
+
onFrameLoad(event) {
|
184
|
+
// get title from hidden input if present
|
185
|
+
const frame = event.target;
|
186
|
+
const titleInput = frame?.querySelector("#mv-drawer-title-text")?.value;
|
187
|
+
if (titleInput) {
|
188
|
+
const header = this._drawerHeader();
|
189
|
+
header.setAttribute("aria-label", titleInput);
|
190
|
+
const titleEle = header.querySelector("h2");
|
191
|
+
titleEle.textContent = titleInput;
|
192
|
+
}
|
193
|
+
|
194
|
+
const urlIdentifier = frame?.querySelector(
|
195
|
+
"#mv-drawer-open-url-identifier",
|
196
|
+
)?.value;
|
197
|
+
if (urlIdentifier) {
|
198
|
+
const url = new URL(window.location);
|
199
|
+
url.searchParams.set("open", urlIdentifier);
|
200
|
+
history.replaceState({}, "", url);
|
201
|
+
}
|
202
|
+
}
|
203
|
+
|
204
|
+
onSubmitEnd(event) {
|
205
|
+
if (event.detail.fetchResponse.statusCode === 299) {
|
206
|
+
const url = new URL(window.location);
|
207
|
+
url.searchParams.delete("open");
|
208
|
+
history.replaceState({}, "", url);
|
209
|
+
window.location.reload();
|
210
|
+
return;
|
211
|
+
} else if (event.detail.fetchResponse.statusCode === 298) {
|
212
|
+
this.close();
|
213
|
+
}
|
214
|
+
|
215
|
+
this.refreshActiveFrame();
|
216
|
+
}
|
217
|
+
|
218
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
219
|
+
|
220
|
+
refreshActiveFrame() {
|
221
|
+
const activeTabAnchor = document.querySelector(".mv-tab.mv-tab--on");
|
222
|
+
const dataName = activeTabAnchor?.dataset.name;
|
223
|
+
const activePanel = document.querySelector(
|
224
|
+
`[data-tabs-target="panel"][data-name="${dataName}"]`,
|
225
|
+
);
|
226
|
+
const activeTurboFrame = activePanel?.querySelector("turbo-frame");
|
227
|
+
this.refreshFrameById(activeTurboFrame.id);
|
228
|
+
}
|
229
|
+
|
230
|
+
refreshFrameById(id) {
|
231
|
+
const frame = document.getElementById(id);
|
232
|
+
if (!frame) return;
|
233
|
+
let url = frame.getAttribute("src") || frame.dataset.src;
|
234
|
+
if (!url) return;
|
235
|
+
try {
|
236
|
+
const u = new URL(url, window.location.href);
|
237
|
+
u.searchParams.set("_", Date.now().toString());
|
238
|
+
frame.src = u.toString();
|
239
|
+
} catch {
|
240
|
+
const sep = url.includes("?") ? "&" : "?";
|
241
|
+
frame.src = url + sep + "_=" + Date.now();
|
242
|
+
}
|
243
|
+
}
|
244
|
+
|
245
|
+
_root() {
|
246
|
+
return document.querySelector(".mv-drawer-root");
|
247
|
+
}
|
248
|
+
|
249
|
+
_drawerHeader() {
|
250
|
+
return this._root()?.querySelector(".mv-drawer-head");
|
251
|
+
}
|
252
|
+
}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { Controller } from "@hotwired/stimulus";
|
8
|
+
|
9
|
+
export default class extends Controller {
|
10
|
+
connect() {
|
11
|
+
const queryParams = this.element.dataset.queryParams;
|
12
|
+
this.queryParamsArray = queryParams ? queryParams.split(",") : [];
|
13
|
+
|
14
|
+
if (this.queryParamsArray.length === 0) {
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
|
18
|
+
const qs = new URLSearchParams(window.location.search);
|
19
|
+
|
20
|
+
// return if qs params are equal to form values, this.queryParamsArray
|
21
|
+
const allMatch = this.queryParamsArray.every((param) => {
|
22
|
+
const formElement = this.element.querySelector(`[name="${param}"]`);
|
23
|
+
if (!formElement) {
|
24
|
+
return true; // skip if form element not found
|
25
|
+
}
|
26
|
+
const formValue = formElement.value || "";
|
27
|
+
const qsValue = qs.get(param) || "";
|
28
|
+
return formValue === qsValue;
|
29
|
+
});
|
30
|
+
|
31
|
+
if (allMatch) {
|
32
|
+
return;
|
33
|
+
}
|
34
|
+
|
35
|
+
// set form values from qs params, this.queryParamsArray
|
36
|
+
this.queryParamsArray.forEach((param) => {
|
37
|
+
const formElement = this.element.querySelector(`[name="${param}"]`);
|
38
|
+
if (!formElement) {
|
39
|
+
return; // skip if form element not found
|
40
|
+
}
|
41
|
+
const qsValue = qs.get(param);
|
42
|
+
if (qsValue !== null) {
|
43
|
+
formElement.value = qsValue;
|
44
|
+
} else {
|
45
|
+
formElement.value = ""; // reset to empty if param not in URL
|
46
|
+
}
|
47
|
+
});
|
48
|
+
|
49
|
+
this.element.requestSubmit();
|
50
|
+
}
|
51
|
+
|
52
|
+
reset() {
|
53
|
+
const selects = this.element.querySelectorAll("select");
|
54
|
+
selects.forEach((select) => {
|
55
|
+
if (select.options.length > 0) {
|
56
|
+
select.selectedIndex = 0;
|
57
|
+
}
|
58
|
+
});
|
59
|
+
|
60
|
+
// remove this.queryParamsArray from URL
|
61
|
+
const url = new URL(window.location);
|
62
|
+
this.queryParamsArray.forEach((param) => {
|
63
|
+
url.searchParams.delete(param);
|
64
|
+
});
|
65
|
+
window.history.replaceState({}, "", url);
|
66
|
+
|
67
|
+
this.element.requestSubmit();
|
68
|
+
}
|
69
|
+
|
70
|
+
autoSubmit(event) {
|
71
|
+
if (!event.target) {
|
72
|
+
return;
|
73
|
+
}
|
74
|
+
if (!event.target.name) {
|
75
|
+
return;
|
76
|
+
}
|
77
|
+
|
78
|
+
const argName = event.target.name;
|
79
|
+
const argValue = event.target.value;
|
80
|
+
|
81
|
+
const url = new URL(window.location);
|
82
|
+
if (event.target.value == "") {
|
83
|
+
url.searchParams.delete(argName);
|
84
|
+
} else {
|
85
|
+
url.searchParams.set(argName, argValue);
|
86
|
+
}
|
87
|
+
window.history.replaceState({}, "", url);
|
88
|
+
this.element.requestSubmit();
|
89
|
+
}
|
90
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { Controller } from "@hotwired/stimulus";
|
8
|
+
|
9
|
+
export default class extends Controller {
|
10
|
+
connect() {
|
11
|
+
setTimeout(() => this.element.remove(), 10000);
|
12
|
+
}
|
13
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { application } from "mat_views/controllers/application";
|
8
|
+
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
|
9
|
+
|
10
|
+
eagerLoadControllersFrom("mat_views/controllers", application);
|
@@ -0,0 +1,281 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { Controller } from "@hotwired/stimulus";
|
8
|
+
export default class extends Controller {
|
9
|
+
static values = {
|
10
|
+
enabled: { type: Boolean, default: true },
|
11
|
+
gap: { type: Number, default: 8 },
|
12
|
+
margin: { type: Number, default: 8 },
|
13
|
+
};
|
14
|
+
|
15
|
+
connect() {
|
16
|
+
if (!this.enabledValue) return;
|
17
|
+
this.installConfirmHooks();
|
18
|
+
}
|
19
|
+
|
20
|
+
disconnect() {
|
21
|
+
this.uninstallConfirmHooks();
|
22
|
+
this.cleanup(false);
|
23
|
+
}
|
24
|
+
|
25
|
+
// ──────────────────────────────────────────────
|
26
|
+
// Turbo hook wiring
|
27
|
+
// ──────────────────────────────────────────────
|
28
|
+
installConfirmHooks() {
|
29
|
+
if (!this.hasTurbo()) return;
|
30
|
+
this.prevFormsConfirm = window.Turbo.config.forms?.confirm;
|
31
|
+
this.prevLinksConfirm = window.Turbo.config.links?.confirm;
|
32
|
+
|
33
|
+
const handler = this.confirm.bind(this);
|
34
|
+
if (window.Turbo.config.forms) window.Turbo.config.forms.confirm = handler;
|
35
|
+
if (window.Turbo.config.links) window.Turbo.config.links.confirm = handler;
|
36
|
+
}
|
37
|
+
|
38
|
+
uninstallConfirmHooks() {
|
39
|
+
if (!this.hasTurbo()) return;
|
40
|
+
if (window.Turbo.config.forms)
|
41
|
+
window.Turbo.config.forms.confirm = this.prevFormsConfirm;
|
42
|
+
if (window.Turbo.config.links)
|
43
|
+
window.Turbo.config.links.confirm = this.prevLinksConfirm;
|
44
|
+
}
|
45
|
+
|
46
|
+
hasTurbo() {
|
47
|
+
return !!window.Turbo?.config;
|
48
|
+
}
|
49
|
+
|
50
|
+
// ──────────────────────────────────────────────
|
51
|
+
// Confirm entry point
|
52
|
+
// ──────────────────────────────────────────────
|
53
|
+
confirm(message, element) {
|
54
|
+
// Fallback for Turbo < 8 or if anything is missing
|
55
|
+
if (!this.hasTurbo()) return Promise.resolve(window.confirm(message));
|
56
|
+
|
57
|
+
return new Promise((resolve) => {
|
58
|
+
this._resolver = resolve;
|
59
|
+
this.open(message, element);
|
60
|
+
});
|
61
|
+
}
|
62
|
+
|
63
|
+
// ──────────────────────────────────────────────
|
64
|
+
// Open/close lifecycle
|
65
|
+
// ──────────────────────────────────────────────
|
66
|
+
open(message, element) {
|
67
|
+
this.pop = this.ensurePopover();
|
68
|
+
this.triggerEl = element;
|
69
|
+
|
70
|
+
const { content } = this.getParts(this.pop);
|
71
|
+
content.textContent = message;
|
72
|
+
|
73
|
+
this.position(this.pop, element);
|
74
|
+
this.show(this.pop);
|
75
|
+
this.bindGlobalListeners();
|
76
|
+
}
|
77
|
+
|
78
|
+
cleanup(result) {
|
79
|
+
if (!this.pop) return;
|
80
|
+
this.hide(this.pop);
|
81
|
+
this.unbindGlobalListeners();
|
82
|
+
const resolve = this._resolver;
|
83
|
+
this._resolver = null;
|
84
|
+
if (resolve) resolve(!!result);
|
85
|
+
}
|
86
|
+
|
87
|
+
// ──────────────────────────────────────────────
|
88
|
+
// DOM helpers
|
89
|
+
// ──────────────────────────────────────────────
|
90
|
+
ensurePopover() {
|
91
|
+
let el = document.getElementById("mv-confirm");
|
92
|
+
if (el) return el;
|
93
|
+
el = this.createPopover();
|
94
|
+
document.body.appendChild(el);
|
95
|
+
return el;
|
96
|
+
}
|
97
|
+
|
98
|
+
createPopover() {
|
99
|
+
const el = document.createElement("div");
|
100
|
+
el.id = "mv-confirm";
|
101
|
+
el.className = "mv-confirm";
|
102
|
+
Object.assign(el.style, {
|
103
|
+
position: "fixed",
|
104
|
+
top: "0",
|
105
|
+
left: "0",
|
106
|
+
transform: "translate(-9999px,-9999px)",
|
107
|
+
});
|
108
|
+
el.innerHTML = `
|
109
|
+
<div class="mv-confirm__content"></div>
|
110
|
+
<div class="mv-confirm__buttons">
|
111
|
+
<button type="button" class="mv-btn mv-btn--sm mv-btn--negative mv-confirm__yes">Yes</button>
|
112
|
+
<button type="button" class="mv-btn mv-btn--sm mv-btn--secondary mv-confirm__no">No</button>
|
113
|
+
</div>
|
114
|
+
<div class="mv-confirm__arrow"></div>`;
|
115
|
+
return el;
|
116
|
+
}
|
117
|
+
|
118
|
+
getParts(pop) {
|
119
|
+
return {
|
120
|
+
content: pop.querySelector(".mv-confirm__content"),
|
121
|
+
yes: pop.querySelector(".mv-confirm__yes"),
|
122
|
+
no: pop.querySelector(".mv-confirm__no"),
|
123
|
+
};
|
124
|
+
}
|
125
|
+
|
126
|
+
show(pop) {
|
127
|
+
pop.setAttribute("data-show", "true");
|
128
|
+
}
|
129
|
+
|
130
|
+
hide(pop) {
|
131
|
+
pop.removeAttribute("data-show");
|
132
|
+
pop.style.transform = "translate(-9999px,-9999px)";
|
133
|
+
}
|
134
|
+
|
135
|
+
// ──────────────────────────────────────────────
|
136
|
+
// Listeners
|
137
|
+
// ──────────────────────────────────────────────
|
138
|
+
bindGlobalListeners() {
|
139
|
+
const { yes, no } = this.getParts(this.pop);
|
140
|
+
|
141
|
+
this.onYes = () => this.cleanup(true);
|
142
|
+
this.onNo = () => this.cleanup(false);
|
143
|
+
this.onEsc = (e) => (e.key === "Escape" ? this.cleanup(false) : null);
|
144
|
+
this.onOutside = (e) => {
|
145
|
+
if (!this.pop.contains(e.target) && e.target !== this.triggerEl)
|
146
|
+
this.cleanup(false);
|
147
|
+
};
|
148
|
+
this.onScrollOrResize = () => this.cleanup(false);
|
149
|
+
|
150
|
+
yes.addEventListener("click", this.onYes);
|
151
|
+
no.addEventListener("click", this.onNo);
|
152
|
+
document.addEventListener("keydown", this.onEsc);
|
153
|
+
document.addEventListener("click", this.onOutside, true);
|
154
|
+
window.addEventListener("scroll", this.onScrollOrResize, true);
|
155
|
+
window.addEventListener("resize", this.onScrollOrResize, true);
|
156
|
+
}
|
157
|
+
|
158
|
+
unbindGlobalListeners() {
|
159
|
+
if (!this.pop) return;
|
160
|
+
const { yes, no } = this.getParts(this.pop);
|
161
|
+
|
162
|
+
yes.removeEventListener("click", this.onYes);
|
163
|
+
no.removeEventListener("click", this.onNo);
|
164
|
+
document.removeEventListener("keydown", this.onEsc);
|
165
|
+
document.removeEventListener("click", this.onOutside, true);
|
166
|
+
window.removeEventListener("scroll", this.onScrollOrResize, true);
|
167
|
+
window.removeEventListener("resize", this.onScrollOrResize, true);
|
168
|
+
|
169
|
+
this.onYes =
|
170
|
+
this.onNo =
|
171
|
+
this.onEsc =
|
172
|
+
this.onOutside =
|
173
|
+
this.onScrollOrResize =
|
174
|
+
null;
|
175
|
+
}
|
176
|
+
|
177
|
+
// ──────────────────────────────────────────────
|
178
|
+
// Positioning
|
179
|
+
// ──────────────────────────────────────────────
|
180
|
+
position(pop, element) {
|
181
|
+
// Prep for measurement
|
182
|
+
pop.style.opacity = "1";
|
183
|
+
pop.style.transform = "translate(-9999px,-9999px)";
|
184
|
+
|
185
|
+
const rect = element.getBoundingClientRect();
|
186
|
+
const pr = pop.getBoundingClientRect();
|
187
|
+
|
188
|
+
const { x, y, placement } = this.computePlacement(rect, pr);
|
189
|
+
this.setPlacementClass(pop, placement);
|
190
|
+
pop.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
|
191
|
+
}
|
192
|
+
|
193
|
+
computePlacement(rect, pr) {
|
194
|
+
const gap = this.gapValue;
|
195
|
+
const margin = this.marginValue;
|
196
|
+
const placements = ["bottom", "top", "right", "left"];
|
197
|
+
|
198
|
+
for (const p of placements) {
|
199
|
+
const { x, y } = this.coordsFor(p, rect, pr, gap);
|
200
|
+
if (this.fits(p, x, y, pr, margin)) {
|
201
|
+
return {
|
202
|
+
x: this.clampX(x, pr, margin),
|
203
|
+
y: this.clampY(y, pr, margin),
|
204
|
+
placement: p,
|
205
|
+
};
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
// Fallback: bottom centered, clamped
|
210
|
+
const fx = rect.left + (rect.width - pr.width) / 2;
|
211
|
+
const fy = rect.bottom + gap;
|
212
|
+
return {
|
213
|
+
x: this.clampX(fx, pr, margin),
|
214
|
+
y: this.clampY(fy, pr, margin),
|
215
|
+
placement: "bottom",
|
216
|
+
};
|
217
|
+
}
|
218
|
+
|
219
|
+
coordsFor(p, rect, pr, gap) {
|
220
|
+
switch (p) {
|
221
|
+
case "bottom":
|
222
|
+
return {
|
223
|
+
x: rect.left + (rect.width - pr.width) / 2,
|
224
|
+
y: rect.bottom + gap,
|
225
|
+
};
|
226
|
+
case "top":
|
227
|
+
return {
|
228
|
+
x: rect.left + (rect.width - pr.width) / 2,
|
229
|
+
y: rect.top - pr.height - gap,
|
230
|
+
};
|
231
|
+
case "right":
|
232
|
+
return {
|
233
|
+
x: rect.right + gap,
|
234
|
+
y: rect.top + (rect.height - pr.height) / 2,
|
235
|
+
};
|
236
|
+
case "left":
|
237
|
+
return {
|
238
|
+
x: rect.left - pr.width - gap,
|
239
|
+
y: rect.top + (rect.height - pr.height) / 2,
|
240
|
+
};
|
241
|
+
default:
|
242
|
+
return { x: 0, y: 0 };
|
243
|
+
}
|
244
|
+
}
|
245
|
+
|
246
|
+
fits(p, x, y, pr, margin) {
|
247
|
+
switch (p) {
|
248
|
+
case "bottom":
|
249
|
+
return y + pr.height + margin <= window.innerHeight;
|
250
|
+
case "top":
|
251
|
+
return y >= margin;
|
252
|
+
case "right":
|
253
|
+
return x + pr.width + margin <= window.innerWidth;
|
254
|
+
case "left":
|
255
|
+
return x >= margin;
|
256
|
+
default:
|
257
|
+
return false;
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
clampX(x, pr, margin) {
|
262
|
+
return Math.max(margin, Math.min(x, window.innerWidth - pr.width - margin));
|
263
|
+
}
|
264
|
+
|
265
|
+
clampY(y, pr, margin) {
|
266
|
+
return Math.max(
|
267
|
+
margin,
|
268
|
+
Math.min(y, window.innerHeight - pr.height - margin),
|
269
|
+
);
|
270
|
+
}
|
271
|
+
|
272
|
+
setPlacementClass(pop, placement) {
|
273
|
+
pop.classList.remove(
|
274
|
+
"mv-confirm--top",
|
275
|
+
"mv-confirm--right",
|
276
|
+
"mv-confirm--bottom",
|
277
|
+
"mv-confirm--left",
|
278
|
+
);
|
279
|
+
pop.classList.add(`mv-confirm--${placement}`);
|
280
|
+
}
|
281
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { Controller } from "@hotwired/stimulus";
|
8
|
+
|
9
|
+
export default class extends Controller {
|
10
|
+
submit() {
|
11
|
+
this.element.disabled = true;
|
12
|
+
this.element.classList.add("opacity-60");
|
13
|
+
if (this.element.form) this.element.form.submit();
|
14
|
+
}
|
15
|
+
}
|