opal-vite 0.1.0 → 0.2.1

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.
@@ -0,0 +1,813 @@
1
+ # backtick_javascript: true
2
+
3
+ module OpalVite
4
+ module Concerns
5
+ # StimulusHelpers - DSL macros for reducing JavaScript backticks in Stimulus controllers
6
+ #
7
+ # This module provides Ruby-friendly methods for common Stimulus patterns,
8
+ # reducing the need for raw JavaScript backticks.
9
+ #
10
+ # Usage:
11
+ # class MyController < StimulusController
12
+ # include StimulusHelpers
13
+ #
14
+ # def connect
15
+ # if has_target?(:input)
16
+ # value = target_value(:input)
17
+ # target_set_html(:output, "Value: #{value}")
18
+ # end
19
+ # end
20
+ # end
21
+ module StimulusHelpers
22
+ # ===== Target Access Methods =====
23
+
24
+ # Check if a Stimulus target exists
25
+ # @param name [Symbol, String] Target name (e.g., :input, :output)
26
+ # @return [Boolean] true if target exists
27
+ def has_target?(name)
28
+ method_name = "has#{camelize(name)}Target"
29
+ `this[#{method_name}]`
30
+ end
31
+
32
+ # Get a Stimulus target element
33
+ # @param name [Symbol, String] Target name
34
+ # @return [Element, nil] The target element or nil
35
+ def get_target(name)
36
+ return nil unless has_target?(name)
37
+ method_name = "#{camelize(name, false)}Target"
38
+ `this[#{method_name}]`
39
+ end
40
+
41
+ # Get all Stimulus targets of a type
42
+ # @param name [Symbol, String] Target name
43
+ # @return [Array] Array of target elements
44
+ def get_targets(name)
45
+ method_name = "#{camelize(name, false)}Targets"
46
+ `Array.from(this[#{method_name}] || [])`
47
+ end
48
+
49
+ # Get the value of a target (input field)
50
+ # @param name [Symbol, String] Target name
51
+ # @return [String, nil] The target's value
52
+ def target_value(name)
53
+ return nil unless has_target?(name)
54
+ method_name = "#{camelize(name, false)}Target"
55
+ `this[#{method_name}].value`
56
+ end
57
+
58
+ # Set the value of a target (input field)
59
+ # @param name [Symbol, String] Target name
60
+ # @param value [String] The value to set
61
+ def target_set_value(name, value)
62
+ return unless has_target?(name)
63
+ method_name = "#{camelize(name, false)}Target"
64
+ `this[#{method_name}].value = #{value}`
65
+ end
66
+
67
+ # Get the innerHTML of a target
68
+ # @param name [Symbol, String] Target name
69
+ # @return [String, nil] The target's innerHTML
70
+ def target_html(name)
71
+ return nil unless has_target?(name)
72
+ method_name = "#{camelize(name, false)}Target"
73
+ `this[#{method_name}].innerHTML`
74
+ end
75
+
76
+ # Set the innerHTML of a target
77
+ # @param name [Symbol, String] Target name
78
+ # @param html [String] The HTML to set
79
+ def target_set_html(name, html)
80
+ return unless has_target?(name)
81
+ method_name = "#{camelize(name, false)}Target"
82
+ `this[#{method_name}].innerHTML = #{html}`
83
+ end
84
+
85
+ # Get the textContent of a target
86
+ # @param name [Symbol, String] Target name
87
+ # @return [String, nil] The target's textContent
88
+ def target_text(name)
89
+ return nil unless has_target?(name)
90
+ method_name = "#{camelize(name, false)}Target"
91
+ `this[#{method_name}].textContent`
92
+ end
93
+
94
+ # Set the textContent of a target
95
+ # @param name [Symbol, String] Target name
96
+ # @param text [String] The text to set
97
+ def target_set_text(name, text)
98
+ return unless has_target?(name)
99
+ method_name = "#{camelize(name, false)}Target"
100
+ `this[#{method_name}].textContent = #{text}`
101
+ end
102
+
103
+ # Get a data attribute from a target
104
+ # @param name [Symbol, String] Target name
105
+ # @param attr [String] The data attribute name (without 'data-' prefix)
106
+ # @return [String, nil] The attribute value
107
+ def target_data(name, attr)
108
+ return nil unless has_target?(name)
109
+ method_name = "#{camelize(name, false)}Target"
110
+ `this[#{method_name}].getAttribute('data-' + #{attr})`
111
+ end
112
+
113
+ # Set a data attribute on a target
114
+ # @param name [Symbol, String] Target name
115
+ # @param attr [String] The data attribute name (without 'data-' prefix)
116
+ # @param value [String] The value to set
117
+ def target_set_data(name, attr, value)
118
+ return unless has_target?(name)
119
+ method_name = "#{camelize(name, false)}Target"
120
+ `this[#{method_name}].setAttribute('data-' + #{attr}, #{value})`
121
+ end
122
+
123
+ # Clear a target's value (for input fields)
124
+ # @param name [Symbol, String] Target name
125
+ def target_clear(name)
126
+ target_set_value(name, '')
127
+ end
128
+
129
+ # Clear a target's innerHTML
130
+ # @param name [Symbol, String] Target name
131
+ def target_clear_html(name)
132
+ target_set_html(name, '')
133
+ end
134
+
135
+ # ===== Target Style Methods =====
136
+
137
+ # Set a style property on a target element
138
+ # @param name [Symbol, String] Target name
139
+ # @param property [String] CSS property name (e.g., 'display', 'color')
140
+ # @param value [String] CSS value
141
+ def set_target_style(name, property, value)
142
+ return unless has_target?(name)
143
+ method_name = "#{camelize(name, false)}Target"
144
+ `this[#{method_name}].style[#{property}] = #{value}`
145
+ end
146
+
147
+ # Get a style property from a target element
148
+ # @param name [Symbol, String] Target name
149
+ # @param property [String] CSS property name
150
+ # @return [String, nil] The style value
151
+ def get_target_style(name, property)
152
+ return nil unless has_target?(name)
153
+ method_name = "#{camelize(name, false)}Target"
154
+ `this[#{method_name}].style[#{property}]`
155
+ end
156
+
157
+ # Show a target element (set display to '')
158
+ # @param name [Symbol, String] Target name
159
+ def show_target(name)
160
+ set_target_style(name, 'display', '')
161
+ end
162
+
163
+ # Hide a target element (set display to 'none')
164
+ # @param name [Symbol, String] Target name
165
+ def hide_target(name)
166
+ set_target_style(name, 'display', 'none')
167
+ end
168
+
169
+ # Toggle target visibility
170
+ # @param name [Symbol, String] Target name
171
+ def toggle_target_visibility(name)
172
+ current = get_target_style(name, 'display')
173
+ if current == 'none'
174
+ show_target(name)
175
+ else
176
+ hide_target(name)
177
+ end
178
+ end
179
+
180
+ # ===== Target Class Methods =====
181
+
182
+ # Add a CSS class to a target element
183
+ # @param name [Symbol, String] Target name
184
+ # @param class_name [String] CSS class to add
185
+ def add_target_class(name, class_name)
186
+ return unless has_target?(name)
187
+ method_name = "#{camelize(name, false)}Target"
188
+ `this[#{method_name}].classList.add(#{class_name})`
189
+ end
190
+
191
+ # Remove a CSS class from a target element
192
+ # @param name [Symbol, String] Target name
193
+ # @param class_name [String] CSS class to remove
194
+ def remove_target_class(name, class_name)
195
+ return unless has_target?(name)
196
+ method_name = "#{camelize(name, false)}Target"
197
+ `this[#{method_name}].classList.remove(#{class_name})`
198
+ end
199
+
200
+ # Toggle a CSS class on a target element
201
+ # @param name [Symbol, String] Target name
202
+ # @param class_name [String] CSS class to toggle
203
+ def toggle_target_class(name, class_name)
204
+ return unless has_target?(name)
205
+ method_name = "#{camelize(name, false)}Target"
206
+ `this[#{method_name}].classList.toggle(#{class_name})`
207
+ end
208
+
209
+ # Check if a target has a CSS class
210
+ # @param name [Symbol, String] Target name
211
+ # @param class_name [String] CSS class to check
212
+ # @return [Boolean] true if target has the class
213
+ def has_target_class?(name, class_name)
214
+ return false unless has_target?(name)
215
+ method_name = "#{camelize(name, false)}Target"
216
+ `this[#{method_name}].classList.contains(#{class_name})`
217
+ end
218
+
219
+ # ===== Date/Time Methods =====
220
+
221
+ # Get current timestamp (milliseconds since epoch)
222
+ # @return [Integer] Current timestamp
223
+ def js_timestamp
224
+ `Date.now()`
225
+ end
226
+
227
+ # Get current date as ISO string
228
+ # @return [String] ISO date string
229
+ def js_iso_date
230
+ `new Date().toISOString()`
231
+ end
232
+
233
+ # Create a new Date object
234
+ # @param value [String, Integer, nil] Optional value to parse
235
+ # @return [Native] JavaScript Date object
236
+ def js_date(value = nil)
237
+ if value
238
+ `new Date(#{value})`
239
+ else
240
+ `new Date()`
241
+ end
242
+ end
243
+
244
+ # ===== RegExp Methods =====
245
+
246
+ # Create a JavaScript RegExp object
247
+ # @param pattern [String] The regex pattern
248
+ # @param flags [String] Optional flags (e.g., 'gi')
249
+ # @return [Native] JavaScript RegExp object
250
+ def js_regexp(pattern, flags = '')
251
+ `new RegExp(#{pattern}, #{flags})`
252
+ end
253
+
254
+ # Test if a string matches a regex pattern
255
+ # @param pattern [String] The regex pattern
256
+ # @param value [String] The string to test
257
+ # @param flags [String] Optional flags
258
+ # @return [Boolean] true if matches
259
+ def js_regexp_test(pattern, value, flags = '')
260
+ `new RegExp(#{pattern}, #{flags}).test(#{value})`
261
+ end
262
+
263
+ # ===== Timer Methods =====
264
+
265
+ # Set a timeout
266
+ # @param delay [Integer] Delay in milliseconds
267
+ # @yield Block to execute after delay
268
+ # @return [Integer] Timer ID
269
+ def set_timeout(delay, &block)
270
+ `setTimeout(function() { #{block.call} }, #{delay})`
271
+ end
272
+
273
+ # Set an interval
274
+ # @param delay [Integer] Interval in milliseconds
275
+ # @yield Block to execute at each interval
276
+ # @return [Integer] Timer ID
277
+ def set_interval(delay, &block)
278
+ `setInterval(function() { #{block.call} }, #{delay})`
279
+ end
280
+
281
+ # Clear a timeout
282
+ # @param timer_id [Integer] Timer ID to clear
283
+ def clear_timeout(timer_id)
284
+ `clearTimeout(#{timer_id})`
285
+ end
286
+
287
+ # Clear an interval
288
+ # @param timer_id [Integer] Timer ID to clear
289
+ def clear_interval(timer_id)
290
+ `clearInterval(#{timer_id})`
291
+ end
292
+
293
+ # ===== Body Style Methods =====
294
+
295
+ # Set document body style property
296
+ # @param property [String] CSS property name
297
+ # @param value [String] CSS value
298
+ def body_style(property, value)
299
+ `document.body.style[#{property}] = #{value}`
300
+ end
301
+
302
+ # Lock body scroll (prevent scrolling)
303
+ def lock_body_scroll
304
+ body_style('overflow', 'hidden')
305
+ end
306
+
307
+ # Unlock body scroll (restore scrolling)
308
+ def unlock_body_scroll
309
+ body_style('overflow', '')
310
+ end
311
+
312
+ # ===== Array Helper Methods =====
313
+
314
+ # Create a new JavaScript array
315
+ # @return [Native] Empty JavaScript array
316
+ def js_array
317
+ `[]`
318
+ end
319
+
320
+ # Push item to JavaScript array
321
+ # @param array [Native] JavaScript array
322
+ # @param item [Object] Item to push
323
+ def js_array_push(array, item)
324
+ `#{array}.push(#{item.to_n})`
325
+ end
326
+
327
+ # Get JavaScript array length
328
+ # @param array [Native] JavaScript array
329
+ # @return [Integer] Array length
330
+ def js_array_length(array)
331
+ `#{array}.length`
332
+ end
333
+
334
+ # Get item from JavaScript array at index
335
+ # @param array [Native] JavaScript array
336
+ # @param index [Integer] Index
337
+ # @return [Object] Item at index
338
+ def js_array_at(array, index)
339
+ `#{array}[#{index}]`
340
+ end
341
+
342
+ # ===== LocalStorage Methods =====
343
+
344
+ # Get item from localStorage
345
+ # @param key [String] Storage key
346
+ # @return [String, nil] Stored value or nil
347
+ def storage_get(key)
348
+ `localStorage.getItem(#{key})`
349
+ end
350
+
351
+ # Set item in localStorage
352
+ # @param key [String] Storage key
353
+ # @param value [String] Value to store
354
+ def storage_set(key, value)
355
+ `localStorage.setItem(#{key}, #{value})`
356
+ end
357
+
358
+ # Remove item from localStorage
359
+ # @param key [String] Storage key
360
+ def storage_remove(key)
361
+ `localStorage.removeItem(#{key})`
362
+ end
363
+
364
+ # Get JSON-parsed value from localStorage
365
+ # @param key [String] Storage key
366
+ # @param default [Object] Default value if key doesn't exist
367
+ # @return [Object] Parsed value or default
368
+ def storage_get_json(key, default = nil)
369
+ stored = storage_get(key)
370
+ return default if `#{stored} === null`
371
+ `JSON.parse(#{stored})`
372
+ end
373
+
374
+ # Set JSON-stringified value in localStorage
375
+ # @param key [String] Storage key
376
+ # @param value [Object] Value to store (will be JSON-stringified)
377
+ def storage_set_json(key, value)
378
+ `localStorage.setItem(#{key}, JSON.stringify(#{value.to_n}))`
379
+ end
380
+
381
+ # ===== Event Methods =====
382
+
383
+ # Dispatch a custom event on window
384
+ # @param name [String] Event name
385
+ # @param detail [Hash] Event detail data
386
+ def dispatch_window_event(name, detail = {})
387
+ native_detail = detail.is_a?(Hash) ? detail.to_n : detail
388
+ `
389
+ const event = new CustomEvent(#{name}, { detail: #{native_detail} });
390
+ window.dispatchEvent(event);
391
+ `
392
+ end
393
+
394
+ # Dispatch a custom event on the controller element
395
+ # @param name [String] Event name
396
+ # @param detail [Hash] Event detail data
397
+ def dispatch_event(name, detail = {})
398
+ native_detail = detail.is_a?(Hash) ? detail.to_n : detail
399
+ `
400
+ const event = new CustomEvent(#{name}, { detail: #{native_detail} });
401
+ this.element.dispatchEvent(event);
402
+ `
403
+ end
404
+
405
+ # Add window event listener
406
+ # @param name [String] Event name
407
+ # @yield Block to execute when event fires
408
+ def on_window_event(name, &block)
409
+ `window.addEventListener(#{name}, #{block})`
410
+ end
411
+
412
+ # Remove window event listener
413
+ # @param name [String] Event name
414
+ # @param handler [Native] Handler function to remove
415
+ def off_window_event(name, handler)
416
+ `window.removeEventListener(#{name}, #{handler})`
417
+ end
418
+
419
+ # Add document event listener
420
+ # @param name [String] Event name
421
+ # @yield Block to execute when event fires
422
+ def on_document_event(name, &block)
423
+ `document.addEventListener(#{name}, #{block})`
424
+ end
425
+
426
+ # Add event listener when DOM is ready
427
+ # @yield Block to execute when DOM is ready
428
+ def on_dom_ready(&block)
429
+ `document.addEventListener('DOMContentLoaded', #{block})`
430
+ end
431
+
432
+ # Add event listener to any element
433
+ # @param element [Native] DOM element
434
+ # @param name [String] Event name
435
+ # @yield Block to execute when event fires
436
+ def on_element_event(element, name, &block)
437
+ `#{element}.addEventListener(#{name}, #{block})`
438
+ end
439
+
440
+ # Remove event listener from element
441
+ # @param element [Native] DOM element
442
+ # @param name [String] Event name
443
+ # @param handler [Native] Handler function to remove
444
+ def off_element_event(element, name, handler)
445
+ `#{element}.removeEventListener(#{name}, #{handler})`
446
+ end
447
+
448
+ # Add event listener to controller's element (this.element)
449
+ # @param name [String] Event name
450
+ # @yield Block to execute when event fires
451
+ def on_controller_event(name, &block)
452
+ `
453
+ const handler = #{block};
454
+ this.element.addEventListener(#{name}, function(e) {
455
+ handler(e);
456
+ });
457
+ `
458
+ end
459
+
460
+ # Get the current event's target element
461
+ # @return [Native] Event target element
462
+ def event_target
463
+ `event.currentTarget`
464
+ end
465
+
466
+ # Get data attribute from current event target
467
+ # @param attr [String] Data attribute name (without 'data-' prefix)
468
+ # @return [String, nil] Attribute value
469
+ def event_data(attr)
470
+ `event.currentTarget.getAttribute('data-' + #{attr})`
471
+ end
472
+
473
+ # Get integer data attribute from current event target
474
+ # @param attr [String] Data attribute name (without 'data-' prefix)
475
+ # @return [Integer, nil] Parsed integer value
476
+ def event_data_int(attr)
477
+ parse_int(event_data(attr))
478
+ end
479
+
480
+ # Prevent default event behavior
481
+ def prevent_default
482
+ `event.preventDefault()`
483
+ end
484
+
485
+ # Get event key (for keyboard events)
486
+ # @return [String] Key name
487
+ def event_key
488
+ `event.key`
489
+ end
490
+
491
+ # ===== Element Methods =====
492
+
493
+ # Add class to element
494
+ # @param element [Native] DOM element
495
+ # @param class_name [String] CSS class to add
496
+ def add_class(element, class_name)
497
+ `#{element}.classList.add(#{class_name})`
498
+ end
499
+
500
+ # Remove class from element
501
+ # @param element [Native] DOM element
502
+ # @param class_name [String] CSS class to remove
503
+ def remove_class(element, class_name)
504
+ `#{element}.classList.remove(#{class_name})`
505
+ end
506
+
507
+ # Toggle class on element
508
+ # @param element [Native] DOM element
509
+ # @param class_name [String] CSS class to toggle
510
+ def toggle_class(element, class_name)
511
+ `#{element}.classList.toggle(#{class_name})`
512
+ end
513
+
514
+ # Check if element has class
515
+ # @param element [Native] DOM element
516
+ # @param class_name [String] CSS class to check
517
+ # @return [Boolean]
518
+ def has_class?(element, class_name)
519
+ `#{element}.classList.contains(#{class_name})`
520
+ end
521
+
522
+ # Set element attribute
523
+ # @param element [Native] DOM element
524
+ # @param attr [String] Attribute name
525
+ # @param value [String] Attribute value
526
+ def set_attr(element, attr, value)
527
+ `#{element}.setAttribute(#{attr}, #{value})`
528
+ end
529
+
530
+ # Get element attribute
531
+ # @param element [Native] DOM element
532
+ # @param attr [String] Attribute name
533
+ # @return [String, nil] Attribute value
534
+ def get_attr(element, attr)
535
+ `#{element}.getAttribute(#{attr})`
536
+ end
537
+
538
+ # Remove element attribute
539
+ # @param element [Native] DOM element
540
+ # @param attr [String] Attribute name
541
+ def remove_attr(element, attr)
542
+ `#{element}.removeAttribute(#{attr})`
543
+ end
544
+
545
+ # Set element style
546
+ # @param element [Native] DOM element
547
+ # @param property [String] CSS property
548
+ # @param value [String] CSS value
549
+ def set_style(element, property, value)
550
+ `#{element}.style[#{property}] = #{value}`
551
+ end
552
+
553
+ # Set element innerHTML
554
+ # @param element [Native] DOM element
555
+ # @param html [String] HTML content
556
+ def set_html(element, html)
557
+ `#{element}.innerHTML = #{html}`
558
+ end
559
+
560
+ # Set element textContent
561
+ # @param element [Native] DOM element
562
+ # @param text [String] Text content
563
+ def set_text(element, text)
564
+ `#{element}.textContent = #{text}`
565
+ end
566
+
567
+ # Get element value (for inputs)
568
+ # @param element [Native] DOM element
569
+ # @return [String] Element value
570
+ def get_value(element)
571
+ `#{element}.value`
572
+ end
573
+
574
+ # Set element value (for inputs)
575
+ # @param element [Native] DOM element
576
+ # @param value [String] Value to set
577
+ def set_value(element, value)
578
+ `#{element}.value = #{value}`
579
+ end
580
+
581
+ # Focus element
582
+ # @param element [Native] DOM element
583
+ def focus(element)
584
+ `#{element}.focus()`
585
+ end
586
+
587
+ # Check if element has attribute
588
+ # @param element [Native] DOM element
589
+ # @param attr [String] Attribute name
590
+ # @return [Boolean]
591
+ def has_attr?(element, attr)
592
+ `#{element}.hasAttribute(#{attr})`
593
+ end
594
+
595
+ # ===== DOM Creation Methods =====
596
+
597
+ # Create a new DOM element
598
+ # @param tag [String] HTML tag name
599
+ # @return [Native] Created element
600
+ def create_element(tag)
601
+ `document.createElement(#{tag})`
602
+ end
603
+
604
+ # Append child to element
605
+ # @param parent [Native] Parent element
606
+ # @param child [Native] Child element to append
607
+ def append_child(parent, child)
608
+ `#{parent}.appendChild(#{child})`
609
+ end
610
+
611
+ # Remove element from DOM
612
+ # @param element [Native] Element to remove
613
+ def remove_element(element)
614
+ `#{element}.remove()`
615
+ end
616
+
617
+ # Get next element sibling
618
+ # @param element [Native] DOM element
619
+ # @return [Native, nil] Next sibling element
620
+ def next_sibling(element)
621
+ `#{element}.nextElementSibling`
622
+ end
623
+
624
+ # Get previous element sibling
625
+ # @param element [Native] DOM element
626
+ # @return [Native, nil] Previous sibling element
627
+ def prev_sibling(element)
628
+ `#{element}.previousElementSibling`
629
+ end
630
+
631
+ # Get parent element
632
+ # @param element [Native] DOM element
633
+ # @return [Native, nil] Parent element
634
+ def parent(element)
635
+ `#{element}.parentElement`
636
+ end
637
+
638
+ # ===== DOM Query Methods =====
639
+
640
+ # Query selector on document
641
+ # @param selector [String] CSS selector
642
+ # @return [Native, nil] Element or nil
643
+ def query(selector)
644
+ `document.querySelector(#{selector})`
645
+ end
646
+
647
+ # Query selector all on document
648
+ # @param selector [String] CSS selector
649
+ # @return [Array] Array of elements
650
+ def query_all(selector)
651
+ `Array.from(document.querySelectorAll(#{selector}))`
652
+ end
653
+
654
+ # Query selector on controller element
655
+ # @param selector [String] CSS selector
656
+ # @return [Native, nil] Element or nil
657
+ def query_element(selector)
658
+ `this.element.querySelector(#{selector})`
659
+ end
660
+
661
+ # Query selector all on controller element
662
+ # @param selector [String] CSS selector
663
+ # @return [Array] Array of elements
664
+ def query_all_element(selector)
665
+ `Array.from(this.element.querySelectorAll(#{selector}))`
666
+ end
667
+
668
+ # ===== Document Methods =====
669
+
670
+ # Get document root element (html)
671
+ # @return [Native] HTML element
672
+ def doc_root
673
+ `document.documentElement`
674
+ end
675
+
676
+ # Set attribute on document root
677
+ # @param attr [String] Attribute name
678
+ # @param value [String] Attribute value
679
+ def set_root_attr(attr, value)
680
+ `document.documentElement.setAttribute(#{attr}, #{value})`
681
+ end
682
+
683
+ # Get attribute from document root
684
+ # @param attr [String] Attribute name
685
+ # @return [String, nil] Attribute value
686
+ def get_root_attr(attr)
687
+ `document.documentElement.getAttribute(#{attr})`
688
+ end
689
+
690
+ # ===== Template Methods =====
691
+
692
+ # Clone a template target's content
693
+ # @param name [Symbol, String] Template target name
694
+ # @return [Native] Cloned content
695
+ def clone_template(name)
696
+ method_name = "#{camelize(name, false)}Target"
697
+ `this[#{method_name}].content.cloneNode(true)`
698
+ end
699
+
700
+ # Get first element child from cloned template
701
+ # @param clone [Native] Cloned template content
702
+ # @return [Native, nil] First element child
703
+ def template_first_child(clone)
704
+ `#{clone}.firstElementChild`
705
+ end
706
+
707
+ # ===== Stimulus Controller Element Methods =====
708
+
709
+ # Add class to controller element
710
+ # @param class_name [String] CSS class to add
711
+ def element_add_class(class_name)
712
+ `this.element.classList.add(#{class_name})`
713
+ end
714
+
715
+ # Remove class from controller element
716
+ # @param class_name [String] CSS class to remove
717
+ def element_remove_class(class_name)
718
+ `this.element.classList.remove(#{class_name})`
719
+ end
720
+
721
+ # Toggle class on controller element
722
+ # @param class_name [String] CSS class to toggle
723
+ def element_toggle_class(class_name)
724
+ `this.element.classList.toggle(#{class_name})`
725
+ end
726
+
727
+ # ===== Utility Methods =====
728
+
729
+ # Generate a unique ID based on timestamp
730
+ # @return [Integer] Unique ID
731
+ def generate_id
732
+ js_timestamp
733
+ end
734
+
735
+ # Focus a target element
736
+ # @param name [Symbol, String] Target name
737
+ def target_focus(name)
738
+ return unless has_target?(name)
739
+ method_name = "#{camelize(name, false)}Target"
740
+ `this[#{method_name}].focus()`
741
+ end
742
+
743
+ # Blur (unfocus) a target element
744
+ # @param name [Symbol, String] Target name
745
+ def target_blur(name)
746
+ return unless has_target?(name)
747
+ method_name = "#{camelize(name, false)}Target"
748
+ `this[#{method_name}].blur()`
749
+ end
750
+
751
+ # ===== Type Conversion Methods =====
752
+
753
+ # Parse string to integer (wrapper for JavaScript parseInt)
754
+ # @param value [String, Number] Value to parse
755
+ # @param radix [Integer] Radix (default: 10)
756
+ # @return [Integer, NaN] Parsed integer
757
+ def parse_int(value, radix = 10)
758
+ `parseInt(#{value}, #{radix})`
759
+ end
760
+
761
+ # Parse string to float (wrapper for JavaScript parseFloat)
762
+ # @param value [String, Number] Value to parse
763
+ # @return [Float, NaN] Parsed float
764
+ def parse_float(value)
765
+ `parseFloat(#{value})`
766
+ end
767
+
768
+ # Check if value is NaN
769
+ # @param value [Number] Value to check
770
+ # @return [Boolean] true if NaN
771
+ def is_nan?(value)
772
+ `Number.isNaN(#{value})`
773
+ end
774
+
775
+ # Parse integer with default value (returns default if NaN)
776
+ # @param value [String, Number] Value to parse
777
+ # @param default_value [Integer] Default value if parsing fails
778
+ # @return [Integer] Parsed integer or default
779
+ def parse_int_or(value, default_value = 0)
780
+ result = parse_int(value)
781
+ is_nan?(result) ? default_value : result
782
+ end
783
+
784
+ # Parse float with default value (returns default if NaN)
785
+ # @param value [String, Number] Value to parse
786
+ # @param default_value [Float] Default value if parsing fails
787
+ # @return [Float] Parsed float or default
788
+ def parse_float_or(value, default_value = 0.0)
789
+ result = parse_float(value)
790
+ is_nan?(result) ? default_value : result
791
+ end
792
+
793
+ private
794
+
795
+ # Convert snake_case to camelCase
796
+ # @param name [Symbol, String] The name to convert
797
+ # @param capitalize_first [Boolean] Whether to capitalize first letter
798
+ # @return [String] camelCase string
799
+ def camelize(name, capitalize_first = true)
800
+ str = name.to_s
801
+ parts = str.split('_')
802
+ if capitalize_first
803
+ parts.map(&:capitalize).join
804
+ else
805
+ ([parts.first] + parts[1..-1].map(&:capitalize)).join
806
+ end
807
+ end
808
+ end
809
+ end
810
+ end
811
+
812
+ # Alias for backward compatibility
813
+ StimulusHelpers = OpalVite::Concerns::StimulusHelpers