opal-vite 0.3.19 → 0.4.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/cli.rb +15 -6
- data/lib/opal/vite/compiler.rb +20 -3
- data/lib/opal/vite/config.rb +24 -6
- data/lib/opal/vite/testing/stable_helpers.rb +5 -4
- data/lib/opal/vite/version.rb +1 -1
- data/opal/opal_vite/concerns/v1/action_cable_helpers.rb +8 -1
- data/opal/opal_vite/concerns/v1/base64_helpers.rb +12 -1
- data/opal/opal_vite/concerns/v1/stimulus_helpers.rb +45 -24
- data/opal/opal_vite/concerns/v1/turbo_helpers.rb +25 -11
- data/opal/opal_vite/concerns/v1/uri_helpers.rb +9 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba1cd330c7e4a9de6c8a3cefac5bebd1e4f61187708babc92bb15ea5bcffa896
|
|
4
|
+
data.tar.gz: dfd2b6ec75acfd368e36b66e1fc88628b047b90a008ac831991f9ef5f2a07bd1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d14e7be8d32dbf925fb7fd1c5f95272272cde0f3ee2ab027baff615d1f1ed80b01ef3d2ef1f3baa59c85060693ab44e5cf3de078316b1d9176a8cec9318397a8
|
|
7
|
+
data.tar.gz: d67ba35d806db20dee767796b82a7f4c1d68361404817665b6d17c4d3583e4c035463f97697932212fed583b164a457fc5c988f939db22fff4f6f3b02d2fa1fe
|
data/lib/opal/vite/cli.rb
CHANGED
|
@@ -29,6 +29,8 @@ module Opal
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
31
|
def compile_command
|
|
32
|
+
# Parse options first so they may appear before or after the file name
|
|
33
|
+
parse_compile_options
|
|
32
34
|
file_path = @argv.shift
|
|
33
35
|
|
|
34
36
|
unless file_path
|
|
@@ -42,12 +44,16 @@ module Opal
|
|
|
42
44
|
exit 1
|
|
43
45
|
end
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
# Without -o the compiled JS goes to stdout, so progress messages go to
|
|
48
|
+
# stderr to keep stdout a clean JS stream
|
|
49
|
+
log = @options[:output] ? $stdout : $stderr
|
|
46
50
|
|
|
47
51
|
begin
|
|
48
|
-
puts "Compiling #{file_path}..."
|
|
52
|
+
log.puts "Compiling #{file_path}..."
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
config = Opal::Vite::Config.new
|
|
55
|
+
config.source_map_enabled = !!@options[:source_map]
|
|
56
|
+
compiler = Opal::Vite::Compiler.new(config: config)
|
|
51
57
|
source = File.read(file_path)
|
|
52
58
|
result = compiler.compile(source, file_path)
|
|
53
59
|
|
|
@@ -66,13 +72,13 @@ module Opal
|
|
|
66
72
|
end
|
|
67
73
|
|
|
68
74
|
if @options[:verbose]
|
|
69
|
-
puts "\nDependencies:"
|
|
75
|
+
log.puts "\nDependencies:"
|
|
70
76
|
result[:dependencies].each do |dep|
|
|
71
|
-
puts " - #{dep}"
|
|
77
|
+
log.puts " - #{dep}"
|
|
72
78
|
end
|
|
73
79
|
end
|
|
74
80
|
|
|
75
|
-
puts "✅ Compilation successful!"
|
|
81
|
+
log.puts "✅ Compilation successful!"
|
|
76
82
|
rescue => e
|
|
77
83
|
puts "❌ Compilation failed: #{e.message}"
|
|
78
84
|
puts e.backtrace.first(5).join("\n") if @options[:verbose]
|
|
@@ -133,6 +139,9 @@ module Opal
|
|
|
133
139
|
@options[:verbose] = true
|
|
134
140
|
end
|
|
135
141
|
end.parse!(@argv)
|
|
142
|
+
rescue OptionParser::ParseError => e
|
|
143
|
+
puts "Error: #{e.message}"
|
|
144
|
+
exit 1
|
|
136
145
|
end
|
|
137
146
|
end
|
|
138
147
|
end
|
data/lib/opal/vite/compiler.rb
CHANGED
|
@@ -22,12 +22,18 @@ module Opal
|
|
|
22
22
|
@compiler_options = options.fetch(:compiler_options, {})
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Opal compiler options for the Builder. Options explicitly set through
|
|
26
|
+
# Opal::Vite.configure come first; options given to Compiler.new win.
|
|
27
|
+
def builder_compiler_options
|
|
28
|
+
@config.explicit_compiler_options.merge(@compiler_options)
|
|
29
|
+
end
|
|
30
|
+
|
|
25
31
|
# Compile Ruby source code to JavaScript
|
|
26
32
|
# Returns a hash with :code, :map, and :dependencies
|
|
27
33
|
def compile(source, file_path)
|
|
28
34
|
begin
|
|
29
35
|
# Use Opal::Builder and add the file's directory to load paths
|
|
30
|
-
builder = Opal::Builder.new(stubs: @stubs, compiler_options:
|
|
36
|
+
builder = Opal::Builder.new(stubs: @stubs, compiler_options: builder_compiler_options)
|
|
31
37
|
builder.prerequired = self.class.runtime_requires if @external_runtime
|
|
32
38
|
|
|
33
39
|
# Add the directory containing the file to load paths
|
|
@@ -65,7 +71,8 @@ module Opal
|
|
|
65
71
|
end
|
|
66
72
|
|
|
67
73
|
result
|
|
68
|
-
rescue StandardError => e
|
|
74
|
+
rescue StandardError, ScriptError => e
|
|
75
|
+
# ScriptError covers LoadError / Opal::MissingRequire and syntax errors
|
|
69
76
|
raise CompilationError, "Failed to compile #{file_path}: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
|
|
70
77
|
end
|
|
71
78
|
end
|
|
@@ -323,6 +330,7 @@ module Opal
|
|
|
323
330
|
section_abs_name = 0
|
|
324
331
|
|
|
325
332
|
first_segment_processed = false
|
|
333
|
+
name_bridged = false
|
|
326
334
|
|
|
327
335
|
lines.each do |line|
|
|
328
336
|
if line.empty?
|
|
@@ -364,12 +372,21 @@ module Opal
|
|
|
364
372
|
new_name_delta = global_abs_name - prev_name
|
|
365
373
|
|
|
366
374
|
new_values = [gen_col_delta, new_source_delta, new_orig_line_delta, new_orig_col_delta]
|
|
367
|
-
|
|
375
|
+
if values.length > 4
|
|
376
|
+
new_values << new_name_delta
|
|
377
|
+
name_bridged = true
|
|
378
|
+
end
|
|
368
379
|
|
|
369
380
|
first_segment_processed = true
|
|
370
381
|
else
|
|
371
382
|
# Subsequent segments: deltas are already correct (relative within section = relative within merged)
|
|
372
383
|
new_values = values.dup
|
|
384
|
+
if values.length > 4 && !name_bridged
|
|
385
|
+
# The first segment of this section had no name, so this is
|
|
386
|
+
# the section's first name: bridge from the merged name state
|
|
387
|
+
new_values[4] = global_abs_name - prev_name
|
|
388
|
+
name_bridged = true
|
|
389
|
+
end
|
|
373
390
|
end
|
|
374
391
|
|
|
375
392
|
result_segments << encode_vlq(new_values)
|
data/lib/opal/vite/config.rb
CHANGED
|
@@ -3,14 +3,22 @@ require 'json'
|
|
|
3
3
|
module Opal
|
|
4
4
|
module Vite
|
|
5
5
|
class Config
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
# Options passed on to Opal's compiler (see #explicit_compiler_options)
|
|
7
|
+
COMPILER_OPTION_KEYS = %i[arity_check freezing esm dynamic_require_severity missing_require_severity].freeze
|
|
8
|
+
|
|
9
|
+
attr_accessor :source_map_enabled
|
|
10
|
+
|
|
11
|
+
attr_reader(*COMPILER_OPTION_KEYS)
|
|
12
|
+
|
|
13
|
+
COMPILER_OPTION_KEYS.each do |key|
|
|
14
|
+
define_method(:"#{key}=") do |value|
|
|
15
|
+
@explicit_compiler_options[key] = value
|
|
16
|
+
instance_variable_set(:"@#{key}", value)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
12
19
|
|
|
13
20
|
def initialize
|
|
21
|
+
@explicit_compiler_options = {}
|
|
14
22
|
@source_map_enabled = true
|
|
15
23
|
@arity_check = false
|
|
16
24
|
@freezing = true
|
|
@@ -35,6 +43,16 @@ module Opal
|
|
|
35
43
|
end
|
|
36
44
|
end
|
|
37
45
|
|
|
46
|
+
# Compiler options that were set explicitly (via a writer, a config file
|
|
47
|
+
# or Opal::Vite.configure). Unset ones are left to Opal's own defaults, so
|
|
48
|
+
# the output does not change unless the user asked for it.
|
|
49
|
+
def explicit_compiler_options
|
|
50
|
+
@explicit_compiler_options.to_h do |key, value|
|
|
51
|
+
# Severities arrive as strings when read from a JSON config file
|
|
52
|
+
[key, key.to_s.end_with?('_severity') && value ? value.to_sym : value]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
38
56
|
def to_compiler_options
|
|
39
57
|
{
|
|
40
58
|
source_map_enabled: source_map_enabled,
|
|
@@ -81,8 +81,9 @@ module Opal
|
|
|
81
81
|
# @param value [String] Value to set
|
|
82
82
|
# @param timeout [Integer] Maximum wait time in seconds
|
|
83
83
|
def stable_set(selector, value, timeout: DEFAULT_TIMEOUT)
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
# js_retry_action escapes the value itself; escaping here too would
|
|
85
|
+
# double-escape quotes and backslashes
|
|
86
|
+
js_retry_action(selector, 'set', value: value.to_s, timeout: timeout)
|
|
86
87
|
end
|
|
87
88
|
|
|
88
89
|
# Send keys to element with stability check
|
|
@@ -360,7 +361,7 @@ module Opal
|
|
|
360
361
|
# @param timeout [Integer] Maximum wait time in seconds
|
|
361
362
|
def js_retry_action(selector, action, value: nil, timeout: DEFAULT_TIMEOUT)
|
|
362
363
|
escaped_selector = escape_js(selector)
|
|
363
|
-
escaped_value = value ? escape_js(value)
|
|
364
|
+
escaped_value = value.nil? ? '' : escape_js(value)
|
|
364
365
|
start_time = Time.now
|
|
365
366
|
|
|
366
367
|
loop do
|
|
@@ -404,7 +405,7 @@ module Opal
|
|
|
404
405
|
|
|
405
406
|
# Escape string for JavaScript
|
|
406
407
|
def escape_js(str)
|
|
407
|
-
str.to_s.gsub('\\', '\\\\\\\\').gsub("'", "\\\\'").gsub("\n", '\\n')
|
|
408
|
+
str.to_s.gsub('\\', '\\\\\\\\').gsub("'", "\\\\'").gsub("\n", '\\n').gsub("\r", '\\r')
|
|
408
409
|
end
|
|
409
410
|
end
|
|
410
411
|
end
|
data/lib/opal/vite/version.rb
CHANGED
|
@@ -88,6 +88,9 @@ module OpalVite
|
|
|
88
88
|
raise "Cable not connected. Call cable_connect first." unless @_cable_consumer
|
|
89
89
|
|
|
90
90
|
@_cable_subscriptions ||= {}
|
|
91
|
+
# Subscribing again with the same channel and params replaces the
|
|
92
|
+
# earlier subscription instead of leaving both active
|
|
93
|
+
unsubscribe_from(channel_name, params)
|
|
91
94
|
subscription_params = { channel: channel_name }.merge(params)
|
|
92
95
|
native_params = subscription_params.to_n
|
|
93
96
|
|
|
@@ -204,6 +207,9 @@ module OpalVite
|
|
|
204
207
|
raise "Cable not connected. Call cable_connect first." unless @_cable_consumer
|
|
205
208
|
|
|
206
209
|
@_cable_subscriptions ||= {}
|
|
210
|
+
# Subscribing again with the same channel and params replaces the
|
|
211
|
+
# earlier subscription instead of leaving both active
|
|
212
|
+
unsubscribe_from(channel_name, params)
|
|
207
213
|
subscription_params = { channel: channel_name }.merge(params)
|
|
208
214
|
native_params = subscription_params.to_n
|
|
209
215
|
|
|
@@ -354,7 +360,8 @@ module OpalVite
|
|
|
354
360
|
def cable_route(data, handlers, type_key = "type")
|
|
355
361
|
data_type = cable_data_type(data, type_key)
|
|
356
362
|
return unless data_type
|
|
357
|
-
|
|
363
|
+
# The type may be a non-String (e.g. a number), so convert before to_sym
|
|
364
|
+
handler = handlers[data_type] || handlers[data_type.to_s] || handlers[data_type.to_s.to_sym]
|
|
358
365
|
handler&.call
|
|
359
366
|
end
|
|
360
367
|
|
|
@@ -164,7 +164,8 @@ module OpalVite
|
|
|
164
164
|
# @return [String] Basic auth header value
|
|
165
165
|
def basic_auth_header(username, password)
|
|
166
166
|
credentials = "#{username}:#{password}"
|
|
167
|
-
|
|
167
|
+
# UTF-8 (RFC 7617), so non-Latin-1 characters work; btoa alone fails on them
|
|
168
|
+
encoded = base64_encode_unicode(credentials)
|
|
168
169
|
"Basic #{encoded}"
|
|
169
170
|
end
|
|
170
171
|
|
|
@@ -177,6 +178,7 @@ module OpalVite
|
|
|
177
178
|
encoded = header[6..-1]
|
|
178
179
|
decoded = base64_decode(encoded)
|
|
179
180
|
return nil unless decoded
|
|
181
|
+
decoded = binary_to_utf8(decoded)
|
|
180
182
|
|
|
181
183
|
parts = decoded.split(':', 2)
|
|
182
184
|
return nil if parts.length != 2
|
|
@@ -200,6 +202,7 @@ module OpalVite
|
|
|
200
202
|
payload_base64 = parts[1]
|
|
201
203
|
payload_json = base64_decode_urlsafe(payload_base64)
|
|
202
204
|
return nil unless payload_json
|
|
205
|
+
payload_json = binary_to_utf8(payload_json)
|
|
203
206
|
|
|
204
207
|
`JSON.parse(#{payload_json})`
|
|
205
208
|
rescue Exception
|
|
@@ -268,6 +271,14 @@ module OpalVite
|
|
|
268
271
|
# unpadded/URL-safe input (len % 4 == 2 or 3) would yield e.g. 16.5.
|
|
269
272
|
((len * 3) / 4).floor - padding
|
|
270
273
|
end
|
|
274
|
+
|
|
275
|
+
private
|
|
276
|
+
|
|
277
|
+
# Reinterpret a binary string (as returned by atob) as UTF-8 text.
|
|
278
|
+
# Input that is not valid UTF-8 is returned unchanged.
|
|
279
|
+
def binary_to_utf8(str)
|
|
280
|
+
`(function(s) { try { return decodeURIComponent(escape(s)); } catch (e) { return s; } })(#{str})`
|
|
281
|
+
end
|
|
271
282
|
end
|
|
272
283
|
end
|
|
273
284
|
end
|
|
@@ -367,12 +367,14 @@ module OpalVite
|
|
|
367
367
|
|
|
368
368
|
# Get JSON-parsed value from localStorage
|
|
369
369
|
# @param key [String] Storage key
|
|
370
|
-
# @param default [Object] Default value if key doesn't exist
|
|
370
|
+
# @param default [Object] Default value if key doesn't exist or is not valid JSON
|
|
371
371
|
# @return [Object] Parsed value or default
|
|
372
372
|
def storage_get_json(key, default = nil)
|
|
373
373
|
stored = storage_get(key)
|
|
374
374
|
return default if `#{stored} === null`
|
|
375
|
-
|
|
375
|
+
# A value that is not valid JSON (e.g. written by something else)
|
|
376
|
+
# yields the default instead of raising
|
|
377
|
+
`(function(s, d) { try { return JSON.parse(s); } catch (e) { return d; } })(#{stored}, #{default})`
|
|
376
378
|
end
|
|
377
379
|
|
|
378
380
|
# Set JSON-stringified value in localStorage
|
|
@@ -457,34 +459,41 @@ module OpalVite
|
|
|
457
459
|
end
|
|
458
460
|
|
|
459
461
|
# Get the current event's target element
|
|
462
|
+
# @param evt [Native, nil] The event (the argument Stimulus passes to the
|
|
463
|
+
# action). Pass it when you can: without it the deprecated global
|
|
464
|
+
# window.event is used, which is undefined after an await / in callbacks.
|
|
460
465
|
# @return [Native] Event target element
|
|
461
|
-
def event_target
|
|
462
|
-
|
|
466
|
+
def event_target(evt: nil)
|
|
467
|
+
e = current_event(evt)
|
|
468
|
+
`#{e}.currentTarget`
|
|
463
469
|
end
|
|
464
470
|
|
|
465
471
|
# Get data attribute from current event target
|
|
466
472
|
# @param attr [String] Data attribute name (without 'data-' prefix)
|
|
467
473
|
# @return [String, nil] Attribute value
|
|
468
|
-
def event_data(attr)
|
|
469
|
-
|
|
474
|
+
def event_data(attr, evt: nil)
|
|
475
|
+
e = current_event(evt)
|
|
476
|
+
`#{e}.currentTarget.getAttribute('data-' + #{attr})`
|
|
470
477
|
end
|
|
471
478
|
|
|
472
479
|
# Get integer data attribute from current event target
|
|
473
480
|
# @param attr [String] Data attribute name (without 'data-' prefix)
|
|
474
481
|
# @return [Integer, nil] Parsed integer value
|
|
475
|
-
def event_data_int(attr)
|
|
476
|
-
parse_int(event_data(attr))
|
|
482
|
+
def event_data_int(attr, evt: nil)
|
|
483
|
+
parse_int(event_data(attr, evt: evt))
|
|
477
484
|
end
|
|
478
485
|
|
|
479
486
|
# Prevent default event behavior
|
|
480
|
-
def prevent_default
|
|
481
|
-
|
|
487
|
+
def prevent_default(evt: nil)
|
|
488
|
+
e = current_event(evt)
|
|
489
|
+
`#{e}.preventDefault()`
|
|
482
490
|
end
|
|
483
491
|
|
|
484
492
|
# Get event key (for keyboard events)
|
|
485
493
|
# @return [String] Key name
|
|
486
|
-
def event_key
|
|
487
|
-
|
|
494
|
+
def event_key(evt: nil)
|
|
495
|
+
e = current_event(evt)
|
|
496
|
+
`#{e}.key`
|
|
488
497
|
end
|
|
489
498
|
|
|
490
499
|
# ===== Element Methods =====
|
|
@@ -1585,8 +1594,9 @@ module OpalVite
|
|
|
1585
1594
|
# def delete(event)
|
|
1586
1595
|
# params = action_params # => { id: 123 }
|
|
1587
1596
|
# end
|
|
1588
|
-
def action_params
|
|
1589
|
-
|
|
1597
|
+
def action_params(evt: nil)
|
|
1598
|
+
e = current_event(evt)
|
|
1599
|
+
`#{e}.params || {}`
|
|
1590
1600
|
end
|
|
1591
1601
|
|
|
1592
1602
|
# Get a specific action parameter
|
|
@@ -1595,16 +1605,17 @@ module OpalVite
|
|
|
1595
1605
|
# @example
|
|
1596
1606
|
# action_param(:id) # => 123 (Number)
|
|
1597
1607
|
# action_param(:url) # => "/api/item" (String)
|
|
1598
|
-
def action_param(name)
|
|
1599
|
-
|
|
1608
|
+
def action_param(name, evt: nil)
|
|
1609
|
+
e = current_event(evt)
|
|
1610
|
+
`(#{e}.params || {})[#{name.to_s}]`
|
|
1600
1611
|
end
|
|
1601
1612
|
|
|
1602
1613
|
# Get action parameter as integer with default
|
|
1603
1614
|
# @param name [Symbol, String] Parameter name
|
|
1604
1615
|
# @param default [Integer] Default value if missing or NaN
|
|
1605
1616
|
# @return [Integer] Parameter value
|
|
1606
|
-
def action_param_int(name, default = 0)
|
|
1607
|
-
value = action_param(name)
|
|
1617
|
+
def action_param_int(name, default = 0, evt: nil)
|
|
1618
|
+
value = action_param(name, evt: evt)
|
|
1608
1619
|
result = parse_int(value)
|
|
1609
1620
|
is_nan?(result) ? default : result
|
|
1610
1621
|
end
|
|
@@ -1612,16 +1623,17 @@ module OpalVite
|
|
|
1612
1623
|
# Get action parameter as boolean
|
|
1613
1624
|
# @param name [Symbol, String] Parameter name
|
|
1614
1625
|
# @return [Boolean] Parameter value
|
|
1615
|
-
def action_param_bool(name)
|
|
1616
|
-
value = action_param(name)
|
|
1626
|
+
def action_param_bool(name, evt: nil)
|
|
1627
|
+
value = action_param(name, evt: evt)
|
|
1617
1628
|
`!!#{value} && #{value} !== "false" && #{value} !== "0"`
|
|
1618
1629
|
end
|
|
1619
1630
|
|
|
1620
1631
|
# Check if an action parameter exists
|
|
1621
1632
|
# @param name [Symbol, String] Parameter name
|
|
1622
1633
|
# @return [Boolean] true if parameter exists
|
|
1623
|
-
def has_action_param?(name)
|
|
1624
|
-
|
|
1634
|
+
def has_action_param?(name, evt: nil)
|
|
1635
|
+
e = current_event(evt)
|
|
1636
|
+
`(#{e}.params || {}).hasOwnProperty(#{name.to_s})`
|
|
1625
1637
|
end
|
|
1626
1638
|
|
|
1627
1639
|
# ===== Stimulus Controller Access =====
|
|
@@ -1777,13 +1789,15 @@ module OpalVite
|
|
|
1777
1789
|
# Deep merge two objects
|
|
1778
1790
|
# @param target [Hash] Target object
|
|
1779
1791
|
# @param source [Hash] Source object
|
|
1780
|
-
# @return [
|
|
1792
|
+
# @return [Native] Merged plain JavaScript object (not a Ruby Hash);
|
|
1793
|
+
# the keys __proto__ / constructor / prototype are skipped
|
|
1781
1794
|
def deep_merge(target, source)
|
|
1782
1795
|
`
|
|
1783
1796
|
function deepMerge(target, source) {
|
|
1784
1797
|
var result = Object.assign({}, target);
|
|
1785
1798
|
for (var key in source) {
|
|
1786
|
-
if (
|
|
1799
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
|
|
1800
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1787
1801
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
1788
1802
|
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
1789
1803
|
} else {
|
|
@@ -1991,6 +2005,13 @@ module OpalVite
|
|
|
1991
2005
|
|
|
1992
2006
|
private
|
|
1993
2007
|
|
|
2008
|
+
# The event to read: the one passed in (a wrapper such as opal_stimulus's
|
|
2009
|
+
# JS::Proxy or Native is unwrapped with to_n), else the global window.event
|
|
2010
|
+
# (deprecated, and undefined outside the synchronous event dispatch).
|
|
2011
|
+
def current_event(evt)
|
|
2012
|
+
`(function(e) { if (e == null || e === #{nil}) return window.event; return typeof e.$to_n === 'function' ? e.$to_n() : e; })(#{evt})`
|
|
2013
|
+
end
|
|
2014
|
+
|
|
1994
2015
|
# Convert snake_case to camelCase, preserving existing camelCase
|
|
1995
2016
|
# @param name [Symbol, String] The name to convert
|
|
1996
2017
|
# @param capitalize_first [Boolean] Whether to capitalize first letter
|
|
@@ -159,15 +159,18 @@ module OpalVite
|
|
|
159
159
|
|
|
160
160
|
# Get the target frame from current event
|
|
161
161
|
# @return [Native, nil] Frame element from event
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
# @param evt [Native, nil] The event; defaults to the deprecated global window.event
|
|
163
|
+
def event_turbo_frame(evt: nil)
|
|
164
|
+
e = `(function(e) { if (e == null || e === #{nil}) return window.event; return typeof e.$to_n === 'function' ? e.$to_n() : e; })(#{evt})`
|
|
165
|
+
`#{e}.target.closest('turbo-frame')`
|
|
164
166
|
end
|
|
165
167
|
|
|
166
168
|
# ===== Turbo Streams =====
|
|
167
169
|
|
|
168
170
|
# Render a Turbo Stream action
|
|
169
171
|
# @param action [Symbol, String] Stream action (:append, :prepend, :replace, :update, :remove, :before, :after)
|
|
170
|
-
# @param target [String] Target element ID
|
|
172
|
+
# @param target [String] Target element ID (HTML-escaped when rendered;
|
|
173
|
+
# the html argument is NOT escaped, escape user data with escape_html)
|
|
171
174
|
# @param html [String] HTML content (not needed for :remove)
|
|
172
175
|
# @example
|
|
173
176
|
# turbo_stream(:append, "messages", "<div>New message</div>")
|
|
@@ -176,7 +179,7 @@ module OpalVite
|
|
|
176
179
|
action_s = action.to_s
|
|
177
180
|
template_content = html ? "<template>#{html}</template>" : ""
|
|
178
181
|
|
|
179
|
-
stream_html = %{<turbo-stream action="#{action_s}" target="#{target}">#{template_content}</turbo-stream>}
|
|
182
|
+
stream_html = %{<turbo-stream action="#{turbo_escape_attr(action_s)}" target="#{turbo_escape_attr(target)}">#{template_content}</turbo-stream>}
|
|
180
183
|
render_turbo_stream(stream_html)
|
|
181
184
|
end
|
|
182
185
|
|
|
@@ -468,6 +471,11 @@ module OpalVite
|
|
|
468
471
|
|
|
469
472
|
private
|
|
470
473
|
|
|
474
|
+
# Escape a value for use inside a double-quoted HTML attribute
|
|
475
|
+
def turbo_escape_attr(value)
|
|
476
|
+
TurboStreamBuilder.escape_attr(value)
|
|
477
|
+
end
|
|
478
|
+
|
|
471
479
|
def turbo_event_name(event_name)
|
|
472
480
|
event_name.start_with?("turbo:") ? event_name : "turbo:#{event_name}"
|
|
473
481
|
end
|
|
@@ -475,36 +483,42 @@ module OpalVite
|
|
|
475
483
|
|
|
476
484
|
# Builder for creating multiple Turbo Stream operations
|
|
477
485
|
class TurboStreamBuilder
|
|
486
|
+
# Escape a value for use inside a double-quoted HTML attribute
|
|
487
|
+
def self.escape_attr(value)
|
|
488
|
+
str = value.to_s
|
|
489
|
+
`#{str}.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')`
|
|
490
|
+
end
|
|
491
|
+
|
|
478
492
|
def initialize
|
|
479
493
|
@streams = []
|
|
480
494
|
end
|
|
481
495
|
|
|
482
496
|
def append(target, html)
|
|
483
|
-
@streams << %{<turbo-stream action="append" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
497
|
+
@streams << %{<turbo-stream action="append" target="#{TurboStreamBuilder.escape_attr(target)}"><template>#{html}</template></turbo-stream>}
|
|
484
498
|
end
|
|
485
499
|
|
|
486
500
|
def prepend(target, html)
|
|
487
|
-
@streams << %{<turbo-stream action="prepend" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
501
|
+
@streams << %{<turbo-stream action="prepend" target="#{TurboStreamBuilder.escape_attr(target)}"><template>#{html}</template></turbo-stream>}
|
|
488
502
|
end
|
|
489
503
|
|
|
490
504
|
def replace(target, html)
|
|
491
|
-
@streams << %{<turbo-stream action="replace" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
505
|
+
@streams << %{<turbo-stream action="replace" target="#{TurboStreamBuilder.escape_attr(target)}"><template>#{html}</template></turbo-stream>}
|
|
492
506
|
end
|
|
493
507
|
|
|
494
508
|
def update(target, html)
|
|
495
|
-
@streams << %{<turbo-stream action="update" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
509
|
+
@streams << %{<turbo-stream action="update" target="#{TurboStreamBuilder.escape_attr(target)}"><template>#{html}</template></turbo-stream>}
|
|
496
510
|
end
|
|
497
511
|
|
|
498
512
|
def remove(target)
|
|
499
|
-
@streams << %{<turbo-stream action="remove" target="#{target}"></turbo-stream>}
|
|
513
|
+
@streams << %{<turbo-stream action="remove" target="#{TurboStreamBuilder.escape_attr(target)}"></turbo-stream>}
|
|
500
514
|
end
|
|
501
515
|
|
|
502
516
|
def before(target, html)
|
|
503
|
-
@streams << %{<turbo-stream action="before" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
517
|
+
@streams << %{<turbo-stream action="before" target="#{TurboStreamBuilder.escape_attr(target)}"><template>#{html}</template></turbo-stream>}
|
|
504
518
|
end
|
|
505
519
|
|
|
506
520
|
def after(target, html)
|
|
507
|
-
@streams << %{<turbo-stream action="after" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
521
|
+
@streams << %{<turbo-stream action="after" target="#{TurboStreamBuilder.escape_attr(target)}"><template>#{html}</template></turbo-stream>}
|
|
508
522
|
end
|
|
509
523
|
|
|
510
524
|
def render
|
|
@@ -277,10 +277,12 @@ module OpalVite
|
|
|
277
277
|
# ===== Path Utilities =====
|
|
278
278
|
|
|
279
279
|
# Join path segments
|
|
280
|
-
# @param segments [Array<String>] Path segments
|
|
280
|
+
# @param segments [Array<String>] Path segments (a leading "/" on the first one is kept)
|
|
281
281
|
# @return [String] Joined path
|
|
282
282
|
def join_path(*segments)
|
|
283
|
-
segments.map { |s| s.to_s.gsub(%r{
|
|
283
|
+
joined = segments.map { |s| s.to_s.gsub(%r{^/+|/+$}, '') }.reject(&:empty?).join('/')
|
|
284
|
+
# Keep the leading slash of an absolute first segment (join_path('/api', 'users') => '/api/users')
|
|
285
|
+
segments.first.to_s.start_with?('/') ? "/#{joined}" : joined
|
|
284
286
|
end
|
|
285
287
|
|
|
286
288
|
# Get the filename from a path
|
|
@@ -294,9 +296,11 @@ module OpalVite
|
|
|
294
296
|
# @param path [String] Path
|
|
295
297
|
# @return [String] Directory path
|
|
296
298
|
def path_dirname(path)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
299
|
+
str = path.to_s.sub(%r{/+$}, '')
|
|
300
|
+
idx = str.rindex('/')
|
|
301
|
+
return '' if idx.nil? # no directory part
|
|
302
|
+
return '/' if idx == 0 # '/file.txt' => '/'
|
|
303
|
+
str[0...idx]
|
|
300
304
|
end
|
|
301
305
|
|
|
302
306
|
# Get the file extension from a path
|