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