opal-vite 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b141645cbef98169c750427f39d902e6f8377430fb41ec282ccfdaf9ff6bc5a5
4
- data.tar.gz: ec0717edcaf168380c5bd92419d84d2e14f12d41430a373d45c7f095109d2b82
3
+ metadata.gz: d4bc96b96e9f545db30dd5ca2d6b0fb0aea32f1a03e9a733c6e3c802dc616506
4
+ data.tar.gz: d753caf10cfca04a2709bfe446f3dde148eafc3da279d5fbb38797ad17b000da
5
5
  SHA512:
6
- metadata.gz: e27461464983db947b29e81c64da1ef4d5872c45709e66e078c315dc9133c15ec453b88d596788a5321a235e604da1c7c7f1eefb87240574566da709d933cbbb
7
- data.tar.gz: 8d70ec17da3cb04a529e55640aec9e8ed6efc6bc1acdf4aad0ac93949b83593820a18d624c86db11df764b3c5481cc2228828617c71e8694005dc4e0abead6f2
6
+ metadata.gz: 7a660f33fceef3720e67a7530cbda406c6218978557056cefc782e7dce014e0c73784ea46397091cfff1a130e2d0edc9003f051694f88f52c4f0f568748cba7c
7
+ data.tar.gz: 017e824fd2eefd1ecc9cdae4dd7677bd74e6dd14082a0c5fc755f22f7cf879d61d0a519e55988f02d93bdf1d74aeb37ffdc5f8822f88d0a3e99b1d6cf229f047
@@ -9,6 +9,7 @@ module Opal
9
9
  def initialize(options = {})
10
10
  @options = options
11
11
  @config = options[:config] || Opal::Vite.config
12
+ @include_concerns = options.fetch(:include_concerns, true)
12
13
  end
13
14
 
14
15
  # Compile Ruby source code to JavaScript
@@ -27,6 +28,9 @@ module Opal
27
28
  parent_dir = File.dirname(file_dir)
28
29
  builder.append_paths(parent_dir)
29
30
 
31
+ # Add gem paths from $LOAD_PATH so Opal can find gems
32
+ add_gem_paths(builder)
33
+
30
34
  builder.build_str(source, file_path)
31
35
 
32
36
  result = {
@@ -72,6 +76,62 @@ module Opal
72
76
 
73
77
  private
74
78
 
79
+ def add_gem_paths(builder)
80
+ # Add gem directories from $LOAD_PATH to Opal's load paths
81
+ # This allows Opal to find and compile gems like inesita
82
+
83
+ # Add opal-vite's built-in concerns if enabled
84
+ if @include_concerns
85
+ opal_vite_opal_path = Opal::Vite.opal_lib_path
86
+ if File.directory?(opal_vite_opal_path)
87
+ builder.append_paths(opal_vite_opal_path) unless builder.path_reader.paths.include?(opal_vite_opal_path)
88
+ end
89
+ end
90
+
91
+ # First collect all gem paths
92
+ gem_lib_paths = []
93
+ gem_opal_paths = []
94
+
95
+ $LOAD_PATH.each do |path|
96
+ # Only process gem directories
97
+ if path.include?('/gems/') && File.directory?(path)
98
+ gem_lib_paths << path
99
+
100
+ # Check if gem has an opal directory (for gems like inesita)
101
+ # Gems with opal-specific code often have an 'opal' directory sibling to 'lib'
102
+ gem_root = File.dirname(path)
103
+ opal_path = File.join(gem_root, 'opal')
104
+ if File.directory?(opal_path)
105
+ gem_opal_paths << opal_path
106
+ end
107
+ end
108
+ end
109
+
110
+ # Add opal directories FIRST so they take priority over lib directories
111
+ # This ensures that 'require "inesita"' finds opal/inesita.rb before lib/inesita.rb
112
+ gem_opal_paths.uniq.each do |path|
113
+ builder.append_paths(path) unless builder.path_reader.paths.include?(path)
114
+ end
115
+
116
+ # Then add regular lib directories, but ONLY for Opal-compatible gems
117
+ # to avoid pulling in Rails/server-side dependencies
118
+ gem_lib_paths.uniq.each do |path|
119
+ # Only add paths from gems that:
120
+ # 1. Have an 'opal' directory (already processed above)
121
+ # 2. OR have 'opal' in their gem name
122
+ gem_root = File.dirname(path)
123
+ gem_name = File.basename(gem_root)
124
+
125
+ # Check if this gem has opal support
126
+ is_opal_gem = gem_name.start_with?('opal') ||
127
+ gem_opal_paths.any? { |opal_path| opal_path.start_with?(gem_root) }
128
+
129
+ if is_opal_gem
130
+ builder.append_paths(path) unless builder.path_reader.paths.include?(path)
131
+ end
132
+ end
133
+ end
134
+
75
135
  def compiler_options
76
136
  @config.to_compiler_options
77
137
  end
@@ -1,5 +1,5 @@
1
1
  module Opal
2
2
  module Vite
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -0,0 +1,89 @@
1
+ # backtick_javascript: true
2
+
3
+ module OpalVite
4
+ module Concerns
5
+ # DomHelpers concern - provides common DOM manipulation methods
6
+ module DomHelpers
7
+ # Create a custom event and dispatch it on a target
8
+ def dispatch_custom_event(event_name, detail = {}, target = nil)
9
+ target ||= window
10
+ `
11
+ const event = new CustomEvent(#{event_name}, {
12
+ detail: #{detail.to_n}
13
+ });
14
+ #{target.to_n}.dispatchEvent(event);
15
+ `
16
+ end
17
+
18
+ # Create a standard event
19
+ def create_event(event_type, options = { bubbles: true })
20
+ `new Event(#{event_type}, #{options.to_n})`
21
+ end
22
+
23
+ # Query selector shorthand on element
24
+ def query(selector)
25
+ element.query_selector(selector)
26
+ end
27
+
28
+ # Query selector all shorthand on element
29
+ def query_all(selector)
30
+ element.query_selector_all(selector)
31
+ end
32
+
33
+ # Add CSS class to element
34
+ def add_class(el, class_name)
35
+ el.class_list.add(class_name)
36
+ end
37
+
38
+ # Remove CSS class from element
39
+ def remove_class(el, class_name)
40
+ el.class_list.remove(class_name)
41
+ end
42
+
43
+ # Toggle CSS class on element
44
+ def toggle_class(el, class_name)
45
+ el.class_list.toggle(class_name)
46
+ end
47
+
48
+ # Check if element has CSS class
49
+ def has_class?(el, class_name)
50
+ el.class_list.contains(class_name)
51
+ end
52
+
53
+ # Set timeout helper
54
+ def set_timeout(delay_ms, &block)
55
+ window.set_timeout(block, delay_ms)
56
+ end
57
+
58
+ # Check if element exists (not null)
59
+ def element_exists?(el)
60
+ !el.nil? && el.to_n
61
+ end
62
+
63
+ # Set style property on element
64
+ def set_style(el, property, value)
65
+ return unless element_exists?(el)
66
+ `#{el.to_n}.style[#{property}] = #{value}`
67
+ end
68
+
69
+ # Get style property from element
70
+ def get_style(el, property)
71
+ return nil unless element_exists?(el)
72
+ `#{el.to_n}.style[#{property}]`
73
+ end
74
+
75
+ # Show element (set display to block)
76
+ def show_element(el)
77
+ set_style(el, 'display', 'block')
78
+ end
79
+
80
+ # Hide element (set display to none)
81
+ def hide_element(el)
82
+ set_style(el, 'display', 'none')
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # Alias for backward compatibility
89
+ DomHelpers = OpalVite::Concerns::DomHelpers
@@ -0,0 +1,414 @@
1
+ # backtick_javascript: true
2
+
3
+ module OpalVite
4
+ module Concerns
5
+ # JS::ProxyEx - Extended JS proxy utilities for Ruby-like JavaScript interop
6
+ #
7
+ # This module provides Ruby-friendly wrappers around JavaScript objects and APIs
8
+ # that opal_stimulus's JS::Proxy doesn't fully cover yet.
9
+ #
10
+ # Usage:
11
+ # include OpalVite::Concerns::JsProxyEx
12
+ #
13
+ # # Global objects
14
+ # local_storage.get_item('key')
15
+ # json.parse(data)
16
+ #
17
+ # # Object creation
18
+ # new_event('click', bubbles: true)
19
+ # new_url('https://example.com')
20
+ # new_regexp('[a-z]+')
21
+ #
22
+ # # Event listeners with proper block handling
23
+ # window.add_event_listener('click') { |event| ... }
24
+ #
25
+ module JsProxyEx
26
+ # ============================================
27
+ # Global JavaScript Objects
28
+ # ============================================
29
+
30
+ def local_storage
31
+ @local_storage ||= JsObject.new(`localStorage`)
32
+ end
33
+
34
+ def session_storage
35
+ @session_storage ||= JsObject.new(`sessionStorage`)
36
+ end
37
+
38
+ def js_json
39
+ @js_json ||= JsonWrapper.new
40
+ end
41
+
42
+ def js_date
43
+ DateWrapper
44
+ end
45
+
46
+ def js_console
47
+ @js_console ||= JsObject.new(`console`)
48
+ end
49
+
50
+ # ============================================
51
+ # Object Creation Helpers
52
+ # ============================================
53
+
54
+ def new_event(type, options = {})
55
+ JsObject.new(`new Event(#{type}, #{options.to_n})`)
56
+ end
57
+
58
+ def new_custom_event(type, detail = {})
59
+ JsObject.new(`new CustomEvent(#{type}, { detail: #{detail.to_n} })`)
60
+ end
61
+
62
+ def new_url(url_string)
63
+ JsObject.new(`new URL(#{url_string})`)
64
+ end
65
+
66
+ def new_regexp(pattern, flags = '')
67
+ RegExpWrapper.new(pattern, flags)
68
+ end
69
+
70
+ def new_date(value = nil)
71
+ if value
72
+ JsObject.new(`new Date(#{value})`)
73
+ else
74
+ JsObject.new(`new Date()`)
75
+ end
76
+ end
77
+
78
+ def date_now
79
+ `Date.now()`
80
+ end
81
+
82
+ # ============================================
83
+ # Wrap existing JS::Proxy objects
84
+ # ============================================
85
+
86
+ # Wrap a JS::Proxy or native object in JsObject for enhanced access
87
+ def wrap_js(obj)
88
+ return nil if obj.nil?
89
+ native = obj.respond_to?(:to_n) ? obj.to_n : obj
90
+ JsObject.new(native)
91
+ end
92
+
93
+ # ============================================
94
+ # Array/Object utilities
95
+ # ============================================
96
+
97
+ # Convert JS array to Ruby array with JsObject wrapping
98
+ def js_array_to_ruby(js_array)
99
+ result = []
100
+ length = `#{js_array}.length`
101
+ length.times do |i|
102
+ item = `#{js_array}[#{i}]`
103
+ result << JsObject.new(item)
104
+ end
105
+ result
106
+ end
107
+
108
+ # Create empty JS array
109
+ def new_js_array
110
+ JsObject.new(`[]`)
111
+ end
112
+
113
+ # ============================================
114
+ # JsObject - Wrapper class with method_missing
115
+ # ============================================
116
+
117
+ class JsObject
118
+ def initialize(native)
119
+ @native = native
120
+ end
121
+
122
+ def to_n
123
+ @native
124
+ end
125
+
126
+ # Access properties with [] - returns wrapped JsObject
127
+ def [](key)
128
+ key_str = key.to_s
129
+ # Convert snake_case to camelCase for property access
130
+ camel_key = snake_to_camel(key_str)
131
+ result = `#{@native}[#{camel_key}]`
132
+ wrap_result(result)
133
+ end
134
+
135
+ # Set properties with []=
136
+ def []=(key, value)
137
+ key_str = key.to_s
138
+ camel_key = snake_to_camel(key_str)
139
+ native_value = value.respond_to?(:to_n) ? value.to_n : value
140
+ `#{@native}[#{camel_key}] = #{native_value}`
141
+ end
142
+
143
+ # Method missing for snake_case -> camelCase conversion
144
+ def method_missing(name, *args, &block)
145
+ name_str = name.to_s
146
+
147
+ # Handle setters (e.g., text_content=)
148
+ if name_str.end_with?('=')
149
+ prop_name = name_str[0..-2]
150
+ camel_name = snake_to_camel(prop_name)
151
+ value = args[0]
152
+ native_value = value.respond_to?(:to_n) ? value.to_n : value
153
+ `#{@native}[#{camel_name}] = #{native_value}`
154
+ return value
155
+ end
156
+
157
+ # Handle predicates (e.g., has_attribute?)
158
+ if name_str.end_with?('?')
159
+ prop_name = name_str[0..-2]
160
+ camel_name = snake_to_camel(prop_name)
161
+ return !!`#{@native}[#{camel_name}]`
162
+ end
163
+
164
+ camel_name = snake_to_camel(name_str)
165
+
166
+ # Check if it's a function
167
+ is_function = `typeof #{@native}[#{camel_name}] === 'function'`
168
+
169
+ if is_function
170
+ # Handle event listeners specially
171
+ if camel_name == 'addEventListener' && block
172
+ native_callback = ->(event) { block.call(JsObject.new(event)) }
173
+ `#{@native}.addEventListener(#{args[0]}, #{native_callback})`
174
+ return nil
175
+ end
176
+
177
+ # Convert args to native
178
+ native_args = args.map { |a| a.respond_to?(:to_n) ? a.to_n : a }
179
+
180
+ # If block provided, wrap it
181
+ if block
182
+ native_callback = ->(*cb_args) {
183
+ wrapped_args = cb_args.map { |a| wrap_result(a) }
184
+ block.call(*wrapped_args)
185
+ }
186
+ native_args << native_callback
187
+ end
188
+
189
+ result = case native_args.length
190
+ when 0 then `#{@native}[#{camel_name}]()`
191
+ when 1 then `#{@native}[#{camel_name}](#{native_args[0]})`
192
+ when 2 then `#{@native}[#{camel_name}](#{native_args[0]}, #{native_args[1]})`
193
+ when 3 then `#{@native}[#{camel_name}](#{native_args[0]}, #{native_args[1]}, #{native_args[2]})`
194
+ else
195
+ # For more args, use apply
196
+ `#{@native}[#{camel_name}].apply(#{@native}, #{native_args})`
197
+ end
198
+
199
+ wrap_result(result)
200
+ else
201
+ # Property access
202
+ result = `#{@native}[#{camel_name}]`
203
+ wrap_result(result)
204
+ end
205
+ end
206
+
207
+ def respond_to_missing?(name, include_private = false)
208
+ true
209
+ end
210
+
211
+ # Enumerable support - iterate over array-like objects
212
+ def each(&block)
213
+ return enum_for(:each) unless block
214
+ length = `#{@native}.length`
215
+ return self unless length
216
+ length.times do |i|
217
+ item = `#{@native}[#{i}]`
218
+ block.call(wrap_result(item))
219
+ end
220
+ self
221
+ end
222
+
223
+ def length
224
+ `#{@native}.length`
225
+ end
226
+
227
+ def size
228
+ length
229
+ end
230
+
231
+ # Check for null/undefined
232
+ def nil?
233
+ `#{@native} == null`
234
+ end
235
+
236
+ def exists?
237
+ !nil?
238
+ end
239
+
240
+ # String conversion
241
+ def to_s
242
+ `String(#{@native})`
243
+ end
244
+
245
+ def inspect
246
+ "#<JsObject: #{to_s}>"
247
+ end
248
+
249
+ private
250
+
251
+ def snake_to_camel(str)
252
+ # Handle special cases first
253
+ return str if str == str.upcase # ALL_CAPS stays as is
254
+
255
+ parts = str.split('_')
256
+ return str if parts.length == 1
257
+
258
+ # First part stays lowercase, rest get capitalized
259
+ parts[0] + parts[1..-1].map(&:capitalize).join
260
+ end
261
+
262
+ def wrap_result(result)
263
+ return nil if `#{result} === null || #{result} === undefined`
264
+ return result if `typeof #{result} === 'number'`
265
+ return result if `typeof #{result} === 'string'`
266
+ return result if `typeof #{result} === 'boolean'`
267
+ JsObject.new(result)
268
+ end
269
+ end
270
+
271
+ # ============================================
272
+ # JsonWrapper - Ruby-like JSON API
273
+ # ============================================
274
+
275
+ class JsonWrapper
276
+ def parse(json_string)
277
+ result = `JSON.parse(#{json_string})`
278
+ JsObject.new(result)
279
+ end
280
+
281
+ def stringify(data)
282
+ native_data = data.respond_to?(:to_n) ? data.to_n : data
283
+ `JSON.stringify(#{native_data})`
284
+ end
285
+ end
286
+
287
+ # ============================================
288
+ # RegExpWrapper - Ruby-like RegExp API
289
+ # ============================================
290
+
291
+ class RegExpWrapper
292
+ def initialize(pattern, flags = '')
293
+ @native = `new RegExp(#{pattern}, #{flags})`
294
+ end
295
+
296
+ def to_n
297
+ @native
298
+ end
299
+
300
+ def test(string)
301
+ `#{@native}.test(#{string})`
302
+ end
303
+
304
+ def match(string)
305
+ result = `#{string}.match(#{@native})`
306
+ return nil if `#{result} === null`
307
+ JsObject.new(result)
308
+ end
309
+
310
+ def =~(string)
311
+ test(string)
312
+ end
313
+ end
314
+
315
+ # ============================================
316
+ # DateWrapper - Static methods for Date
317
+ # ============================================
318
+
319
+ module DateWrapper
320
+ def self.now
321
+ `Date.now()`
322
+ end
323
+
324
+ def self.new(value = nil)
325
+ if value
326
+ JsObject.new(`new Date(#{value})`)
327
+ else
328
+ JsObject.new(`new Date()`)
329
+ end
330
+ end
331
+
332
+ def self.parse(date_string)
333
+ `Date.parse(#{date_string})`
334
+ end
335
+ end
336
+
337
+ # ============================================
338
+ # Stimulus Target Helpers
339
+ # For accessing Stimulus targets with snake_case
340
+ # ============================================
341
+
342
+ # Check if target exists: has_target?(:submit_btn)
343
+ def has_target?(target_name)
344
+ camel_name = snake_to_camel(target_name.to_s)
345
+ has_method = "has#{camel_name[0].upcase}#{camel_name[1..-1]}Target"
346
+ `this[#{has_method}]`
347
+ end
348
+
349
+ # Get target element: target(:submit_btn)
350
+ def target(target_name)
351
+ camel_name = snake_to_camel(target_name.to_s)
352
+ target_method = "#{camel_name}Target"
353
+ result = `this[#{target_method}]`
354
+ JsObject.new(result)
355
+ end
356
+
357
+ # Get all targets: targets(:field)
358
+ def targets(target_name)
359
+ camel_name = snake_to_camel(target_name.to_s)
360
+ targets_method = "#{camel_name}Targets"
361
+ result = `this[#{targets_method}]`
362
+ js_array_to_ruby(result)
363
+ end
364
+
365
+ # Update target text content: set_target_text(:status, "Hello")
366
+ def set_target_text(target_name, text)
367
+ camel_name = snake_to_camel(target_name.to_s)
368
+ has_method = "has#{camel_name[0].upcase}#{camel_name[1..-1]}Target"
369
+ target_method = "#{camel_name}Target"
370
+ `
371
+ if (this[#{has_method}]) {
372
+ this[#{target_method}].textContent = #{text};
373
+ }
374
+ `
375
+ end
376
+
377
+ # Update target class: set_target_class(:status, "form-status error")
378
+ def set_target_class(target_name, class_name)
379
+ camel_name = snake_to_camel(target_name.to_s)
380
+ has_method = "has#{camel_name[0].upcase}#{camel_name[1..-1]}Target"
381
+ target_method = "#{camel_name}Target"
382
+ `
383
+ if (this[#{has_method}]) {
384
+ this[#{target_method}].className = #{class_name};
385
+ }
386
+ `
387
+ end
388
+
389
+ # Set target disabled state: set_target_disabled(:submit_btn, true)
390
+ def set_target_disabled(target_name, disabled)
391
+ camel_name = snake_to_camel(target_name.to_s)
392
+ has_method = "has#{camel_name[0].upcase}#{camel_name[1..-1]}Target"
393
+ target_method = "#{camel_name}Target"
394
+ `
395
+ if (this[#{has_method}]) {
396
+ this[#{target_method}].disabled = #{disabled};
397
+ }
398
+ `
399
+ end
400
+
401
+ private
402
+
403
+ def snake_to_camel(str)
404
+ return str if str == str.upcase # ALL_CAPS stays as is
405
+ parts = str.to_s.split('_')
406
+ return str if parts.length == 1
407
+ parts[0] + parts[1..-1].map(&:capitalize).join
408
+ end
409
+ end
410
+ end
411
+ end
412
+
413
+ # Alias for backward compatibility
414
+ JsProxyEx = OpalVite::Concerns::JsProxyEx
@@ -0,0 +1,386 @@
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
+ # ===== Utility Methods =====
343
+
344
+ # Generate a unique ID based on timestamp
345
+ # @return [Integer] Unique ID
346
+ def generate_id
347
+ js_timestamp
348
+ end
349
+
350
+ # Focus a target element
351
+ # @param name [Symbol, String] Target name
352
+ def target_focus(name)
353
+ return unless has_target?(name)
354
+ method_name = "#{camelize(name, false)}Target"
355
+ `this[#{method_name}].focus()`
356
+ end
357
+
358
+ # Blur (unfocus) a target element
359
+ # @param name [Symbol, String] Target name
360
+ def target_blur(name)
361
+ return unless has_target?(name)
362
+ method_name = "#{camelize(name, false)}Target"
363
+ `this[#{method_name}].blur()`
364
+ end
365
+
366
+ private
367
+
368
+ # Convert snake_case to camelCase
369
+ # @param name [Symbol, String] The name to convert
370
+ # @param capitalize_first [Boolean] Whether to capitalize first letter
371
+ # @return [String] camelCase string
372
+ def camelize(name, capitalize_first = true)
373
+ str = name.to_s
374
+ parts = str.split('_')
375
+ if capitalize_first
376
+ parts.map(&:capitalize).join
377
+ else
378
+ ([parts.first] + parts[1..-1].map(&:capitalize)).join
379
+ end
380
+ end
381
+ end
382
+ end
383
+ end
384
+
385
+ # Alias for backward compatibility
386
+ StimulusHelpers = OpalVite::Concerns::StimulusHelpers
@@ -0,0 +1,31 @@
1
+ # backtick_javascript: true
2
+
3
+ module OpalVite
4
+ module Concerns
5
+ # Storable concern - provides LocalStorage functionality
6
+ module Storable
7
+ def storage_get(key)
8
+ stored = `localStorage.getItem(#{key})`
9
+ return nil unless stored
10
+
11
+ begin
12
+ `JSON.parse(stored)`
13
+ rescue
14
+ nil
15
+ end
16
+ end
17
+
18
+ def storage_set(key, data)
19
+ json = `JSON.stringify(#{data.to_n})`
20
+ `localStorage.setItem(#{key}, json)`
21
+ end
22
+
23
+ def storage_remove(key)
24
+ `localStorage.removeItem(#{key})`
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ # Alias for backward compatibility
31
+ Storable = OpalVite::Concerns::Storable
@@ -0,0 +1,36 @@
1
+ # backtick_javascript: true
2
+
3
+ module OpalVite
4
+ module Concerns
5
+ # Toastable concern - provides toast notification functionality
6
+ module Toastable
7
+ def dispatch_toast(message, type = 'info')
8
+ `
9
+ const event = new CustomEvent('show-toast', {
10
+ detail: { message: #{message}, type: #{type} }
11
+ });
12
+ window.dispatchEvent(event);
13
+ `
14
+ end
15
+
16
+ def show_success(message)
17
+ dispatch_toast(message, 'success')
18
+ end
19
+
20
+ def show_error(message)
21
+ dispatch_toast(message, 'error')
22
+ end
23
+
24
+ def show_warning(message)
25
+ dispatch_toast(message, 'warning')
26
+ end
27
+
28
+ def show_info(message)
29
+ dispatch_toast(message, 'info')
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ # Alias for backward compatibility
36
+ Toastable = OpalVite::Concerns::Toastable
@@ -0,0 +1,6 @@
1
+ # Load all OpalVite concerns
2
+ require 'opal_vite/concerns/js_proxy_ex'
3
+ require 'opal_vite/concerns/dom_helpers'
4
+ require 'opal_vite/concerns/toastable'
5
+ require 'opal_vite/concerns/storable'
6
+ require 'opal_vite/concerns/stimulus_helpers'
data/opal/opal_vite.rb ADDED
@@ -0,0 +1,16 @@
1
+ # OpalVite - Opal + Vite integration library
2
+ #
3
+ # Usage:
4
+ # require 'opal_vite'
5
+ # require 'opal_vite/concerns'
6
+ #
7
+ # Or require individual concerns:
8
+ # require 'opal_vite/concerns/js_proxy_ex'
9
+ # require 'opal_vite/concerns/dom_helpers'
10
+ # require 'opal_vite/concerns/toastable'
11
+ # require 'opal_vite/concerns/storable'
12
+
13
+ module OpalVite
14
+ module Concerns
15
+ end
16
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opal-vite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - stofu1234
@@ -110,6 +110,13 @@ files:
110
110
  - lib/opal/vite/config.rb
111
111
  - lib/opal/vite/source_map.rb
112
112
  - lib/opal/vite/version.rb
113
+ - opal/opal_vite.rb
114
+ - opal/opal_vite/concerns.rb
115
+ - opal/opal_vite/concerns/dom_helpers.rb
116
+ - opal/opal_vite/concerns/js_proxy_ex.rb
117
+ - opal/opal_vite/concerns/stimulus_helpers.rb
118
+ - opal/opal_vite/concerns/storable.rb
119
+ - opal/opal_vite/concerns/toastable.rb
113
120
  homepage: https://github.com/stofu1234/opal-vite
114
121
  licenses:
115
122
  - MIT