wilday_ui 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,6 +11,24 @@ module WildayUi
11
11
  wrapper_required: false,
12
12
  stimulus_controller: "button",
13
13
  default_stimulus_action: "click->button#toggleLoading"
14
+ },
15
+ copy_to_clipboard: {
16
+ wrapper_required: true,
17
+ stimulus_controller: "clipboard button",
18
+ default_stimulus_action: "click->clipboard#copy click->button#toggleLoading"
19
+ },
20
+ confirm: {
21
+ wrapper_required: true,
22
+ stimulus_controller: "confirmation",
23
+ default_stimulus_action: "click->confirmation#showDialog"
24
+ },
25
+ tooltip: {
26
+ wrapper_required: true,
27
+ stimulus_controller: "tooltip",
28
+ default_stimulus_action: {
29
+ hover: "mouseenter->tooltip#show mouseleave->tooltip#hide focusin->tooltip#show focusout->tooltip#hide",
30
+ click: "click->tooltip#toggle"
31
+ }
14
32
  }
15
33
  # Add more features here as needed
16
34
  # tooltip: {
@@ -42,6 +60,9 @@ module WildayUi
42
60
  dropdown_items: nil,
43
61
  dropdown_icon: nil,
44
62
  theme: nil,
63
+ copy_to_clipboard: nil,
64
+ confirm: nil,
65
+ tooltip: nil,
45
66
  **options
46
67
  )
47
68
  content_for(:head) { stylesheet_link_tag "wilday_ui/components/button/index", media: "all" }
@@ -68,7 +89,7 @@ module WildayUi
68
89
  gradient_class = get_gradient_class(gradient)
69
90
 
70
91
  # Setup features that require Stimulus controllers
71
- active_features = determine_active_features(loading, dropdown, loading_text, use_default_controller)
92
+ active_features = determine_active_features(loading, dropdown, loading_text, copy_to_clipboard, confirm, tooltip, use_default_controller)
72
93
 
73
94
  setup_features(active_features, options, use_default_controller, loading_text)
74
95
 
@@ -84,6 +105,33 @@ module WildayUi
84
105
  )
85
106
  end
86
107
 
108
+ if copy_to_clipboard
109
+ setup_clipboard_options(
110
+ options,
111
+ additional_classes,
112
+ copy_to_clipboard,
113
+ wrapper_data
114
+ )
115
+ end
116
+
117
+ if confirm
118
+ setup_confirmation_options(
119
+ options,
120
+ additional_classes,
121
+ confirm,
122
+ wrapper_data
123
+ )
124
+ end
125
+
126
+ if tooltip
127
+ setup_tooltip_options(
128
+ options,
129
+ additional_classes,
130
+ tooltip,
131
+ wrapper_data
132
+ )
133
+ end
134
+
87
135
  # Setup wrapper options if any feature requires it
88
136
  wrapper_options = setup_wrapper_options(
89
137
  active_features,
@@ -267,10 +315,13 @@ module WildayUi
267
315
  styles.map { |k, v| "#{k}: #{v}" }.join(";")
268
316
  end
269
317
 
270
- def determine_active_features(loading, dropdown, loading_text = nil, use_default_controller = true)
318
+ def determine_active_features(loading, dropdown, loading_text = nil, copy_to_clipboard = nil, confirm = nil, tooltip = nil, use_default_controller = true)
271
319
  features = {}
272
320
  features[:loading] = true if (loading || loading_text.present?) && use_default_controller
273
321
  features[:dropdown] = true if dropdown && use_default_controller
322
+ features[:copy_to_clipboard] = true if copy_to_clipboard.present? && use_default_controller
323
+ features[:confirm] = true if confirm.present? && use_default_controller
324
+ features[:tooltip] = true if tooltip.present? && use_default_controller
274
325
  features
275
326
  end
276
327
 
@@ -389,6 +440,206 @@ module WildayUi
389
440
  "#{base}#{index}"
390
441
  end
391
442
 
443
+ def setup_clipboard_options(options, additional_classes, copy_to_clipboard, wrapper_data)
444
+ return unless copy_to_clipboard.present?
445
+
446
+ clipboard_config = normalize_clipboard_options(copy_to_clipboard)
447
+
448
+ wrapper_data.merge!(
449
+ controller: "clipboard button",
450
+ clipboard_text_value: clipboard_config[:text],
451
+ clipboard_feedback_text_value: clipboard_config[:feedback_text],
452
+ clipboard_feedback_position_value: clipboard_config[:position],
453
+ clipboard_feedback_duration_value: clipboard_config[:duration]
454
+ )
455
+
456
+ options[:data][:clipboard_target] = "button"
457
+ options[:data][:button_target] = "button"
458
+ end
459
+
460
+ def normalize_clipboard_options(options)
461
+ if options.is_a?(Hash)
462
+ {
463
+ text: options[:text],
464
+ feedback_text: options[:feedback_text] || "Copied!",
465
+ position: options[:position] || "top",
466
+ duration: options[:duration] || 2000
467
+ }
468
+ else
469
+ {
470
+ text: options.to_s,
471
+ feedback_text: "Copied!",
472
+ position: "top",
473
+ duration: 2000
474
+ }
475
+ end
476
+ end
477
+
478
+ def setup_confirmation_options(options, additional_classes, confirm, wrapper_data)
479
+ return unless confirm.present?
480
+
481
+ confirm_config = normalize_confirmation_options(confirm)
482
+
483
+ # Use the same theme processing as regular buttons
484
+ confirm_theme_styles = process_theme(:solid, { name: confirm_config[:variant] })
485
+ cancel_theme_styles = process_theme(:subtle, { name: :secondary })
486
+
487
+ wrapper_data.merge!(
488
+ controller: "confirmation",
489
+ confirmation_title_value: confirm_config[:title],
490
+ confirmation_message_value: confirm_config[:message],
491
+ confirmation_icon_color_value: confirm_config[:variant],
492
+ confirmation_confirm_text_value: confirm_config[:confirm_text],
493
+ confirmation_cancel_text_value: confirm_config[:cancel_text],
494
+ confirmation_confirm_styles_value: confirm_theme_styles,
495
+ confirmation_cancel_styles_value: cancel_theme_styles
496
+ )
497
+
498
+ # Only add loading state if enabled
499
+ if confirm_config[:loading]
500
+ wrapper_data.merge!(
501
+ confirmation_loading_value: "true",
502
+ confirmation_loading_text_value: confirm_config[:loading_text]
503
+ )
504
+ end
505
+ end
506
+
507
+ def normalize_confirmation_options(options)
508
+ if options.is_a?(String)
509
+ {
510
+ title: "Confirm Action",
511
+ message: options,
512
+ variant: :info,
513
+ confirm_text: "Confirm",
514
+ cancel_text: "Cancel"
515
+ }
516
+ else
517
+ {
518
+ title: options[:title] || "Confirm Action",
519
+ message: options[:message],
520
+ variant: options[:variant] || :info,
521
+ confirm_text: options[:confirm_text] || "Confirm",
522
+ cancel_text: options[:cancel_text] || "Cancel",
523
+ loading: options[:loading] || false,
524
+ loading_text: options[:loading_text] || "Processing..."
525
+ }
526
+ end
527
+ end
528
+
529
+ def setup_tooltip_options(options, additional_classes, tooltip, wrapper_data)
530
+ tooltip_config = normalize_tooltip_options(tooltip)
531
+
532
+ # Check if dropdown is present
533
+ has_dropdown = wrapper_data[:controller]&.include?("dropdown")
534
+ has_clipboard = wrapper_data[:controller]&.include?("clipboard")
535
+
536
+ # Get the appropriate action based on trigger type
537
+ # Force hover behavior if dropdown is present
538
+ trigger_type = (has_dropdown || has_clipboard) ? :hover : tooltip_config[:trigger].to_sym
539
+ tooltip_action = BUTTON_FEATURES[:tooltip][:default_stimulus_action][trigger_type]
540
+
541
+ # Merge controllers
542
+ existing_controller = wrapper_data[:controller]
543
+ wrapper_data[:controller] = [
544
+ existing_controller,
545
+ "tooltip"
546
+ ].compact.join(" ")
547
+
548
+ # Merge actions
549
+ existing_action = wrapper_data[:action]
550
+ if has_dropdown
551
+ # Keep the dropdown toggle action and add tooltip hover actions
552
+ wrapper_data[:action] = [
553
+ "click->dropdown#toggle", # Ensure dropdown action comes first
554
+ tooltip_action
555
+ ].compact.join(" ")
556
+ elsif has_clipboard
557
+ # Keep the clipboard copy action and add tooltip hover actions
558
+ wrapper_data[:action] = [
559
+ "click->clipboard#copy click->button#toggleLoading",
560
+ tooltip_action
561
+ ].compact.join(" ")
562
+ else
563
+ wrapper_data[:action] = [
564
+ existing_action,
565
+ tooltip_action
566
+ ].compact.join(" ")
567
+ end
568
+
569
+ # Handle theme data
570
+ theme = tooltip_config[:theme]
571
+ theme_name = theme.is_a?(Hash) ? theme[:name] : theme
572
+
573
+ wrapper_data.merge!(
574
+ tooltip_content_value: tooltip_config[:content],
575
+ tooltip_position_value: tooltip_config[:position],
576
+ tooltip_align_value: tooltip_config[:align],
577
+ tooltip_trigger_value: trigger_type,
578
+ tooltip_show_delay_value: tooltip_config[:delay][:show],
579
+ tooltip_hide_delay_value: tooltip_config[:delay][:hide],
580
+ tooltip_offset_value: tooltip_config[:offset],
581
+ tooltip_theme_value: theme_name,
582
+ tooltip_size_value: tooltip_config[:size],
583
+ tooltip_arrow_value: tooltip_config[:arrow]
584
+ )
585
+
586
+ # Add custom theme styles if present
587
+ if theme.is_a?(Hash) && theme[:custom]
588
+ custom_styles = []
589
+ custom_styles << "--tooltip-text-color: #{theme[:custom][:color]}" if theme[:custom][:color]
590
+ custom_styles << "--tooltip-bg-color: #{theme[:custom][:background]}" if theme[:custom][:background]
591
+ wrapper_data[:tooltip_custom_style_value] = custom_styles.join(";")
592
+ end
593
+
594
+ options[:data][:tooltip_target] = "trigger"
595
+ options[:aria] ||= {}
596
+ options[:aria][:describedby] = "tooltip-#{SecureRandom.hex(4)}"
597
+ end
598
+
599
+ def normalize_tooltip_options(options)
600
+ if options.is_a?(String)
601
+ {
602
+ content: options,
603
+ position: "top",
604
+ align: "center",
605
+ trigger: "hover",
606
+ delay: { show: 0, hide: 0 },
607
+ offset: 8,
608
+ theme: "light",
609
+ size: "md",
610
+ arrow: false
611
+ }
612
+ else
613
+ theme = options[:theme]
614
+ theme_data = if theme.is_a?(Hash) && theme[:custom]
615
+ {
616
+ name: "custom",
617
+ custom: {
618
+ color: theme.dig(:custom, :color),
619
+ background: theme.dig(:custom, :background)
620
+ }
621
+ }
622
+ else
623
+ { name: theme || "light" }
624
+ end
625
+
626
+ {
627
+ content: options[:content],
628
+ position: options[:position] || "top",
629
+ align: options[:align] || "center",
630
+ trigger: options[:trigger] || "hover",
631
+ delay: {
632
+ show: options.dig(:delay, :show) || 0,
633
+ hide: options.dig(:delay, :hide) || 0
634
+ },
635
+ offset: options[:offset] || 8,
636
+ theme: theme_data,
637
+ size: options[:size] || "md",
638
+ arrow: options[:arrow] || false
639
+ }
640
+ end
641
+ end
642
+
392
643
  def render_button(content, variant_class, size_class, radius_class, gradient_class, icon, icon_position, icon_only,
393
644
  loading, loading_text, additional_classes, disabled, options, href, underline,
394
645
  dropdown, dropdown_items, dropdown_icon, wrapper_options)
@@ -0,0 +1,76 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["button", "feedback"];
5
+ static values = {
6
+ text: String,
7
+ feedbackText: { type: String, default: "Copied!" },
8
+ feedbackPosition: { type: String, default: "top" },
9
+ feedbackDuration: { type: Number, default: 2000 },
10
+ };
11
+
12
+ connect() {
13
+ // Optional: Initialize any necessary setup
14
+ }
15
+
16
+ async copy(event) {
17
+ event.preventDefault();
18
+
19
+ try {
20
+ await navigator.clipboard.writeText(this.textValue);
21
+ this.showFeedback();
22
+ } catch (err) {
23
+ console.error("Failed to copy text:", err);
24
+ // Fallback for older browsers
25
+ this.fallbackCopy();
26
+ }
27
+ }
28
+
29
+ fallbackCopy() {
30
+ const textArea = document.createElement("textarea");
31
+ textArea.value = this.textValue;
32
+ textArea.style.position = "fixed";
33
+ textArea.style.left = "-9999px";
34
+ document.body.appendChild(textArea);
35
+ textArea.select();
36
+
37
+ try {
38
+ document.execCommand("copy");
39
+ this.showFeedback();
40
+ } catch (err) {
41
+ console.error("Fallback: Oops, unable to copy", err);
42
+ }
43
+
44
+ document.body.removeChild(textArea);
45
+ }
46
+
47
+ showFeedback() {
48
+ const feedback = this.hasFeedbackTarget
49
+ ? this.feedbackTarget
50
+ : this.createFeedbackElement();
51
+ feedback.textContent = this.feedbackTextValue;
52
+
53
+ // Remove any existing position classes
54
+ feedback.className = "w-button-feedback";
55
+
56
+ // Add position-specific classes
57
+ const positionClasses = this.feedbackPositionValue.split("-");
58
+ positionClasses.forEach((pos) => {
59
+ feedback.classList.add(`w-button-feedback-${pos}`);
60
+ });
61
+
62
+ feedback.classList.add("w-button-feedback-show");
63
+
64
+ setTimeout(() => {
65
+ feedback.classList.remove("w-button-feedback-show");
66
+ }, this.feedbackDurationValue);
67
+ }
68
+
69
+ createFeedbackElement() {
70
+ const feedback = document.createElement("div");
71
+ feedback.classList.add("w-button-feedback");
72
+ feedback.setAttribute("data-clipboard-target", "feedback");
73
+ this.element.appendChild(feedback);
74
+ return feedback;
75
+ }
76
+ }
@@ -0,0 +1,216 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["dialog", "confirmButton", "cancelButton"];
5
+ static values = {
6
+ title: String,
7
+ message: String,
8
+ iconColor: String,
9
+ confirmText: String,
10
+ cancelText: String,
11
+ confirmStyles: String,
12
+ cancelStyles: String,
13
+ };
14
+
15
+ // Store the original event to be used later
16
+ originalEvent = null;
17
+
18
+ connect() {
19
+ // Create and append dialog if it doesn't exist
20
+ if (!this.hasDialogTarget) {
21
+ this.element.insertAdjacentHTML("beforeend", this.dialogHTML);
22
+ }
23
+ }
24
+
25
+ disconnect() {
26
+ this.originalEvent = null;
27
+ this.isConfirmed = false;
28
+ }
29
+
30
+ showDialog(event) {
31
+ // Don't show dialog if already confirmed
32
+ if (this.isConfirmed) {
33
+ this.isConfirmed = false;
34
+ return;
35
+ }
36
+
37
+ // console.log("Show dialog triggered", event);
38
+ event.preventDefault();
39
+ // Store the original event and element
40
+ this.originalEvent = {
41
+ type: event.type,
42
+ element: event.currentTarget,
43
+ ctrlKey: event.ctrlKey,
44
+ metaKey: event.metaKey,
45
+ };
46
+ // console.log("Original event stored:", this.originalEvent);
47
+ this.dialogTarget.showModal();
48
+ this.focusConfirmButton();
49
+ }
50
+
51
+ confirm(event) {
52
+ // console.log("Confirm clicked");
53
+ event.preventDefault();
54
+ this.dialogTarget.close();
55
+
56
+ if (this.originalEvent?.element) {
57
+ const element = this.originalEvent.element;
58
+ // console.log("Processing element:", element);
59
+
60
+ // Let Turbo handle its own elements
61
+ if (
62
+ this.hasTurbo &&
63
+ !element.hasAttribute("data-turbo") &&
64
+ !element.hasAttribute("data-turbo-method")
65
+ ) {
66
+ this.resumeOriginalEvent();
67
+ return;
68
+ }
69
+
70
+ // Dispatch standard DOM custom event
71
+ const confirmEvent = new CustomEvent("confirm", {
72
+ bubbles: true,
73
+ cancelable: true,
74
+ detail: {
75
+ element: element,
76
+ originalEvent: this.originalEvent,
77
+ },
78
+ });
79
+
80
+ const wasHandled = !element.dispatchEvent(confirmEvent);
81
+ if (wasHandled) return;
82
+
83
+ // If not handled by custom event, resume original event
84
+ this.resumeOriginalEvent();
85
+ }
86
+ }
87
+
88
+ resumeOriginalEvent() {
89
+ if (!this.originalEvent) return;
90
+
91
+ const element = this.originalEvent.element;
92
+ // console.log("Resuming original event for:", element);
93
+
94
+ // Set flag before triggering the event
95
+ this.isConfirmed = true;
96
+
97
+ // Handle form submissions
98
+ if (element.closest("form")) {
99
+ const form = element.closest("form");
100
+ // console.log("Submitting form:", form);
101
+ const submitEvent = new Event("submit", {
102
+ bubbles: true,
103
+ cancelable: true,
104
+ });
105
+ form.dispatchEvent(submitEvent); // This will trigger the event listener
106
+ this.originalEvent = null;
107
+ return;
108
+ }
109
+
110
+ // Handle links
111
+ if (element.tagName === "A" || element.hasAttribute("href")) {
112
+ // console.log("Processing link click");
113
+ const click = new MouseEvent("click", {
114
+ bubbles: true,
115
+ cancelable: true,
116
+ ctrlKey: this.originalEvent.ctrlKey,
117
+ metaKey: this.originalEvent.metaKey,
118
+ });
119
+ element.dispatchEvent(click);
120
+ this.originalEvent = null;
121
+ return;
122
+ }
123
+
124
+ // Handle regular button click
125
+ if (
126
+ !element.closest("form") &&
127
+ !(element.tagName === "A" || element.hasAttribute("href"))
128
+ ) {
129
+ // console.log("Processing button click");
130
+ element.click();
131
+ this.originalEvent = null;
132
+ return;
133
+ }
134
+ }
135
+
136
+ cancel(event) {
137
+ event.preventDefault();
138
+ this.closeDialog();
139
+ }
140
+
141
+ closeDialog() {
142
+ this.dialogTarget.close();
143
+ this.originalEvent = null;
144
+ }
145
+
146
+ handleKeydown(event) {
147
+ if (event.key === "Escape") {
148
+ this.cancel(event);
149
+ }
150
+ }
151
+
152
+ handleClickOutside(event) {
153
+ if (event.target === this.dialogTarget) {
154
+ this.cancel(event);
155
+ }
156
+ }
157
+
158
+ focusConfirmButton() {
159
+ this.confirmButtonTarget.focus();
160
+ }
161
+
162
+ get hasTurbo() {
163
+ return typeof Turbo !== "undefined";
164
+ }
165
+
166
+ get dialogHTML() {
167
+ return `
168
+ <dialog class="w-button-confirmation-dialog"
169
+ data-confirmation-target="dialog"
170
+ data-action="click->confirmation#handleClickOutside keydown->confirmation#handleKeydown">
171
+ <div class="w-button-confirmation-dialog-content">
172
+ <div class="w-button-confirmation-dialog-icon ${this.iconColorValue}">
173
+ ${this.iconHTML}
174
+ </div>
175
+
176
+ <h3 class="w-button-confirmation-dialog-title">
177
+ ${this.titleValue}
178
+ </h3>
179
+
180
+ <div class="w-button-confirmation-dialog-message">
181
+ ${this.messageValue}
182
+ </div>
183
+
184
+ <div class="w-button-confirmation-dialog-actions">
185
+ <button data-confirmation-target="cancelButton"
186
+ data-action="click->confirmation#cancel"
187
+ class="w-button w-button-subtle w-button-medium w-button-rounded"
188
+ style="${this.cancelStylesValue}">
189
+ ${this.cancelTextValue}
190
+ </button>
191
+
192
+ <button data-confirmation-target="confirmButton"
193
+ data-action="click->confirmation#confirm"
194
+ class="w-button w-button-solid w-button-medium w-button-rounded"
195
+ style="${this.confirmStylesValue}">
196
+ ${this.confirmTextValue}
197
+ </button>
198
+ </div>
199
+ </div>
200
+ </dialog>
201
+ `;
202
+ }
203
+
204
+ get iconHTML() {
205
+ const icons = {
206
+ info: '<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
207
+ success:
208
+ '<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
209
+ warning:
210
+ '<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>',
211
+ danger:
212
+ '<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
213
+ };
214
+ return icons[this.iconColorValue] || icons.info;
215
+ }
216
+ }
@@ -1,7 +1,9 @@
1
1
  import { Application } from "@hotwired/stimulus";
2
2
  import ButtonController from "./button_controller";
3
3
  import DropdownController from "./dropdown_controller";
4
-
4
+ import ClipboardController from "./clipboard_controller";
5
+ import ConfirmationController from "./confirmation_controller";
6
+ import TooltipController from "./tooltip_controller";
5
7
  // Initialize Stimulus
6
8
  const application = Application.start();
7
9
  window.Stimulus = application;
@@ -9,6 +11,9 @@ window.Stimulus = application;
9
11
  // Register the button controller
10
12
  application.register("button", ButtonController);
11
13
  application.register("dropdown", DropdownController);
14
+ application.register("clipboard", ClipboardController);
15
+ application.register("confirmation", ConfirmationController);
16
+ application.register("tooltip", TooltipController);
12
17
  // Debug check to ensure Stimulus is loaded
13
18
  // if (window.Stimulus) {
14
19
  // console.log("✅ Stimulus is loaded and initialized.");