opal-vite 0.2.7 → 0.2.8

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,7 @@
1
1
  # backtick_javascript: true
2
+ # Backward compatibility wrapper - delegates to v1
3
+ require 'opal_vite/concerns/v1/storable'
2
4
 
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
5
+ `console.warn("[DEPRECATION] require 'opal_vite/concerns/storable' is deprecated. Please use require 'opal_vite/concerns/v1/storable' instead.")`
10
6
 
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
7
+ # Module is already defined by v1, re-exported for backward compatibility
@@ -1,36 +1,7 @@
1
1
  # backtick_javascript: true
2
+ # Backward compatibility wrapper - delegates to v1
3
+ require 'opal_vite/concerns/v1/toastable'
2
4
 
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
5
+ `console.warn("[DEPRECATION] require 'opal_vite/concerns/toastable' is deprecated. Please use require 'opal_vite/concerns/v1/toastable' instead.")`
15
6
 
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
7
+ # Module is already defined by v1, re-exported for backward compatibility
@@ -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