wilday_ui 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/builds/wilday_ui/index.js +237 -0
- data/app/assets/builds/wilday_ui/index.js.map +3 -3
- data/app/assets/stylesheets/wilday_ui/components/button/features/tooltip.css +258 -0
- data/app/assets/stylesheets/wilday_ui/components/button/index.css +1 -0
- data/app/helpers/wilday_ui/application_helper.rb +0 -1
- data/app/helpers/wilday_ui/components/button_helper.rb +135 -2
- data/app/javascript/wilday_ui/controllers/index.js +2 -0
- data/app/javascript/wilday_ui/controllers/tooltip_controller.js +318 -0
- data/lib/wilday_ui/version.rb +1 -1
- metadata +4 -3
- data/app/helpers/wilday_ui/theme_helper.rb +0 -19
@@ -0,0 +1,318 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["trigger"];
|
5
|
+
static values = {
|
6
|
+
content: String,
|
7
|
+
position: { type: String, default: "top" },
|
8
|
+
align: { type: String, default: "center" },
|
9
|
+
trigger: { type: String, default: "hover" },
|
10
|
+
showDelay: { type: Number, default: 0 },
|
11
|
+
hideDelay: { type: Number, default: 0 },
|
12
|
+
offset: { type: Number, default: 8 },
|
13
|
+
theme: { type: String, default: "light" },
|
14
|
+
size: { type: String, default: "md" },
|
15
|
+
arrow: { type: Boolean, default: false },
|
16
|
+
customStyle: String,
|
17
|
+
};
|
18
|
+
|
19
|
+
connect() {
|
20
|
+
this.tooltipElement = null;
|
21
|
+
this.showTimeoutId = null;
|
22
|
+
this.hideTimeoutId = null;
|
23
|
+
this.setupTooltip();
|
24
|
+
|
25
|
+
// Add scroll handler
|
26
|
+
this.scrollHandler = () => {
|
27
|
+
if (this.tooltipElement.style.display !== "none") {
|
28
|
+
// Check if trigger is in viewport
|
29
|
+
const triggerRect = this.triggerTarget.getBoundingClientRect();
|
30
|
+
const isInViewport =
|
31
|
+
triggerRect.top >= 0 &&
|
32
|
+
triggerRect.left >= 0 &&
|
33
|
+
triggerRect.bottom <=
|
34
|
+
(window.innerHeight || document.documentElement.clientHeight) &&
|
35
|
+
triggerRect.right <=
|
36
|
+
(window.innerWidth || document.documentElement.clientWidth);
|
37
|
+
|
38
|
+
if (isInViewport) {
|
39
|
+
// Update position if trigger is visible
|
40
|
+
this.position();
|
41
|
+
} else {
|
42
|
+
// Hide tooltip if trigger is not visible
|
43
|
+
this.hide();
|
44
|
+
}
|
45
|
+
}
|
46
|
+
};
|
47
|
+
window.addEventListener("scroll", this.scrollHandler);
|
48
|
+
}
|
49
|
+
|
50
|
+
disconnect() {
|
51
|
+
if (this.clickOutsideHandler) {
|
52
|
+
document.removeEventListener("click", this.clickOutsideHandler);
|
53
|
+
}
|
54
|
+
window.removeEventListener("scroll", this.scrollHandler);
|
55
|
+
this.removeTooltip();
|
56
|
+
}
|
57
|
+
|
58
|
+
setupTooltip() {
|
59
|
+
this.tooltipElement = this.createTooltipElement();
|
60
|
+
document.body.appendChild(this.tooltipElement);
|
61
|
+
|
62
|
+
if (this.triggerValue === "hover") {
|
63
|
+
this.triggerTarget.addEventListener("mouseenter", () => this.show());
|
64
|
+
this.triggerTarget.addEventListener("mouseleave", () => this.hide());
|
65
|
+
this.triggerTarget.addEventListener("focusin", () => this.show());
|
66
|
+
this.triggerTarget.addEventListener("focusout", () => this.hide());
|
67
|
+
} else if (this.triggerValue === "click") {
|
68
|
+
// Check if dropdown or clipboard is present
|
69
|
+
const hasDropdown =
|
70
|
+
this.element.hasAttribute("data-controller") &&
|
71
|
+
this.element.getAttribute("data-controller").includes("dropdown");
|
72
|
+
const hasClipboard =
|
73
|
+
this.element.hasAttribute("data-controller") &&
|
74
|
+
this.element.getAttribute("data-controller").includes("clipboard");
|
75
|
+
|
76
|
+
if (hasDropdown || hasClipboard) {
|
77
|
+
// Force hover behavior for dropdown/clipboard buttons
|
78
|
+
this.triggerTarget.addEventListener("mouseenter", () => this.show());
|
79
|
+
this.triggerTarget.addEventListener("mouseleave", () => this.hide());
|
80
|
+
} else {
|
81
|
+
// Normal click behavior
|
82
|
+
this.triggerTarget.addEventListener("click", (e) => {
|
83
|
+
e.stopPropagation();
|
84
|
+
this.toggle();
|
85
|
+
});
|
86
|
+
|
87
|
+
this.clickOutsideHandler = (e) => {
|
88
|
+
if (
|
89
|
+
!this.triggerTarget.contains(e.target) &&
|
90
|
+
!this.tooltipElement.contains(e.target)
|
91
|
+
) {
|
92
|
+
this.hide();
|
93
|
+
}
|
94
|
+
};
|
95
|
+
document.addEventListener("click", this.clickOutsideHandler);
|
96
|
+
}
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
createTooltipElement() {
|
101
|
+
const tooltip = document.createElement("div");
|
102
|
+
tooltip.id = this.triggerTarget.getAttribute("aria-describedby");
|
103
|
+
tooltip.className = `w-tooltip w-tooltip-${this.themeValue} w-tooltip-size-${this.sizeValue}`;
|
104
|
+
if (this.arrowValue) tooltip.classList.add("w-tooltip-arrow"); // Add arrow class if enabled
|
105
|
+
tooltip.setAttribute("role", "tooltip");
|
106
|
+
tooltip.setAttribute("data-position", this.positionValue);
|
107
|
+
tooltip.setAttribute("data-align", this.alignValue);
|
108
|
+
tooltip.innerHTML = this.contentValue;
|
109
|
+
|
110
|
+
// Apply custom styles if present
|
111
|
+
if (this.hasCustomStyleValue && this.customStyleValue) {
|
112
|
+
tooltip.style.cssText += this.customStyleValue;
|
113
|
+
}
|
114
|
+
|
115
|
+
tooltip.style.display = "none";
|
116
|
+
return tooltip;
|
117
|
+
}
|
118
|
+
|
119
|
+
show() {
|
120
|
+
clearTimeout(this.hideTimeoutId);
|
121
|
+
this.showTimeoutId = setTimeout(() => {
|
122
|
+
// Reset any existing transforms and make tooltip visible but hidden
|
123
|
+
this.tooltipElement.style.transform = "none";
|
124
|
+
this.tooltipElement.style.visibility = "hidden";
|
125
|
+
this.tooltipElement.style.display = "block";
|
126
|
+
|
127
|
+
// Force a reflow to ensure dimensions are calculated correctly
|
128
|
+
this.tooltipElement.offsetHeight;
|
129
|
+
|
130
|
+
// Position the tooltip
|
131
|
+
this.position();
|
132
|
+
|
133
|
+
// Make visible with transition
|
134
|
+
this.tooltipElement.style.visibility = "visible";
|
135
|
+
requestAnimationFrame(() => {
|
136
|
+
this.tooltipElement.classList.add("w-tooltip-visible");
|
137
|
+
});
|
138
|
+
}, this.showDelayValue);
|
139
|
+
}
|
140
|
+
|
141
|
+
hide() {
|
142
|
+
clearTimeout(this.showTimeoutId);
|
143
|
+
this.hideTimeoutId = setTimeout(() => {
|
144
|
+
this.tooltipElement.classList.remove("w-tooltip-visible");
|
145
|
+
setTimeout(() => {
|
146
|
+
this.tooltipElement.style.display = "none";
|
147
|
+
}, 150); // Match transition duration
|
148
|
+
}, this.hideDelayValue);
|
149
|
+
}
|
150
|
+
|
151
|
+
toggle() {
|
152
|
+
if (this.tooltipElement.style.display === "none") {
|
153
|
+
this.show();
|
154
|
+
} else {
|
155
|
+
this.hide();
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
position() {
|
160
|
+
const triggerRect = this.triggerTarget.getBoundingClientRect();
|
161
|
+
const tooltipRect = this.tooltipElement.getBoundingClientRect();
|
162
|
+
const viewportHeight = window.innerHeight;
|
163
|
+
const viewportWidth = window.innerWidth;
|
164
|
+
const arrowOffset = this.arrowValue ? 2 : 0;
|
165
|
+
|
166
|
+
// Calculate scroll position
|
167
|
+
const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
|
168
|
+
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
169
|
+
|
170
|
+
// Debug initial values
|
171
|
+
console.log("=== Initial Values ===");
|
172
|
+
console.log("Viewport Height:", viewportHeight);
|
173
|
+
console.log("Viewport Width:", viewportWidth);
|
174
|
+
console.log("Trigger Top:", triggerRect.top);
|
175
|
+
console.log("Trigger Bottom:", triggerRect.bottom);
|
176
|
+
console.log("Trigger Left:", triggerRect.left);
|
177
|
+
console.log("Trigger Right:", triggerRect.right);
|
178
|
+
console.log("Tooltip Height:", tooltipRect.height);
|
179
|
+
console.log("Tooltip Width:", tooltipRect.width);
|
180
|
+
|
181
|
+
let position = this.positionValue;
|
182
|
+
let top, left;
|
183
|
+
|
184
|
+
// Calculate available space in viewport
|
185
|
+
const spaceBelow = viewportHeight - triggerRect.bottom;
|
186
|
+
const spaceAbove = triggerRect.top;
|
187
|
+
const spaceRight = viewportWidth - triggerRect.right;
|
188
|
+
const spaceLeft = triggerRect.left;
|
189
|
+
const requiredSpaceVertical =
|
190
|
+
tooltipRect.height + this.offsetValue + arrowOffset;
|
191
|
+
const requiredSpaceHorizontal =
|
192
|
+
tooltipRect.width + this.offsetValue + arrowOffset;
|
193
|
+
|
194
|
+
// Debug space
|
195
|
+
console.log("\n=== Space Calculation ===");
|
196
|
+
console.log("Space Above:", spaceAbove);
|
197
|
+
console.log("Space Below:", spaceBelow);
|
198
|
+
console.log("Space Left:", spaceLeft);
|
199
|
+
console.log("Space Right:", spaceRight);
|
200
|
+
console.log("Required Space Vertical:", requiredSpaceVertical);
|
201
|
+
console.log("Required Space Horizontal:", requiredSpaceHorizontal);
|
202
|
+
|
203
|
+
// Determine position based on available viewport space
|
204
|
+
if (position === "right" && spaceRight >= requiredSpaceHorizontal) {
|
205
|
+
position = "right";
|
206
|
+
console.log("Using right - sufficient space");
|
207
|
+
} else if (position === "left" && spaceLeft >= requiredSpaceHorizontal) {
|
208
|
+
position = "left";
|
209
|
+
console.log("Using left - sufficient space");
|
210
|
+
} else if (position === "top" && spaceAbove >= requiredSpaceVertical) {
|
211
|
+
position = "top";
|
212
|
+
console.log("Using top - sufficient space");
|
213
|
+
} else if (position === "bottom" && spaceBelow >= requiredSpaceVertical) {
|
214
|
+
position = "bottom";
|
215
|
+
console.log("Using bottom - sufficient space");
|
216
|
+
} else if (spaceBelow >= requiredSpaceVertical) {
|
217
|
+
position = "bottom";
|
218
|
+
console.log("Fallback to bottom - sufficient space");
|
219
|
+
} else {
|
220
|
+
position = "top";
|
221
|
+
console.log("Fallback to top - insufficient space below");
|
222
|
+
}
|
223
|
+
|
224
|
+
// Calculate viewport-relative position
|
225
|
+
switch (position) {
|
226
|
+
case "top":
|
227
|
+
// Position above trigger
|
228
|
+
top =
|
229
|
+
triggerRect.top - tooltipRect.height - this.offsetValue - arrowOffset;
|
230
|
+
if (this.alignValue === "end") {
|
231
|
+
left = triggerRect.right - tooltipRect.width;
|
232
|
+
} else if (this.alignValue === "center") {
|
233
|
+
left =
|
234
|
+
triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
|
235
|
+
} else {
|
236
|
+
// start
|
237
|
+
left = triggerRect.left;
|
238
|
+
}
|
239
|
+
break;
|
240
|
+
case "bottom":
|
241
|
+
// Position below trigger
|
242
|
+
top = triggerRect.bottom + this.offsetValue + arrowOffset;
|
243
|
+
if (this.alignValue === "end") {
|
244
|
+
left = triggerRect.right - tooltipRect.width;
|
245
|
+
} else if (this.alignValue === "center") {
|
246
|
+
left =
|
247
|
+
triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
|
248
|
+
} else {
|
249
|
+
// start
|
250
|
+
left = triggerRect.left;
|
251
|
+
}
|
252
|
+
break;
|
253
|
+
case "left":
|
254
|
+
// Position to the left of trigger
|
255
|
+
left =
|
256
|
+
triggerRect.left - tooltipRect.width - this.offsetValue - arrowOffset;
|
257
|
+
if (this.alignValue === "end") {
|
258
|
+
top = triggerRect.bottom - tooltipRect.height;
|
259
|
+
} else if (this.alignValue === "center") {
|
260
|
+
top =
|
261
|
+
triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
|
262
|
+
} else {
|
263
|
+
// start
|
264
|
+
top = triggerRect.top;
|
265
|
+
}
|
266
|
+
break;
|
267
|
+
case "right":
|
268
|
+
// Position to the right of trigger
|
269
|
+
left = triggerRect.right + this.offsetValue + arrowOffset;
|
270
|
+
if (this.alignValue === "end") {
|
271
|
+
top = triggerRect.bottom - tooltipRect.height;
|
272
|
+
} else if (this.alignValue === "center") {
|
273
|
+
top =
|
274
|
+
triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
|
275
|
+
} else {
|
276
|
+
// start
|
277
|
+
top = triggerRect.top;
|
278
|
+
}
|
279
|
+
break;
|
280
|
+
}
|
281
|
+
|
282
|
+
// Ensure tooltip stays within viewport
|
283
|
+
if (top < 0) {
|
284
|
+
top = this.offsetValue; // Keep some padding from top
|
285
|
+
} else if (top + tooltipRect.height > viewportHeight) {
|
286
|
+
top = viewportHeight - tooltipRect.height - this.offsetValue;
|
287
|
+
}
|
288
|
+
|
289
|
+
if (left < 0) {
|
290
|
+
left = this.offsetValue; // Keep some padding from left
|
291
|
+
} else if (left + tooltipRect.width > viewportWidth) {
|
292
|
+
left = viewportWidth - tooltipRect.width - this.offsetValue;
|
293
|
+
}
|
294
|
+
|
295
|
+
// Debug final position
|
296
|
+
console.log("\n=== Final Position ===");
|
297
|
+
console.log("Position:", position);
|
298
|
+
console.log("Top:", top);
|
299
|
+
console.log("Left:", left);
|
300
|
+
console.log("Alignment:", this.alignValue);
|
301
|
+
|
302
|
+
// Add scroll position to final coordinates
|
303
|
+
top += scrollY;
|
304
|
+
left += scrollX;
|
305
|
+
|
306
|
+
// Update tooltip position and data attribute
|
307
|
+
this.tooltipElement.setAttribute("data-position", position);
|
308
|
+
this.tooltipElement.style.top = `${top}px`;
|
309
|
+
this.tooltipElement.style.left = `${left}px`;
|
310
|
+
}
|
311
|
+
|
312
|
+
removeTooltip() {
|
313
|
+
if (this.tooltipElement) {
|
314
|
+
this.tooltipElement.remove();
|
315
|
+
this.tooltipElement = null;
|
316
|
+
}
|
317
|
+
}
|
318
|
+
}
|
data/lib/wilday_ui/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wilday_ui
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- davidwinalda
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-12-
|
11
|
+
date: 2024-12-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -54,6 +54,7 @@ files:
|
|
54
54
|
- app/assets/stylesheets/wilday_ui/components/button/features/old_variants.css
|
55
55
|
- app/assets/stylesheets/wilday_ui/components/button/features/radius.css
|
56
56
|
- app/assets/stylesheets/wilday_ui/components/button/features/sizes.css
|
57
|
+
- app/assets/stylesheets/wilday_ui/components/button/features/tooltip.css
|
57
58
|
- app/assets/stylesheets/wilday_ui/components/button/features/variants.css
|
58
59
|
- app/assets/stylesheets/wilday_ui/components/button/index.css
|
59
60
|
- app/assets/stylesheets/wilday_ui/tokens/colors.css
|
@@ -61,13 +62,13 @@ files:
|
|
61
62
|
- app/helpers/wilday_ui/application_helper.rb
|
62
63
|
- app/helpers/wilday_ui/components/button_helper.rb
|
63
64
|
- app/helpers/wilday_ui/javascript_helper.rb
|
64
|
-
- app/helpers/wilday_ui/theme_helper.rb
|
65
65
|
- app/javascript/wilday_ui/components/button.js
|
66
66
|
- app/javascript/wilday_ui/controllers/button_controller.js
|
67
67
|
- app/javascript/wilday_ui/controllers/clipboard_controller.js
|
68
68
|
- app/javascript/wilday_ui/controllers/confirmation_controller.js
|
69
69
|
- app/javascript/wilday_ui/controllers/dropdown_controller.js
|
70
70
|
- app/javascript/wilday_ui/controllers/index.js
|
71
|
+
- app/javascript/wilday_ui/controllers/tooltip_controller.js
|
71
72
|
- app/javascript/wilday_ui/index.js
|
72
73
|
- app/jobs/wilday_ui/application_job.rb
|
73
74
|
- app/mailers/wilday_ui/application_mailer.rb
|
@@ -1,19 +0,0 @@
|
|
1
|
-
module WildayUi
|
2
|
-
module ThemeHelper
|
3
|
-
def generate_theme_css_variables
|
4
|
-
colors = WildayUi::Config::Theme.configuration.colors
|
5
|
-
css_vars = colors.map do |color_name, shades|
|
6
|
-
shades.map do |shade, value|
|
7
|
-
"--w-color-#{color_name}-#{shade}: #{value};"
|
8
|
-
end
|
9
|
-
end.flatten.join("\n")
|
10
|
-
|
11
|
-
"<style>:root { #{css_vars} }</style>".html_safe
|
12
|
-
end
|
13
|
-
|
14
|
-
# Helper to get specific color value
|
15
|
-
def theme_color(name, shade = "500")
|
16
|
-
WildayUi::Config::Theme.configuration.colors.dig(name.to_s, shade.to_s)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|