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 +4 -4
- data/lib/opal/vite/compiler.rb +60 -0
- data/lib/opal/vite/version.rb +1 -1
- data/opal/opal_vite/concerns/dom_helpers.rb +89 -0
- data/opal/opal_vite/concerns/js_proxy_ex.rb +414 -0
- data/opal/opal_vite/concerns/stimulus_helpers.rb +386 -0
- data/opal/opal_vite/concerns/storable.rb +31 -0
- data/opal/opal_vite/concerns/toastable.rb +36 -0
- data/opal/opal_vite/concerns.rb +6 -0
- data/opal/opal_vite.rb +16 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4bc96b96e9f545db30dd5ca2d6b0fb0aea32f1a03e9a733c6e3c802dc616506
|
|
4
|
+
data.tar.gz: d753caf10cfca04a2709bfe446f3dde148eafc3da279d5fbb38797ad17b000da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a660f33fceef3720e67a7530cbda406c6218978557056cefc782e7dce014e0c73784ea46397091cfff1a130e2d0edc9003f051694f88f52c4f0f568748cba7c
|
|
7
|
+
data.tar.gz: 017e824fd2eefd1ecc9cdae4dd7677bd74e6dd14082a0c5fc755f22f7cf879d61d0a519e55988f02d93bdf1d74aeb37ffdc5f8822f88d0a3e99b1d6cf229f047
|
data/lib/opal/vite/compiler.rb
CHANGED
|
@@ -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
|
data/lib/opal/vite/version.rb
CHANGED
|
@@ -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
|
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.
|
|
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
|