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.
@@ -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
+ }
@@ -1,3 +1,3 @@
1
1
  module WildayUi
2
- VERSION = "0.7.0"
2
+ VERSION = "0.8.0"
3
3
  end
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.7.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-21 00:00:00.000000000 Z
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