smriti 0.5.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +168 -0
- data/Rakefile +15 -0
- data/app/assets/images/smriti/android-chrome-192x192.png +0 -0
- data/app/assets/images/smriti/android-chrome-512x512.png +0 -0
- data/app/assets/images/smriti/apple-touch-icon.png +0 -0
- data/app/assets/images/smriti/favicon-16x16.png +0 -0
- data/app/assets/images/smriti/favicon-32x32.png +0 -0
- data/app/assets/images/smriti/favicon-48x48.png +0 -0
- data/app/assets/images/smriti/favicon.ico +0 -0
- data/app/assets/images/smriti/favicon.svg +18 -0
- data/app/assets/images/smriti/logo.svg +18 -0
- data/app/assets/images/smriti/mask-icon.svg +5 -0
- data/app/assets/stylesheets/smriti/application.css +1040 -0
- data/app/controllers/smriti/admin/application_controller.rb +135 -0
- data/app/controllers/smriti/admin/dashboard_controller.rb +32 -0
- data/app/controllers/smriti/admin/mat_view_definitions_controller.rb +372 -0
- data/app/controllers/smriti/admin/mat_view_runs_controller.rb +185 -0
- data/app/controllers/smriti/admin/preferences_controller.rb +91 -0
- data/app/helpers/smriti/admin/datatable_helper.rb +249 -0
- data/app/helpers/smriti/admin/localized_digit_helper.rb +70 -0
- data/app/helpers/smriti/admin/ui_helper.rb +539 -0
- data/app/javascript/smriti/application.js +8 -0
- data/app/javascript/smriti/controllers/application.js +10 -0
- data/app/javascript/smriti/controllers/body_setup_controller.js +120 -0
- data/app/javascript/smriti/controllers/datatable_controller.js +351 -0
- data/app/javascript/smriti/controllers/details_controller.js +200 -0
- data/app/javascript/smriti/controllers/drawer_controller.js +470 -0
- data/app/javascript/smriti/controllers/flash_controller.js +112 -0
- data/app/javascript/smriti/controllers/index.js +10 -0
- data/app/javascript/smriti/controllers/mv_confirm_controller.js +435 -0
- data/app/javascript/smriti/controllers/tabs_controller.js +184 -0
- data/app/javascript/smriti/controllers/tooltip_controller.js +525 -0
- data/app/javascript/smriti/controllers/turbo_frame_lifecycle_controller.js +342 -0
- data/app/jobs/smriti/application_job.rb +144 -0
- data/app/jobs/smriti/create_view_job.rb +87 -0
- data/app/jobs/smriti/delete_view_job.rb +89 -0
- data/app/jobs/smriti/refresh_view_job.rb +94 -0
- data/app/models/concerns/smriti_i18n.rb +139 -0
- data/app/models/concerns/smriti_paginate.rb +70 -0
- data/app/models/concerns/smriti_query_helper.rb +36 -0
- data/app/models/smriti/application_record.rb +39 -0
- data/app/models/smriti/mat_view_definition.rb +254 -0
- data/app/models/smriti/mat_view_run.rb +275 -0
- data/app/views/layouts/smriti/_footer.html.erb +47 -0
- data/app/views/layouts/smriti/_header.html.erb +25 -0
- data/app/views/layouts/smriti/admin.html.erb +47 -0
- data/app/views/layouts/smriti/turbo_frame.html.erb +3 -0
- data/app/views/smriti/admin/dashboard/index.html.erb +38 -0
- data/app/views/smriti/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
- data/app/views/smriti/admin/mat_view_definitions/_dt-index-empty-row.html.erb +11 -0
- data/app/views/smriti/admin/mat_view_definitions/_dt-index-row.html.erb +27 -0
- data/app/views/smriti/admin/mat_view_definitions/empty.html.erb +1 -0
- data/app/views/smriti/admin/mat_view_definitions/form.html.erb +79 -0
- data/app/views/smriti/admin/mat_view_definitions/index.html.erb +10 -0
- data/app/views/smriti/admin/mat_view_definitions/show.html.erb +40 -0
- data/app/views/smriti/admin/mat_view_runs/_dt-index-empty-row.html.erb +11 -0
- data/app/views/smriti/admin/mat_view_runs/_dt-index-row.html.erb +41 -0
- data/app/views/smriti/admin/mat_view_runs/index.html.erb +1 -0
- data/app/views/smriti/admin/mat_view_runs/show.html.erb +64 -0
- data/app/views/smriti/admin/preferences/show.html.erb +49 -0
- data/app/views/smriti/admin/ui/_card.html.erb +15 -0
- data/app/views/smriti/admin/ui/_datatable.html.erb +34 -0
- data/app/views/smriti/admin/ui/_datatable_filters.html.erb +45 -0
- data/app/views/smriti/admin/ui/_datatable_tbody.html.erb +11 -0
- data/app/views/smriti/admin/ui/_datatable_tfoot.html.erb +70 -0
- data/app/views/smriti/admin/ui/_datatable_thead.html.erb +105 -0
- data/app/views/smriti/admin/ui/_details.html.erb +10 -0
- data/app/views/smriti/admin/ui/_flash.html.erb +6 -0
- data/app/views/smriti/admin/ui/_table.html.erb +8 -0
- data/config/importmap.rb +9 -0
- data/config/locales/ar.yml +223 -0
- data/config/locales/de.yml +230 -0
- data/config/locales/en-AU-ocker.yml +223 -0
- data/config/locales/en-AU.yml +202 -0
- data/config/locales/en-BORK.yml +225 -0
- data/config/locales/en-CA.yml +223 -0
- data/config/locales/en-GB.yml +223 -0
- data/config/locales/en-LOL.yml +219 -0
- data/config/locales/en-SCOT.yml +223 -0
- data/config/locales/en-SHAKESPEARE.yml +225 -0
- data/config/locales/en-US-pirate.yml +222 -0
- data/config/locales/en-US.yml +225 -0
- data/config/locales/en-YODA.yml +221 -0
- data/config/locales/en.yml +223 -0
- data/config/locales/es.yml +226 -0
- data/config/locales/fa.yml +223 -0
- data/config/locales/fr-CA.yml +227 -0
- data/config/locales/fr.yml +227 -0
- data/config/locales/he.yml +218 -0
- data/config/locales/hi.yml +223 -0
- data/config/locales/it.yml +225 -0
- data/config/locales/ja-JP.yml +215 -0
- data/config/locales/pt.yml +225 -0
- data/config/locales/ru.yml +228 -0
- data/config/locales/ur.yml +225 -0
- data/config/locales/zh-CN.yml +214 -0
- data/config/locales/zh-TW.yml +214 -0
- data/config/routes.rb +36 -0
- data/lib/ext/exception.rb +20 -0
- data/lib/generators/smriti/install/install_generator.rb +86 -0
- data/lib/generators/smriti/install/templates/create_mat_view_definitions.rb +29 -0
- data/lib/generators/smriti/install/templates/create_mat_view_runs.rb +32 -0
- data/lib/generators/smriti/install/templates/smriti_initializer.rb +23 -0
- data/lib/smriti/admin/auth_bridge.rb +93 -0
- data/lib/smriti/admin/default_auth.rb +62 -0
- data/lib/smriti/configuration.rb +58 -0
- data/lib/smriti/engine.rb +82 -0
- data/lib/smriti/helpers/ui_test_ids.rb +49 -0
- data/lib/smriti/jobs/adapter.rb +81 -0
- data/lib/smriti/service_response.rb +75 -0
- data/lib/smriti/services/base_service.rb +471 -0
- data/lib/smriti/services/check_matview_exists.rb +76 -0
- data/lib/smriti/services/concurrent_refresh.rb +94 -0
- data/lib/smriti/services/create_view.rb +173 -0
- data/lib/smriti/services/delete_view.rb +111 -0
- data/lib/smriti/services/regular_refresh.rb +90 -0
- data/lib/smriti/services/swap_refresh.rb +181 -0
- data/lib/smriti/version.rb +21 -0
- data/lib/smriti.rb +64 -0
- data/lib/tasks/helpers.rb +185 -0
- data/lib/tasks/smriti_tasks.rake +151 -0
- metadata +206 -0
|
@@ -0,0 +1,525 @@
|
|
|
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
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Stimulus Controller: TooltipController
|
|
10
|
+
* --------------------------------------
|
|
11
|
+
* Displays a lightweight, accessible tooltip near the trigger element with
|
|
12
|
+
* viewport-aware placement, hover/focus/touch interactions, and delayed show/hide.
|
|
13
|
+
*
|
|
14
|
+
* Responsibilities:
|
|
15
|
+
* - Render a single reusable tooltip element (`#mv-tooltip`) in the DOM.
|
|
16
|
+
* - Show/hide on mouse/keyboard/touch with small delays to avoid flicker.
|
|
17
|
+
* - Compute placement (top/right/bottom/left) with clamping to viewport.
|
|
18
|
+
* - Manage ARIA (`aria-describedby`) and avoid the native browser tooltip.
|
|
19
|
+
*
|
|
20
|
+
* Key Components:
|
|
21
|
+
* - Public actions: `show`, `hide`, `toggle`, `forceHide`
|
|
22
|
+
* - Internals (show/hide): `_actuallyShow`, `_actuallyHide`
|
|
23
|
+
* - Listeners: `_bindHandlers`, `_addListeners`, `_removeListeners`
|
|
24
|
+
* - DOM helpers: `_ensureTooltipEl`, `_prepareForMeasure`, `_applyPlacementClass`,
|
|
25
|
+
* `_removeAllPlacementClasses`, `_setTransform`, `_offscreen`,
|
|
26
|
+
* `_showTooltip`, `_hideTooltip`, `_setDescribedBy`, `_clearDescribedBy`
|
|
27
|
+
* - Data/title: `_resolveText`, `_resolvePlacement`, `_saveAndRemoveTitle`, `_restoreTitle`
|
|
28
|
+
* - Timers: `_schedule`, `_clearTimer`, `_clearAllTimers`, `_isVisible`
|
|
29
|
+
* - Geometry: `_computePosition`, `_coordsFor`, `_clampX`, `_clampY`
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* <button
|
|
33
|
+
* data-controller="tooltip"
|
|
34
|
+
* data-tooltip-text-value="Save"
|
|
35
|
+
* data-tooltip-placement="bottom"
|
|
36
|
+
* >
|
|
37
|
+
* Save
|
|
38
|
+
* </button>
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { Controller } from "@hotwired/stimulus";
|
|
42
|
+
|
|
43
|
+
export default class extends Controller {
|
|
44
|
+
/**
|
|
45
|
+
* Values:
|
|
46
|
+
* - text: explicit tooltip text; falls back to aria-label or title.
|
|
47
|
+
* - placement: preferred placement ("top" | "right" | "bottom" | "left").
|
|
48
|
+
* - delay: ms before showing on hover/focus.
|
|
49
|
+
* - hideDelay: ms before hiding after leave/blur.
|
|
50
|
+
* - disabled: disable tooltip behavior when true.
|
|
51
|
+
* - gap: pixel gap between trigger and tooltip.
|
|
52
|
+
* - margin: minimum distance from viewport edges.
|
|
53
|
+
*/
|
|
54
|
+
static values = {
|
|
55
|
+
text: String,
|
|
56
|
+
placement: { type: String, default: "top" },
|
|
57
|
+
delay: { type: Number, default: 120 },
|
|
58
|
+
hideDelay: { type: Number, default: 80 },
|
|
59
|
+
disabled: { type: Boolean, default: false },
|
|
60
|
+
gap: { type: Number, default: 8 },
|
|
61
|
+
margin: { type: Number, default: 8 },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ──────────────────────────────────────────────
|
|
65
|
+
// Lifecycle
|
|
66
|
+
// ──────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Creates/locates the singleton tooltip, binds handlers, and installs listeners.
|
|
70
|
+
* @return {void}
|
|
71
|
+
*/
|
|
72
|
+
connect() {
|
|
73
|
+
this.tooltipEl = this._ensureTooltipEl();
|
|
74
|
+
this.contentEl = this.tooltipEl.querySelector(".mv-tooltip__content");
|
|
75
|
+
this.element.classList.add("mv-cursor-pointer");
|
|
76
|
+
this._bindHandlers();
|
|
77
|
+
this._addListeners();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clears timers, removes listeners, restores native title, and ARIA cleanup.
|
|
82
|
+
* @return {void}
|
|
83
|
+
*/
|
|
84
|
+
disconnect() {
|
|
85
|
+
this._clearAllTimers();
|
|
86
|
+
this._removeListeners();
|
|
87
|
+
this._restoreTitle();
|
|
88
|
+
this._clearDescribedBy();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ──────────────────────────────────────────────
|
|
92
|
+
// Public actions
|
|
93
|
+
// ──────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Schedules tooltip show after `delay`. Cancels pending hide.
|
|
97
|
+
* @return {void}
|
|
98
|
+
*/
|
|
99
|
+
show() {
|
|
100
|
+
if (this.disabledValue) return;
|
|
101
|
+
this._clearTimer("_hideTimer");
|
|
102
|
+
this._schedule("_showTimer", () => this._actuallyShow(), this.delayValue);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Schedules tooltip hide after `hideDelay`. Cancels pending show.
|
|
107
|
+
* @return {void}
|
|
108
|
+
*/
|
|
109
|
+
hide() {
|
|
110
|
+
this._clearTimer("_showTimer");
|
|
111
|
+
this._schedule(
|
|
112
|
+
"_hideTimer",
|
|
113
|
+
() => this._actuallyHide(),
|
|
114
|
+
this.hideDelayValue,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Toggles visibility based on current state.
|
|
120
|
+
* @return {void}
|
|
121
|
+
*/
|
|
122
|
+
toggle() {
|
|
123
|
+
this._isVisible() ? this.hide() : this.show();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Immediately hides the tooltip and moves it offscreen (no delay).
|
|
128
|
+
* @return {void}
|
|
129
|
+
*/
|
|
130
|
+
forceHide() {
|
|
131
|
+
this._clearAllTimers();
|
|
132
|
+
this._actuallyHide(true);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ──────────────────────────────────────────────
|
|
136
|
+
// Internals: show/hide
|
|
137
|
+
// ──────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Resolves text, removes native title, positions, and shows the tooltip.
|
|
141
|
+
* @return {void}
|
|
142
|
+
*/
|
|
143
|
+
_actuallyShow() {
|
|
144
|
+
const text = this._resolveText();
|
|
145
|
+
if (!text) return;
|
|
146
|
+
|
|
147
|
+
// Remove native title to avoid default browser tooltip
|
|
148
|
+
this._saveAndRemoveTitle();
|
|
149
|
+
|
|
150
|
+
// Fill content
|
|
151
|
+
this.contentEl.textContent = text;
|
|
152
|
+
|
|
153
|
+
// Prep, place, show
|
|
154
|
+
this._prepareForMeasure();
|
|
155
|
+
const rect = this.element.getBoundingClientRect();
|
|
156
|
+
const ttRect = this.tooltipEl.getBoundingClientRect();
|
|
157
|
+
const placement = this._resolvePlacement();
|
|
158
|
+
const { x, y } = this._computePosition(
|
|
159
|
+
rect,
|
|
160
|
+
ttRect,
|
|
161
|
+
placement,
|
|
162
|
+
this.gapValue,
|
|
163
|
+
this.marginValue,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
this._applyPlacementClass(placement);
|
|
167
|
+
this._setTransform(x, y);
|
|
168
|
+
this._showTooltip();
|
|
169
|
+
this._setDescribedBy();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Hides tooltip; optionally skip transition and move offscreen immediately.
|
|
174
|
+
* @param {boolean} [immediate=false]
|
|
175
|
+
* @return {void}
|
|
176
|
+
*/
|
|
177
|
+
_actuallyHide(immediate = false) {
|
|
178
|
+
this._hideTooltip();
|
|
179
|
+
|
|
180
|
+
if (immediate) {
|
|
181
|
+
this._offscreen();
|
|
182
|
+
} else {
|
|
183
|
+
// Allow CSS transition to finish before moving offscreen (if not reopened)
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
if (!this._isVisible()) this._offscreen();
|
|
186
|
+
}, 150);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this._restoreTitle();
|
|
190
|
+
this._clearDescribedBy();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ──────────────────────────────────────────────
|
|
194
|
+
// Internals: listeners and handlers
|
|
195
|
+
// ──────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Binds event handlers with correct `this`.
|
|
199
|
+
* @return {void}
|
|
200
|
+
*/
|
|
201
|
+
_bindHandlers() {
|
|
202
|
+
this._onEnter = this.show.bind(this);
|
|
203
|
+
this._onLeave = this.hide.bind(this);
|
|
204
|
+
this._onFocus = this.show.bind(this);
|
|
205
|
+
this._onBlur = this.hide.bind(this);
|
|
206
|
+
this._onKey = (e) => {
|
|
207
|
+
if (e.key === "Escape") this.forceHide();
|
|
208
|
+
};
|
|
209
|
+
this._onTouch = (e) => {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
this.show();
|
|
212
|
+
this._clearTimer("_touchTimer");
|
|
213
|
+
this._touchTimer = setTimeout(() => this.hide(), 1500);
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Installs mouse/keyboard/touch listeners on the trigger element.
|
|
219
|
+
* @return {void}
|
|
220
|
+
*/
|
|
221
|
+
_addListeners() {
|
|
222
|
+
const el = this.element;
|
|
223
|
+
el.addEventListener("mouseenter", this._onEnter);
|
|
224
|
+
el.addEventListener("mouseleave", this._onLeave);
|
|
225
|
+
el.addEventListener("focus", this._onFocus);
|
|
226
|
+
el.addEventListener("blur", this._onBlur);
|
|
227
|
+
el.addEventListener("keydown", this._onKey);
|
|
228
|
+
// preventDefault() requires passive: false
|
|
229
|
+
el.addEventListener("touchstart", this._onTouch, { passive: false });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Removes listeners installed by `_addListeners`.
|
|
234
|
+
* @return {void}
|
|
235
|
+
*/
|
|
236
|
+
_removeListeners() {
|
|
237
|
+
const el = this.element;
|
|
238
|
+
if (!el) return;
|
|
239
|
+
el.removeEventListener("mouseenter", this._onEnter);
|
|
240
|
+
el.removeEventListener("mouseleave", this._onLeave);
|
|
241
|
+
el.removeEventListener("focus", this._onFocus);
|
|
242
|
+
el.removeEventListener("blur", this._onBlur);
|
|
243
|
+
el.removeEventListener("keydown", this._onKey);
|
|
244
|
+
el.removeEventListener("touchstart", this._onTouch, { passive: false });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ──────────────────────────────────────────────
|
|
248
|
+
// Internals: DOM helpers
|
|
249
|
+
// ──────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Ensures a single reusable tooltip element exists in the document.
|
|
253
|
+
* @return {HTMLElement}
|
|
254
|
+
*/
|
|
255
|
+
_ensureTooltipEl() {
|
|
256
|
+
let el = document.getElementById("mv-tooltip");
|
|
257
|
+
if (el) return el;
|
|
258
|
+
|
|
259
|
+
el = document.createElement("div");
|
|
260
|
+
el.id = "mv-tooltip";
|
|
261
|
+
el.setAttribute("role", "tooltip");
|
|
262
|
+
el.className = "mv-tooltip";
|
|
263
|
+
Object.assign(el.style, {
|
|
264
|
+
position: "fixed",
|
|
265
|
+
top: "0",
|
|
266
|
+
left: "0",
|
|
267
|
+
pointerEvents: "none",
|
|
268
|
+
opacity: "0",
|
|
269
|
+
transform: "translate(-9999px,-9999px)",
|
|
270
|
+
});
|
|
271
|
+
el.innerHTML =
|
|
272
|
+
'<div class="mv-tooltip__content"></div><div class="mv-tooltip__arrow" data-arrow></div>';
|
|
273
|
+
document.body.appendChild(el);
|
|
274
|
+
return el;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Prepares tooltip for measurement: hide, offscreen, remove placement classes.
|
|
279
|
+
* @return {void}
|
|
280
|
+
*/
|
|
281
|
+
_prepareForMeasure() {
|
|
282
|
+
this.tooltipEl.style.opacity = "0";
|
|
283
|
+
this._offscreen();
|
|
284
|
+
this._removeAllPlacementClasses();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Applies the placement modifier class (e.g., `mv-tooltip--top`).
|
|
289
|
+
* @param {"top"|"right"|"bottom"|"left"} placement
|
|
290
|
+
* @return {void}
|
|
291
|
+
*/
|
|
292
|
+
_applyPlacementClass(placement) {
|
|
293
|
+
this._removeAllPlacementClasses();
|
|
294
|
+
this.tooltipEl.classList.add(`mv-tooltip--${placement}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Removes all placement modifier classes.
|
|
299
|
+
* @return {void}
|
|
300
|
+
*/
|
|
301
|
+
_removeAllPlacementClasses() {
|
|
302
|
+
this.tooltipEl.classList.remove(
|
|
303
|
+
"mv-tooltip--top",
|
|
304
|
+
"mv-tooltip--right",
|
|
305
|
+
"mv-tooltip--bottom",
|
|
306
|
+
"mv-tooltip--left",
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Applies a CSS transform to move the tooltip to (x, y).
|
|
312
|
+
* @param {number} x
|
|
313
|
+
* @param {number} y
|
|
314
|
+
* @return {void}
|
|
315
|
+
*/
|
|
316
|
+
_setTransform(x, y) {
|
|
317
|
+
this.tooltipEl.style.transform = `translate(${x}px, ${y}px)`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Moves the tooltip offscreen.
|
|
322
|
+
* @return {void}
|
|
323
|
+
*/
|
|
324
|
+
_offscreen() {
|
|
325
|
+
this.tooltipEl.style.transform = "translate(-9999px,-9999px)";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Shows the tooltip via opacity/data attribute (for CSS transitions).
|
|
330
|
+
* @return {void}
|
|
331
|
+
*/
|
|
332
|
+
_showTooltip() {
|
|
333
|
+
this.tooltipEl.style.opacity = "1";
|
|
334
|
+
this.tooltipEl.setAttribute("data-show", "true");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Hides the tooltip via opacity/data attribute (for CSS transitions).
|
|
339
|
+
* @return {void}
|
|
340
|
+
*/
|
|
341
|
+
_hideTooltip() {
|
|
342
|
+
this.tooltipEl.removeAttribute("data-show");
|
|
343
|
+
this.tooltipEl.style.opacity = "0";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Adds `aria-describedby` to the trigger to reference the tooltip.
|
|
348
|
+
* @return {void}
|
|
349
|
+
*/
|
|
350
|
+
_setDescribedBy() {
|
|
351
|
+
this.element.setAttribute("aria-describedby", "mv-tooltip");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Removes `aria-describedby` from the trigger.
|
|
356
|
+
* @return {void}
|
|
357
|
+
*/
|
|
358
|
+
_clearDescribedBy() {
|
|
359
|
+
this.element.removeAttribute("aria-describedby");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ──────────────────────────────────────────────
|
|
363
|
+
// Internals: data/text/title
|
|
364
|
+
// ──────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Returns tooltip text from value, aria-label, or title attribute.
|
|
368
|
+
* @return {string|undefined}
|
|
369
|
+
*/
|
|
370
|
+
_resolveText() {
|
|
371
|
+
return (
|
|
372
|
+
this.textValue ||
|
|
373
|
+
this.element.getAttribute("aria-label") ||
|
|
374
|
+
this.element.getAttribute("title")
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Returns desired placement from data attribute or value.
|
|
380
|
+
* @return {"top"|"right"|"bottom"|"left"}
|
|
381
|
+
*/
|
|
382
|
+
_resolvePlacement() {
|
|
383
|
+
return (
|
|
384
|
+
this.element.getAttribute("data-tooltip-placement") || this.placementValue
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Saves and removes native title to suppress browser tooltip.
|
|
390
|
+
* @return {void}
|
|
391
|
+
*/
|
|
392
|
+
_saveAndRemoveTitle() {
|
|
393
|
+
if (this.element.hasAttribute("title")) {
|
|
394
|
+
this._savedTitle = this.element.getAttribute("title");
|
|
395
|
+
this.element.removeAttribute("title");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Restores the saved native title after hiding.
|
|
401
|
+
* @return {void}
|
|
402
|
+
*/
|
|
403
|
+
_restoreTitle() {
|
|
404
|
+
if (this._savedTitle) {
|
|
405
|
+
this.element.setAttribute("title", this._savedTitle);
|
|
406
|
+
this._savedTitle = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ──────────────────────────────────────────────
|
|
411
|
+
// Internals: timers & state helpers
|
|
412
|
+
// ──────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Schedules a `setTimeout` under a named slot, clearing any existing one.
|
|
416
|
+
* @param {string} name
|
|
417
|
+
* @param {Function} fn
|
|
418
|
+
* @param {number} ms
|
|
419
|
+
* @return {void}
|
|
420
|
+
*/
|
|
421
|
+
_schedule(name, fn, ms) {
|
|
422
|
+
this._clearTimer(name);
|
|
423
|
+
this[name] = setTimeout(fn, ms);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Clears a named timeout if present.
|
|
428
|
+
* @param {string} name
|
|
429
|
+
* @return {void}
|
|
430
|
+
*/
|
|
431
|
+
_clearTimer(name) {
|
|
432
|
+
if (this[name]) {
|
|
433
|
+
clearTimeout(this[name]);
|
|
434
|
+
this[name] = null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Clears all internal timers.
|
|
440
|
+
* @return {void}
|
|
441
|
+
*/
|
|
442
|
+
_clearAllTimers() {
|
|
443
|
+
this._clearTimer("_showTimer");
|
|
444
|
+
this._clearTimer("_hideTimer");
|
|
445
|
+
this._clearTimer("_touchTimer");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Returns true when tooltip is currently visible.
|
|
450
|
+
* @return {boolean}
|
|
451
|
+
*/
|
|
452
|
+
_isVisible() {
|
|
453
|
+
return this.tooltipEl?.getAttribute("data-show") === "true";
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ──────────────────────────────────────────────
|
|
457
|
+
// Geometry
|
|
458
|
+
// ──────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Computes final (x,y) with clamping for viewport edges.
|
|
462
|
+
* @param {DOMRect} targetRect
|
|
463
|
+
* @param {DOMRect} ttRect
|
|
464
|
+
* @param {"top"|"right"|"bottom"|"left"} placement
|
|
465
|
+
* @param {number} gap
|
|
466
|
+
* @param {number} margin
|
|
467
|
+
* @return {{x:number, y:number}}
|
|
468
|
+
*/
|
|
469
|
+
_computePosition(targetRect, ttRect, placement, gap, margin) {
|
|
470
|
+
const coords = this._coordsFor(placement, targetRect, ttRect, gap);
|
|
471
|
+
const x = this._clampX(coords.x, ttRect.width, margin);
|
|
472
|
+
const y = this._clampY(coords.y, ttRect.height, margin);
|
|
473
|
+
return { x: Math.round(x), y: Math.round(y) };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Calculates raw (x,y) for a given placement before clamping.
|
|
478
|
+
* @param {"top"|"right"|"bottom"|"left"} placement
|
|
479
|
+
* @param {DOMRect} r
|
|
480
|
+
* @param {DOMRect} tt
|
|
481
|
+
* @param {number} gap
|
|
482
|
+
* @return {{x:number, y:number}}
|
|
483
|
+
*/
|
|
484
|
+
_coordsFor(placement, r, tt, gap) {
|
|
485
|
+
switch (placement) {
|
|
486
|
+
case "top":
|
|
487
|
+
return {
|
|
488
|
+
x: r.left + (r.width - tt.width) / 2,
|
|
489
|
+
y: r.top - tt.height - gap,
|
|
490
|
+
};
|
|
491
|
+
case "bottom":
|
|
492
|
+
return { x: r.left + (r.width - tt.width) / 2, y: r.bottom + gap };
|
|
493
|
+
case "left":
|
|
494
|
+
return {
|
|
495
|
+
x: r.left - tt.width - gap,
|
|
496
|
+
y: r.top + (r.height - tt.height) / 2,
|
|
497
|
+
};
|
|
498
|
+
case "right":
|
|
499
|
+
default:
|
|
500
|
+
return { x: r.right + gap, y: r.top + (r.height - tt.height) / 2 };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Clamps X to viewport with margin.
|
|
506
|
+
* @param {number} x
|
|
507
|
+
* @param {number} width
|
|
508
|
+
* @param {number} margin
|
|
509
|
+
* @return {number}
|
|
510
|
+
*/
|
|
511
|
+
_clampX(x, width, margin) {
|
|
512
|
+
return Math.max(margin, Math.min(x, window.innerWidth - width - margin));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Clamps Y to viewport with margin.
|
|
517
|
+
* @param {number} y
|
|
518
|
+
* @param {number} height
|
|
519
|
+
* @param {number} margin
|
|
520
|
+
* @return {number}
|
|
521
|
+
*/
|
|
522
|
+
_clampY(y, height, margin) {
|
|
523
|
+
return Math.max(margin, Math.min(y, window.innerHeight - height - margin));
|
|
524
|
+
}
|
|
525
|
+
}
|