opal-vite 0.2.7 → 0.2.9
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 +4 -4
- data/lib/opal/vite/version.rb +1 -1
- data/opal/opal_vite/concerns/dom_helpers.rb +6 -83
- data/opal/opal_vite/concerns/js_proxy_ex.rb +6 -408
- data/opal/opal_vite/concerns/react_helpers.rb +7 -310
- data/opal/opal_vite/concerns/stimulus_helpers.rb +6 -1300
- data/opal/opal_vite/concerns/storable.rb +6 -25
- data/opal/opal_vite/concerns/toastable.rb +6 -30
- data/opal/opal_vite/concerns/v1/dom_helpers.rb +91 -0
- data/opal/opal_vite/concerns/v1/js_proxy_ex.rb +416 -0
- data/opal/opal_vite/concerns/v1/react_helpers.rb +324 -0
- data/opal/opal_vite/concerns/v1/stimulus_helpers.rb +1308 -0
- data/opal/opal_vite/concerns/v1/storable.rb +33 -0
- data/opal/opal_vite/concerns/v1/toastable.rb +38 -0
- data/opal/opal_vite/concerns/v1/vue_helpers.rb +259 -0
- data/opal/opal_vite/concerns/v1.rb +8 -0
- data/opal/opal_vite/concerns/vue_helpers.rb +7 -244
- data/opal/opal_vite/concerns.rb +6 -6
- metadata +9 -1
|
@@ -1,1306 +1,12 @@
|
|
|
1
1
|
# backtick_javascript: true
|
|
2
|
+
# Backward compatibility wrapper - delegates to v1
|
|
3
|
+
require 'opal_vite/concerns/v1/stimulus_helpers'
|
|
2
4
|
|
|
5
|
+
`console.warn("[DEPRECATION] require 'opal_vite/concerns/stimulus_helpers' is deprecated. Please use require 'opal_vite/concerns/v1/stimulus_helpers' instead.")`
|
|
6
|
+
|
|
7
|
+
# Alias old module path for backward compatibility
|
|
3
8
|
module OpalVite
|
|
4
9
|
module Concerns
|
|
5
|
-
|
|
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
|
-
# ===== JavaScript Property Access Methods =====
|
|
794
|
-
|
|
795
|
-
# Get a JavaScript property from the controller (this[name])
|
|
796
|
-
# @param name [Symbol, String] Property name
|
|
797
|
-
# @return [Native] Property value
|
|
798
|
-
def js_prop(name)
|
|
799
|
-
`this[#{name.to_s}]`
|
|
800
|
-
end
|
|
801
|
-
|
|
802
|
-
# Set a JavaScript property on the controller (this[name] = value)
|
|
803
|
-
# @param name [Symbol, String] Property name
|
|
804
|
-
# @param value [Object] Value to set
|
|
805
|
-
def js_set_prop(name, value)
|
|
806
|
-
`this[#{name.to_s}] = #{value}`
|
|
807
|
-
end
|
|
808
|
-
|
|
809
|
-
# Check if controller has a JavaScript property
|
|
810
|
-
# @param name [Symbol, String] Property name
|
|
811
|
-
# @return [Boolean] true if property exists and is truthy
|
|
812
|
-
def js_has_prop?(name)
|
|
813
|
-
`!!this[#{name.to_s}]`
|
|
814
|
-
end
|
|
815
|
-
|
|
816
|
-
# Call a JavaScript method on the controller (this[name](...args))
|
|
817
|
-
# @param name [Symbol, String] Method name
|
|
818
|
-
# @param args [Array] Arguments to pass
|
|
819
|
-
# @return [Native] Method return value
|
|
820
|
-
def js_call(name, *args)
|
|
821
|
-
if args.empty?
|
|
822
|
-
`this[#{name.to_s}]()`
|
|
823
|
-
else
|
|
824
|
-
native_args = args.map { |a| a.respond_to?(:to_n) ? a.to_n : a }
|
|
825
|
-
`this[#{name.to_s}].apply(this, #{native_args})`
|
|
826
|
-
end
|
|
827
|
-
end
|
|
828
|
-
|
|
829
|
-
# Call a method on a JavaScript object
|
|
830
|
-
# @param obj [Native] JavaScript object
|
|
831
|
-
# @param method [Symbol, String] Method name
|
|
832
|
-
# @param args [Array] Arguments to pass
|
|
833
|
-
# @return [Native] Method return value
|
|
834
|
-
def js_call_on(obj, method, *args)
|
|
835
|
-
if args.empty?
|
|
836
|
-
`#{obj}[#{method.to_s}]()`
|
|
837
|
-
else
|
|
838
|
-
native_args = args.map { |a| a.respond_to?(:to_n) ? a.to_n : a }
|
|
839
|
-
`#{obj}[#{method.to_s}].apply(#{obj}, #{native_args})`
|
|
840
|
-
end
|
|
841
|
-
end
|
|
842
|
-
|
|
843
|
-
# Get a property from a JavaScript object
|
|
844
|
-
# @param obj [Native] JavaScript object
|
|
845
|
-
# @param prop [Symbol, String] Property name
|
|
846
|
-
# @return [Native] Property value
|
|
847
|
-
def js_get(obj, prop)
|
|
848
|
-
`#{obj}[#{prop.to_s}]`
|
|
849
|
-
end
|
|
850
|
-
|
|
851
|
-
# Set a property on a JavaScript object
|
|
852
|
-
# @param obj [Native] JavaScript object
|
|
853
|
-
# @param prop [Symbol, String] Property name
|
|
854
|
-
# @param value [Object] Value to set
|
|
855
|
-
def js_set(obj, prop, value)
|
|
856
|
-
`#{obj}[#{prop.to_s}] = #{value}`
|
|
857
|
-
end
|
|
858
|
-
|
|
859
|
-
# ===== JSON Methods =====
|
|
860
|
-
|
|
861
|
-
# Parse JSON string to JavaScript object
|
|
862
|
-
# @param json_string [String] JSON string
|
|
863
|
-
# @return [Native] Parsed JavaScript object
|
|
864
|
-
def json_parse(json_string)
|
|
865
|
-
`JSON.parse(#{json_string})`
|
|
866
|
-
end
|
|
867
|
-
|
|
868
|
-
# Stringify JavaScript object to JSON
|
|
869
|
-
# @param obj [Object] Object to stringify
|
|
870
|
-
# @return [String] JSON string
|
|
871
|
-
def json_stringify(obj)
|
|
872
|
-
native_obj = obj.respond_to?(:to_n) ? obj.to_n : obj
|
|
873
|
-
`JSON.stringify(#{native_obj})`
|
|
874
|
-
end
|
|
875
|
-
|
|
876
|
-
# ===== Console Methods =====
|
|
877
|
-
|
|
878
|
-
# Log to console
|
|
879
|
-
# @param args [Array] Arguments to log
|
|
880
|
-
def console_log(*args)
|
|
881
|
-
`console.log.apply(console, #{args})`
|
|
882
|
-
end
|
|
883
|
-
|
|
884
|
-
# Log warning to console
|
|
885
|
-
# @param args [Array] Arguments to log
|
|
886
|
-
def console_warn(*args)
|
|
887
|
-
`console.warn.apply(console, #{args})`
|
|
888
|
-
end
|
|
889
|
-
|
|
890
|
-
# Log error to console
|
|
891
|
-
# @param args [Array] Arguments to log
|
|
892
|
-
def console_error(*args)
|
|
893
|
-
`console.error.apply(console, #{args})`
|
|
894
|
-
end
|
|
895
|
-
|
|
896
|
-
# ===== String Methods =====
|
|
897
|
-
|
|
898
|
-
# Get character at index from string
|
|
899
|
-
# @param str [String] JavaScript string
|
|
900
|
-
# @param index [Integer] Character index
|
|
901
|
-
# @return [String] Character at index
|
|
902
|
-
def js_string_char_at(str, index)
|
|
903
|
-
`#{str}.charAt(#{index})`
|
|
904
|
-
end
|
|
905
|
-
|
|
906
|
-
# Get substring
|
|
907
|
-
# @param str [String] JavaScript string
|
|
908
|
-
# @param start [Integer] Start index
|
|
909
|
-
# @param end_idx [Integer, nil] End index (optional)
|
|
910
|
-
# @return [String] Substring
|
|
911
|
-
def js_substring(str, start, end_idx = nil)
|
|
912
|
-
if end_idx
|
|
913
|
-
`#{str}.substring(#{start}, #{end_idx})`
|
|
914
|
-
else
|
|
915
|
-
`#{str}.substring(#{start})`
|
|
916
|
-
end
|
|
917
|
-
end
|
|
918
|
-
|
|
919
|
-
# Split string
|
|
920
|
-
# @param str [String] JavaScript string
|
|
921
|
-
# @param separator [String] Separator
|
|
922
|
-
# @return [Native] Array of substrings
|
|
923
|
-
def js_split(str, separator)
|
|
924
|
-
`#{str}.split(#{separator})`
|
|
925
|
-
end
|
|
926
|
-
|
|
927
|
-
# Trim whitespace from string
|
|
928
|
-
# @param str [String] JavaScript string
|
|
929
|
-
# @return [String] Trimmed string
|
|
930
|
-
def js_trim(str)
|
|
931
|
-
`#{str}.trim()`
|
|
932
|
-
end
|
|
933
|
-
|
|
934
|
-
# Check if string includes substring
|
|
935
|
-
# @param str [String] JavaScript string
|
|
936
|
-
# @param search [String] Substring to search for
|
|
937
|
-
# @return [Boolean] true if includes
|
|
938
|
-
def js_includes?(str, search)
|
|
939
|
-
`#{str}.includes(#{search})`
|
|
940
|
-
end
|
|
941
|
-
|
|
942
|
-
# ===== Comparison Methods =====
|
|
943
|
-
|
|
944
|
-
# Check strict equality (===) between two JavaScript values
|
|
945
|
-
# @param a [Native] First value
|
|
946
|
-
# @param b [Native] Second value
|
|
947
|
-
# @return [Boolean] true if strictly equal
|
|
948
|
-
def js_equals?(a, b)
|
|
949
|
-
`#{a} === #{b}`
|
|
950
|
-
end
|
|
951
|
-
|
|
952
|
-
# Check loose equality (==) between two JavaScript values
|
|
953
|
-
# @param a [Native] First value
|
|
954
|
-
# @param b [Native] Second value
|
|
955
|
-
# @return [Boolean] true if loosely equal
|
|
956
|
-
def js_loose_equals?(a, b)
|
|
957
|
-
`#{a} == #{b}`
|
|
958
|
-
end
|
|
959
|
-
|
|
960
|
-
# ===== Math Methods =====
|
|
961
|
-
|
|
962
|
-
# Generate random number between 0 and 1
|
|
963
|
-
# @return [Float] Random number
|
|
964
|
-
def js_random
|
|
965
|
-
`Math.random()`
|
|
966
|
-
end
|
|
967
|
-
|
|
968
|
-
# Get minimum of two numbers
|
|
969
|
-
# @param a [Number] First number
|
|
970
|
-
# @param b [Number] Second number
|
|
971
|
-
# @return [Number] Minimum value
|
|
972
|
-
def js_min(a, b)
|
|
973
|
-
`Math.min(#{a}, #{b})`
|
|
974
|
-
end
|
|
975
|
-
|
|
976
|
-
# Get maximum of two numbers
|
|
977
|
-
# @param a [Number] First number
|
|
978
|
-
# @param b [Number] Second number
|
|
979
|
-
# @return [Number] Maximum value
|
|
980
|
-
def js_max(a, b)
|
|
981
|
-
`Math.max(#{a}, #{b})`
|
|
982
|
-
end
|
|
983
|
-
|
|
984
|
-
# Get absolute value
|
|
985
|
-
# @param num [Number] Number
|
|
986
|
-
# @return [Number] Absolute value
|
|
987
|
-
def js_abs(num)
|
|
988
|
-
`Math.abs(#{num})`
|
|
989
|
-
end
|
|
990
|
-
|
|
991
|
-
# Round number
|
|
992
|
-
# @param num [Number] Number
|
|
993
|
-
# @return [Integer] Rounded number
|
|
994
|
-
def js_round(num)
|
|
995
|
-
`Math.round(#{num})`
|
|
996
|
-
end
|
|
997
|
-
|
|
998
|
-
# Ceiling of number
|
|
999
|
-
# @param num [Number] Number
|
|
1000
|
-
# @return [Integer] Ceiling value
|
|
1001
|
-
def js_ceil(num)
|
|
1002
|
-
`Math.ceil(#{num})`
|
|
1003
|
-
end
|
|
1004
|
-
|
|
1005
|
-
# Format number with fixed decimal places
|
|
1006
|
-
# @param num [Number] Number to format
|
|
1007
|
-
# @param digits [Integer] Number of decimal places
|
|
1008
|
-
# @return [String] Formatted number string
|
|
1009
|
-
def js_to_fixed(num, digits)
|
|
1010
|
-
`#{num}.toFixed(#{digits})`
|
|
1011
|
-
end
|
|
1012
|
-
|
|
1013
|
-
# Generate random integer between 0 and max (exclusive)
|
|
1014
|
-
# @param max [Integer] Maximum value (exclusive)
|
|
1015
|
-
# @return [Integer] Random integer
|
|
1016
|
-
def random_int(max)
|
|
1017
|
-
`Math.floor(Math.random() * #{max})`
|
|
1018
|
-
end
|
|
1019
|
-
|
|
1020
|
-
# Floor a number
|
|
1021
|
-
# @param num [Number] Number to floor
|
|
1022
|
-
# @return [Integer] Floored number
|
|
1023
|
-
def js_floor(num)
|
|
1024
|
-
`Math.floor(#{num})`
|
|
1025
|
-
end
|
|
1026
|
-
|
|
1027
|
-
# ===== Global Object Access =====
|
|
1028
|
-
|
|
1029
|
-
# Check if a global JavaScript object/class exists
|
|
1030
|
-
# @param name [String] Global name (e.g., 'Chart', 'React')
|
|
1031
|
-
# @return [Boolean] true if exists
|
|
1032
|
-
def js_global_exists?(name)
|
|
1033
|
-
`typeof window[#{name}] !== 'undefined'`
|
|
1034
|
-
end
|
|
1035
|
-
|
|
1036
|
-
# Get a global JavaScript object/class
|
|
1037
|
-
# @param name [String] Global name
|
|
1038
|
-
# @return [Native] Global object
|
|
1039
|
-
def js_global(name)
|
|
1040
|
-
`window[#{name}]`
|
|
1041
|
-
end
|
|
1042
|
-
|
|
1043
|
-
# Create new instance of a JavaScript class
|
|
1044
|
-
# @param klass [Native] JavaScript class/constructor
|
|
1045
|
-
# @param args [Array] Constructor arguments
|
|
1046
|
-
# @return [Native] New instance
|
|
1047
|
-
def js_new(klass, *args)
|
|
1048
|
-
if args.empty?
|
|
1049
|
-
`new klass()`
|
|
1050
|
-
else
|
|
1051
|
-
# Convert Ruby objects to native, pass JS objects as-is
|
|
1052
|
-
native_args = args.map { |a| `#{a} != null && typeof #{a}.$to_n === 'function' ? #{a}.$to_n() : #{a}` }
|
|
1053
|
-
# Use Reflect.construct for dynamic argument passing
|
|
1054
|
-
`Reflect.construct(#{klass}, #{native_args})`
|
|
1055
|
-
end
|
|
1056
|
-
end
|
|
1057
|
-
|
|
1058
|
-
# Define a JavaScript function on the controller (this[name] = function)
|
|
1059
|
-
# @param name [Symbol, String] Function name
|
|
1060
|
-
# @yield Block that becomes the function body
|
|
1061
|
-
def js_define_method(name, &block)
|
|
1062
|
-
`this[#{name.to_s}] = #{block}`
|
|
1063
|
-
end
|
|
1064
|
-
|
|
1065
|
-
# Define a JavaScript function on an object (obj[name] = function)
|
|
1066
|
-
# @param obj [Native] JavaScript object
|
|
1067
|
-
# @param name [Symbol, String] Function name
|
|
1068
|
-
# @yield Block that becomes the function body
|
|
1069
|
-
def js_define_method_on(obj, name, &block)
|
|
1070
|
-
`#{obj}[#{name.to_s}] = #{block}`
|
|
1071
|
-
end
|
|
1072
|
-
|
|
1073
|
-
# ===== Array Methods =====
|
|
1074
|
-
|
|
1075
|
-
# Get array length
|
|
1076
|
-
# @param arr [Native] JavaScript array
|
|
1077
|
-
# @return [Integer] Array length
|
|
1078
|
-
def js_length(arr)
|
|
1079
|
-
`#{arr}.length`
|
|
1080
|
-
end
|
|
1081
|
-
|
|
1082
|
-
# Map over array with block
|
|
1083
|
-
# @param arr [Native] JavaScript array
|
|
1084
|
-
# @yield [item] Block to execute for each item
|
|
1085
|
-
# @return [Native] New array with mapped values
|
|
1086
|
-
def js_map(arr, &block)
|
|
1087
|
-
`#{arr}.map(#{block})`
|
|
1088
|
-
end
|
|
1089
|
-
|
|
1090
|
-
# Filter array with block
|
|
1091
|
-
# @param arr [Native] JavaScript array
|
|
1092
|
-
# @yield [item] Block to execute for each item
|
|
1093
|
-
# @return [Native] Filtered array
|
|
1094
|
-
def js_filter(arr, &block)
|
|
1095
|
-
`#{arr}.filter(#{block})`
|
|
1096
|
-
end
|
|
1097
|
-
|
|
1098
|
-
# Reduce array with block
|
|
1099
|
-
# @param arr [Native] JavaScript array
|
|
1100
|
-
# @param initial [Object] Initial value
|
|
1101
|
-
# @yield [acc, item] Block to execute for each item
|
|
1102
|
-
# @return [Object] Reduced value
|
|
1103
|
-
def js_reduce(arr, initial, &block)
|
|
1104
|
-
`#{arr}.reduce(#{block}, #{initial})`
|
|
1105
|
-
end
|
|
1106
|
-
|
|
1107
|
-
# ForEach over array with block
|
|
1108
|
-
# @param arr [Native] JavaScript array
|
|
1109
|
-
# @yield [item, index] Block to execute for each item
|
|
1110
|
-
def js_each(arr, &block)
|
|
1111
|
-
`#{arr}.forEach(#{block})`
|
|
1112
|
-
end
|
|
1113
|
-
|
|
1114
|
-
# Slice array
|
|
1115
|
-
# @param arr [Native] JavaScript array
|
|
1116
|
-
# @param start [Integer] Start index
|
|
1117
|
-
# @param end_idx [Integer, nil] End index (optional)
|
|
1118
|
-
# @return [Native] Sliced array
|
|
1119
|
-
def js_slice(arr, start, end_idx = nil)
|
|
1120
|
-
if end_idx
|
|
1121
|
-
`#{arr}.slice(#{start}, #{end_idx})`
|
|
1122
|
-
else
|
|
1123
|
-
`#{arr}.slice(#{start})`
|
|
1124
|
-
end
|
|
1125
|
-
end
|
|
1126
|
-
|
|
1127
|
-
# ===== Object Methods =====
|
|
1128
|
-
|
|
1129
|
-
# Create empty JavaScript object
|
|
1130
|
-
# @return [Native] Empty JavaScript object
|
|
1131
|
-
def js_object
|
|
1132
|
-
`{}`
|
|
1133
|
-
end
|
|
1134
|
-
|
|
1135
|
-
# Get object keys
|
|
1136
|
-
# @param obj [Native] JavaScript object
|
|
1137
|
-
# @return [Native] Array of keys
|
|
1138
|
-
def js_keys(obj)
|
|
1139
|
-
`Object.keys(#{obj})`
|
|
1140
|
-
end
|
|
1141
|
-
|
|
1142
|
-
# Get object values
|
|
1143
|
-
# @param obj [Native] JavaScript object
|
|
1144
|
-
# @return [Native] Array of values
|
|
1145
|
-
def js_values(obj)
|
|
1146
|
-
`Object.values(#{obj})`
|
|
1147
|
-
end
|
|
1148
|
-
|
|
1149
|
-
# Get object entries
|
|
1150
|
-
# @param obj [Native] JavaScript object
|
|
1151
|
-
# @return [Native] Array of [key, value] pairs
|
|
1152
|
-
def js_entries(obj)
|
|
1153
|
-
`Object.entries(#{obj})`
|
|
1154
|
-
end
|
|
1155
|
-
|
|
1156
|
-
# Create Set from array and get size
|
|
1157
|
-
# @param arr [Native] JavaScript array
|
|
1158
|
-
# @return [Integer] Number of unique elements
|
|
1159
|
-
def js_unique_count(arr)
|
|
1160
|
-
`new Set(#{arr}).size`
|
|
1161
|
-
end
|
|
1162
|
-
|
|
1163
|
-
# ===== Fetch API =====
|
|
1164
|
-
|
|
1165
|
-
# Simple fetch that returns Promise-wrapped response
|
|
1166
|
-
# @param url [String] URL to fetch
|
|
1167
|
-
# @return [Native] Promise
|
|
1168
|
-
def js_fetch(url)
|
|
1169
|
-
`fetch(#{url})`
|
|
1170
|
-
end
|
|
1171
|
-
|
|
1172
|
-
# Fetch JSON from URL with callback
|
|
1173
|
-
# @param url [String] URL to fetch
|
|
1174
|
-
# @yield [data] Block to handle response data
|
|
1175
|
-
def fetch_json(url, &success_block)
|
|
1176
|
-
`
|
|
1177
|
-
fetch(#{url})
|
|
1178
|
-
.then(response => response.json())
|
|
1179
|
-
.then(data => #{success_block}.$call(data))
|
|
1180
|
-
.catch(error => console.error('Fetch error:', error))
|
|
1181
|
-
`
|
|
1182
|
-
end
|
|
1183
|
-
|
|
1184
|
-
# Fetch JSON from URL returning a Promise (for chaining)
|
|
1185
|
-
# @param url [String] URL to fetch
|
|
1186
|
-
# @return [Native] Promise that resolves to JSON data
|
|
1187
|
-
def fetch_json_promise(url)
|
|
1188
|
-
`fetch(#{url}).then(response => response.json())`
|
|
1189
|
-
end
|
|
1190
|
-
|
|
1191
|
-
# Fetch JSON with response validation
|
|
1192
|
-
# @param url [String] URL to fetch
|
|
1193
|
-
# @return [Native] Promise that resolves to JSON data or rejects on error
|
|
1194
|
-
def fetch_json_safe(url)
|
|
1195
|
-
`fetch(#{url}).then(function(response) { if (!response.ok) { throw new Error('Network response was not ok: ' + response.status); } return response.json(); })`
|
|
1196
|
-
end
|
|
1197
|
-
|
|
1198
|
-
# Fetch multiple URLs in parallel and get JSON results
|
|
1199
|
-
# @param urls [Array<String>] URLs to fetch
|
|
1200
|
-
# @return [Native] Promise that resolves to array of JSON results
|
|
1201
|
-
def fetch_all_json(urls)
|
|
1202
|
-
promises = urls.map { |url| fetch_json_promise(url) }
|
|
1203
|
-
`Promise.all(#{promises})`
|
|
1204
|
-
end
|
|
1205
|
-
|
|
1206
|
-
# Fetch JSON with success and error callbacks
|
|
1207
|
-
# @param url [String] URL to fetch
|
|
1208
|
-
# @param on_success [Proc] Success callback receiving data
|
|
1209
|
-
# @param on_error [Proc] Error callback receiving error
|
|
1210
|
-
def fetch_json_with_handlers(url, on_success:, on_error: nil)
|
|
1211
|
-
promise = fetch_json_safe(url)
|
|
1212
|
-
promise = js_then(promise) { |data| on_success.call(data) }
|
|
1213
|
-
if on_error
|
|
1214
|
-
js_catch(promise) { |error| on_error.call(error) }
|
|
1215
|
-
else
|
|
1216
|
-
js_catch(promise) { |error| console_error('Fetch error:', error) }
|
|
1217
|
-
end
|
|
1218
|
-
end
|
|
1219
|
-
|
|
1220
|
-
# ===== Promise Methods =====
|
|
1221
|
-
|
|
1222
|
-
# Create Promise.all from array of promises
|
|
1223
|
-
# @param promises [Array<Native>] Array of promises
|
|
1224
|
-
# @return [Native] Promise that resolves when all complete
|
|
1225
|
-
def promise_all(promises)
|
|
1226
|
-
`Promise.all(#{promises})`
|
|
1227
|
-
end
|
|
1228
|
-
|
|
1229
|
-
# Create Promise.race from array of promises
|
|
1230
|
-
# @param promises [Array<Native>] Array of promises
|
|
1231
|
-
# @return [Native] Promise that resolves when first completes
|
|
1232
|
-
def promise_race(promises)
|
|
1233
|
-
`Promise.race(#{promises})`
|
|
1234
|
-
end
|
|
1235
|
-
|
|
1236
|
-
# Create a resolved Promise with value
|
|
1237
|
-
# @param value [Object] Value to resolve with
|
|
1238
|
-
# @return [Native] Resolved Promise
|
|
1239
|
-
def promise_resolve(value)
|
|
1240
|
-
native_value = value.respond_to?(:to_n) ? value.to_n : value
|
|
1241
|
-
`Promise.resolve(#{native_value})`
|
|
1242
|
-
end
|
|
1243
|
-
|
|
1244
|
-
# Create a rejected Promise with error
|
|
1245
|
-
# @param error [Object] Error to reject with
|
|
1246
|
-
# @return [Native] Rejected Promise
|
|
1247
|
-
def promise_reject(error)
|
|
1248
|
-
`Promise.reject(#{error})`
|
|
1249
|
-
end
|
|
1250
|
-
|
|
1251
|
-
# Add then handler to promise
|
|
1252
|
-
# @param promise [Native] JavaScript Promise
|
|
1253
|
-
# @yield [value] Block to handle resolved value
|
|
1254
|
-
# @return [Native] New Promise
|
|
1255
|
-
def js_then(promise, &block)
|
|
1256
|
-
`#{promise}.then(#{block})`
|
|
1257
|
-
end
|
|
1258
|
-
|
|
1259
|
-
# Add catch handler to promise
|
|
1260
|
-
# @param promise [Native] JavaScript Promise
|
|
1261
|
-
# @yield [error] Block to handle rejection
|
|
1262
|
-
# @return [Native] New Promise
|
|
1263
|
-
def js_catch(promise, &block)
|
|
1264
|
-
`#{promise}.catch(#{block})`
|
|
1265
|
-
end
|
|
1266
|
-
|
|
1267
|
-
# Add finally handler to promise
|
|
1268
|
-
# @param promise [Native] JavaScript Promise
|
|
1269
|
-
# @yield Block to execute regardless of outcome
|
|
1270
|
-
# @return [Native] New Promise
|
|
1271
|
-
def js_finally(promise, &block)
|
|
1272
|
-
`#{promise}.finally(#{block})`
|
|
1273
|
-
end
|
|
1274
|
-
|
|
1275
|
-
private
|
|
1276
|
-
|
|
1277
|
-
# Convert snake_case to camelCase, preserving existing camelCase
|
|
1278
|
-
# @param name [Symbol, String] The name to convert
|
|
1279
|
-
# @param capitalize_first [Boolean] Whether to capitalize first letter
|
|
1280
|
-
# @return [String] camelCase string
|
|
1281
|
-
def camelize(name, capitalize_first = true)
|
|
1282
|
-
str = name.to_s
|
|
1283
|
-
|
|
1284
|
-
# If no underscores, assume already camelCase - just adjust first letter
|
|
1285
|
-
unless str.include?('_')
|
|
1286
|
-
if capitalize_first
|
|
1287
|
-
return str[0].upcase + str[1..-1].to_s
|
|
1288
|
-
else
|
|
1289
|
-
return str[0].downcase + str[1..-1].to_s
|
|
1290
|
-
end
|
|
1291
|
-
end
|
|
1292
|
-
|
|
1293
|
-
# Convert snake_case to camelCase
|
|
1294
|
-
parts = str.split('_')
|
|
1295
|
-
if capitalize_first
|
|
1296
|
-
parts.map(&:capitalize).join
|
|
1297
|
-
else
|
|
1298
|
-
([parts.first] + parts[1..-1].map(&:capitalize)).join
|
|
1299
|
-
end
|
|
1300
|
-
end
|
|
1301
|
-
end
|
|
10
|
+
StimulusHelpers = V1::StimulusHelpers
|
|
1302
11
|
end
|
|
1303
12
|
end
|
|
1304
|
-
|
|
1305
|
-
# Alias for backward compatibility
|
|
1306
|
-
StimulusHelpers = OpalVite::Concerns::StimulusHelpers
|