opal-vite 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d450e521fc2311f795d94b0a92a4d7b8f6e2b676d6320309fb8b3f69696e27ad
|
|
4
|
+
data.tar.gz: '05863ede7a7b4e8f44beceb2d3a7d0438d8e75102fde5dc496acb2251254d126'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 390e78cf526a75c960387caaf4844923fe68814f271e11394a536d198cdcae1fbdaaca31290ddc58b60e6c0a4fe5b0d4d2598669ef246b040f3342b21892b51d
|
|
7
|
+
data.tar.gz: a15d5f251a90702fd67d93639b40a939d073fa43b008ec29693c44f2a32875e04d955b372547e74e4fb1c3355a200cb20012ffa7bb556a7fa0fb42c8618a7284
|
data/lib/opal/vite/version.rb
CHANGED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# backtick_javascript: true
|
|
2
|
+
|
|
3
|
+
module OpalVite
|
|
4
|
+
module Concerns
|
|
5
|
+
module V1
|
|
6
|
+
# ActionCableHelpers - Ruby-friendly DSL for ActionCable WebSocket communication
|
|
7
|
+
#
|
|
8
|
+
# This module provides methods for connecting to and interacting with
|
|
9
|
+
# Rails ActionCable channels from Opal/Stimulus controllers.
|
|
10
|
+
#
|
|
11
|
+
# Prerequisites:
|
|
12
|
+
# - @rails/actioncable package must be installed (npm install @rails/actioncable)
|
|
13
|
+
# - ActionCable consumer must be imported in your JavaScript entry point
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# # In your main.js:
|
|
17
|
+
# import { createConsumer } from "@rails/actioncable"
|
|
18
|
+
# window.ActionCable = { createConsumer }
|
|
19
|
+
#
|
|
20
|
+
# # In your Opal controller:
|
|
21
|
+
# class ChatController < StimulusController
|
|
22
|
+
# include OpalVite::Concerns::V1::ActionCableHelpers
|
|
23
|
+
#
|
|
24
|
+
# def connect
|
|
25
|
+
# cable_connect("/cable")
|
|
26
|
+
# subscribe_to("ChatChannel", room_id: 1) do |subscription|
|
|
27
|
+
# on_cable_received(subscription) do |data|
|
|
28
|
+
# append_message(data)
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# def disconnect
|
|
34
|
+
# cable_disconnect
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# def send_message
|
|
38
|
+
# cable_perform(:speak, message: target_value(:input))
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
module ActionCableHelpers
|
|
42
|
+
# ===== Consumer Management =====
|
|
43
|
+
|
|
44
|
+
# Create and store an ActionCable consumer
|
|
45
|
+
# @param url [String] WebSocket URL (e.g., "/cable" or "wss://example.com/cable")
|
|
46
|
+
# @return [Native] ActionCable consumer instance
|
|
47
|
+
# @example
|
|
48
|
+
# cable_connect("/cable")
|
|
49
|
+
# cable_connect("wss://api.example.com/cable")
|
|
50
|
+
def cable_connect(url = "/cable")
|
|
51
|
+
@_cable_consumer = `window.ActionCable.createConsumer(#{url})`
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get the current ActionCable consumer
|
|
55
|
+
# @return [Native, nil] Consumer instance or nil
|
|
56
|
+
def cable_consumer
|
|
57
|
+
@_cable_consumer
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Disconnect and cleanup the ActionCable consumer
|
|
61
|
+
def cable_disconnect
|
|
62
|
+
return unless @_cable_consumer
|
|
63
|
+
`#{@_cable_consumer}.disconnect()`
|
|
64
|
+
@_cable_subscriptions = nil
|
|
65
|
+
@_cable_consumer = nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check if ActionCable is connected
|
|
69
|
+
# @return [Boolean] true if connected
|
|
70
|
+
def cable_connected?
|
|
71
|
+
return false unless @_cable_consumer
|
|
72
|
+
`#{@_cable_consumer}.connection.isOpen()`
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# ===== Subscription Management =====
|
|
76
|
+
|
|
77
|
+
# Subscribe to an ActionCable channel
|
|
78
|
+
# @param channel_name [String] Channel class name (e.g., "ChatChannel")
|
|
79
|
+
# @param params [Hash] Channel parameters (e.g., room_id: 1)
|
|
80
|
+
# @yield [subscription] Block that receives the subscription for setup
|
|
81
|
+
# @return [Native] Subscription instance
|
|
82
|
+
# @example
|
|
83
|
+
# subscribe_to("ChatChannel", room_id: 1) do |subscription|
|
|
84
|
+
# on_cable_connected(subscription) { puts "Connected!" }
|
|
85
|
+
# on_cable_received(subscription) { |data| handle_data(data) }
|
|
86
|
+
# end
|
|
87
|
+
def subscribe_to(channel_name, params = {}, &setup_block)
|
|
88
|
+
raise "Cable not connected. Call cable_connect first." unless @_cable_consumer
|
|
89
|
+
|
|
90
|
+
@_cable_subscriptions ||= {}
|
|
91
|
+
subscription_params = { channel: channel_name }.merge(params)
|
|
92
|
+
native_params = subscription_params.to_n
|
|
93
|
+
|
|
94
|
+
# Create subscription with empty callbacks (will be set via helpers)
|
|
95
|
+
subscription = `#{@_cable_consumer}.subscriptions.create(#{native_params}, {
|
|
96
|
+
connected: function() {},
|
|
97
|
+
disconnected: function() {},
|
|
98
|
+
received: function(data) {},
|
|
99
|
+
rejected: function() {}
|
|
100
|
+
})`
|
|
101
|
+
|
|
102
|
+
# Store subscription with a key
|
|
103
|
+
key = cable_subscription_key(channel_name, params)
|
|
104
|
+
@_cable_subscriptions[key] = subscription
|
|
105
|
+
|
|
106
|
+
# Yield for setting up callbacks
|
|
107
|
+
setup_block.call(subscription) if setup_block
|
|
108
|
+
|
|
109
|
+
subscription
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Unsubscribe from a channel
|
|
113
|
+
# @param channel_name [String] Channel class name
|
|
114
|
+
# @param params [Hash] Channel parameters
|
|
115
|
+
def unsubscribe_from(channel_name, params = {})
|
|
116
|
+
return unless @_cable_subscriptions
|
|
117
|
+
key = cable_subscription_key(channel_name, params)
|
|
118
|
+
subscription = @_cable_subscriptions.delete(key)
|
|
119
|
+
`#{subscription}.unsubscribe()` if subscription
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Get a subscription by channel name and params
|
|
123
|
+
# @param channel_name [String] Channel class name
|
|
124
|
+
# @param params [Hash] Channel parameters
|
|
125
|
+
# @return [Native, nil] Subscription instance or nil
|
|
126
|
+
def get_subscription(channel_name, params = {})
|
|
127
|
+
return nil unless @_cable_subscriptions
|
|
128
|
+
key = cable_subscription_key(channel_name, params)
|
|
129
|
+
@_cable_subscriptions[key]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# ===== Subscription Callbacks =====
|
|
133
|
+
|
|
134
|
+
# Set the connected callback for a subscription
|
|
135
|
+
# @param subscription [Native] Subscription instance
|
|
136
|
+
# @yield Block to execute when connected
|
|
137
|
+
def on_cable_connected(subscription, &block)
|
|
138
|
+
`#{subscription}.connected = function() { #{block.call} }`
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Set the disconnected callback for a subscription
|
|
142
|
+
# @param subscription [Native] Subscription instance
|
|
143
|
+
# @yield Block to execute when disconnected
|
|
144
|
+
def on_cable_disconnected(subscription, &block)
|
|
145
|
+
`#{subscription}.disconnected = function() { #{block.call} }`
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Set the received callback for a subscription
|
|
149
|
+
# @param subscription [Native] Subscription instance
|
|
150
|
+
# @yield [data] Block that receives incoming data
|
|
151
|
+
def on_cable_received(subscription, &block)
|
|
152
|
+
`#{subscription}.received = function(data) { #{block.call(`data`)} }`
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Set the rejected callback for a subscription
|
|
156
|
+
# @param subscription [Native] Subscription instance
|
|
157
|
+
# @yield Block to execute when subscription is rejected
|
|
158
|
+
def on_cable_rejected(subscription, &block)
|
|
159
|
+
`#{subscription}.rejected = function() { #{block.call} }`
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# ===== Sending Data =====
|
|
163
|
+
|
|
164
|
+
# Perform an action on the default/first subscription
|
|
165
|
+
# @param action [Symbol, String] Action name (server-side method)
|
|
166
|
+
# @param data [Hash] Data to send
|
|
167
|
+
# @example
|
|
168
|
+
# cable_perform(:speak, message: "Hello!")
|
|
169
|
+
# cable_perform(:typing, user_id: current_user_id)
|
|
170
|
+
def cable_perform(action, data = {})
|
|
171
|
+
subscription = default_subscription
|
|
172
|
+
raise "No active subscription" unless subscription
|
|
173
|
+
perform_on(subscription, action, data)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Perform an action on a specific subscription
|
|
177
|
+
# @param subscription [Native] Subscription instance
|
|
178
|
+
# @param action [Symbol, String] Action name
|
|
179
|
+
# @param data [Hash] Data to send
|
|
180
|
+
def perform_on(subscription, action, data = {})
|
|
181
|
+
native_data = data.to_n
|
|
182
|
+
`#{subscription}.perform(#{action.to_s}, #{native_data})`
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Send raw data on a subscription
|
|
186
|
+
# @param subscription [Native] Subscription instance
|
|
187
|
+
# @param data [Hash] Data to send
|
|
188
|
+
def cable_send(subscription, data)
|
|
189
|
+
native_data = data.to_n
|
|
190
|
+
`#{subscription}.send(#{native_data})`
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# ===== Convenience Methods =====
|
|
194
|
+
|
|
195
|
+
# Subscribe and setup all callbacks in one call
|
|
196
|
+
# @param channel_name [String] Channel class name
|
|
197
|
+
# @param params [Hash] Channel parameters
|
|
198
|
+
# @param on_connected [Proc] Connected callback
|
|
199
|
+
# @param on_disconnected [Proc] Disconnected callback
|
|
200
|
+
# @param on_received [Proc] Received callback (receives data)
|
|
201
|
+
# @param on_rejected [Proc] Rejected callback
|
|
202
|
+
# @return [Native] Subscription instance
|
|
203
|
+
def cable_subscribe(channel_name, params: {}, on_connected: nil, on_disconnected: nil, on_received: nil, on_rejected: nil)
|
|
204
|
+
raise "Cable not connected. Call cable_connect first." unless @_cable_consumer
|
|
205
|
+
|
|
206
|
+
@_cable_subscriptions ||= {}
|
|
207
|
+
subscription_params = { channel: channel_name }.merge(params)
|
|
208
|
+
native_params = subscription_params.to_n
|
|
209
|
+
|
|
210
|
+
# Create subscription with callbacks directly
|
|
211
|
+
subscription = `#{@_cable_consumer}.subscriptions.create(#{native_params}, {
|
|
212
|
+
connected: function() {
|
|
213
|
+
#{on_connected.call if on_connected}
|
|
214
|
+
},
|
|
215
|
+
disconnected: function() {
|
|
216
|
+
#{on_disconnected.call if on_disconnected}
|
|
217
|
+
},
|
|
218
|
+
received: function(data) {
|
|
219
|
+
#{on_received.call(`data`) if on_received}
|
|
220
|
+
},
|
|
221
|
+
rejected: function() {
|
|
222
|
+
#{on_rejected.call if on_rejected}
|
|
223
|
+
}
|
|
224
|
+
})`
|
|
225
|
+
|
|
226
|
+
# Store subscription with a key
|
|
227
|
+
key = cable_subscription_key(channel_name, params)
|
|
228
|
+
@_cable_subscriptions[key] = subscription
|
|
229
|
+
|
|
230
|
+
subscription
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Quick subscription setup for simple channels
|
|
234
|
+
# @param channel_name [String] Channel class name
|
|
235
|
+
# @param params [Hash] Channel parameters
|
|
236
|
+
# @yield [data] Block that handles received data
|
|
237
|
+
# @return [Native] Subscription instance
|
|
238
|
+
# @example
|
|
239
|
+
# quick_subscribe("NotificationChannel") do |data|
|
|
240
|
+
# show_notification(data["message"])
|
|
241
|
+
# end
|
|
242
|
+
def quick_subscribe(channel_name, params = {}, &on_received)
|
|
243
|
+
subscribe_to(channel_name, params) do |subscription|
|
|
244
|
+
on_cable_received(subscription) { |data| on_received.call(data) } if on_received
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# ===== Broadcast Helpers =====
|
|
249
|
+
# These methods help handle specific broadcast patterns
|
|
250
|
+
|
|
251
|
+
# Handle a broadcast that contains HTML to insert
|
|
252
|
+
# @param data [Native] Received data object
|
|
253
|
+
# @param target_selector [String] CSS selector for target element
|
|
254
|
+
# @param position [Symbol] Insert position (:append, :prepend, :replace, :before, :after)
|
|
255
|
+
def handle_html_broadcast(data, target_selector, position = :append)
|
|
256
|
+
html = `#{data}.html || #{data}.content || #{data}.body || ""`
|
|
257
|
+
return if `#{html} === ""`
|
|
258
|
+
|
|
259
|
+
target = `document.querySelector(#{target_selector})`
|
|
260
|
+
return unless `#{target}`
|
|
261
|
+
|
|
262
|
+
case position
|
|
263
|
+
when :append
|
|
264
|
+
`#{target}.insertAdjacentHTML('beforeend', #{html})`
|
|
265
|
+
when :prepend
|
|
266
|
+
`#{target}.insertAdjacentHTML('afterbegin', #{html})`
|
|
267
|
+
when :replace
|
|
268
|
+
`#{target}.innerHTML = #{html}`
|
|
269
|
+
when :before
|
|
270
|
+
`#{target}.insertAdjacentHTML('beforebegin', #{html})`
|
|
271
|
+
when :after
|
|
272
|
+
`#{target}.insertAdjacentHTML('afterend', #{html})`
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Handle a broadcast that updates a specific element
|
|
277
|
+
# @param data [Native] Received data with id and content
|
|
278
|
+
# @param id_key [String] Key in data containing element ID
|
|
279
|
+
# @param content_key [String] Key in data containing new content
|
|
280
|
+
def handle_update_broadcast(data, id_key = "id", content_key = "content")
|
|
281
|
+
element_id = `#{data}[#{id_key}]`
|
|
282
|
+
content = `#{data}[#{content_key}]`
|
|
283
|
+
return if `#{element_id} === undefined || #{content} === undefined`
|
|
284
|
+
|
|
285
|
+
element = `document.getElementById(#{element_id})`
|
|
286
|
+
`#{element}.innerHTML = #{content}` if `#{element}`
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Handle a broadcast that removes an element
|
|
290
|
+
# @param data [Native] Received data with id
|
|
291
|
+
# @param id_key [String] Key in data containing element ID
|
|
292
|
+
def handle_remove_broadcast(data, id_key = "id")
|
|
293
|
+
element_id = `#{data}[#{id_key}]`
|
|
294
|
+
return if `#{element_id} === undefined`
|
|
295
|
+
|
|
296
|
+
element = `document.getElementById(#{element_id})`
|
|
297
|
+
`#{element}.remove()` if `#{element}`
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# ===== Data Extraction Helpers =====
|
|
301
|
+
|
|
302
|
+
# Extract a value from received ActionCable data
|
|
303
|
+
# @param data [Native] Received data object
|
|
304
|
+
# @param key [String, Symbol] Key to extract
|
|
305
|
+
# @param default [Object] Default value if key doesn't exist
|
|
306
|
+
# @return [Object] Extracted value or default
|
|
307
|
+
def cable_data(data, key, default = nil)
|
|
308
|
+
key_s = key.to_s
|
|
309
|
+
value = `#{data}[#{key_s}]`
|
|
310
|
+
`#{value} === undefined` ? default : value
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Extract and parse a JSON string from received data
|
|
314
|
+
# @param data [Native] Received data object
|
|
315
|
+
# @param key [String, Symbol] Key containing JSON string
|
|
316
|
+
# @return [Native] Parsed JSON object or nil
|
|
317
|
+
def cable_data_json(data, key)
|
|
318
|
+
json_str = cable_data(data, key)
|
|
319
|
+
return nil if json_str.nil?
|
|
320
|
+
`JSON.parse(#{json_str})`
|
|
321
|
+
rescue
|
|
322
|
+
nil
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Check if received data has a specific key
|
|
326
|
+
# @param data [Native] Received data object
|
|
327
|
+
# @param key [String, Symbol] Key to check
|
|
328
|
+
# @return [Boolean] true if key exists
|
|
329
|
+
def cable_data_has?(data, key)
|
|
330
|
+
key_s = key.to_s
|
|
331
|
+
`#{data}.hasOwnProperty(#{key_s})`
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Get data type from received data (for routing actions)
|
|
335
|
+
# @param data [Native] Received data object
|
|
336
|
+
# @param type_key [String] Key containing type/action name
|
|
337
|
+
# @return [String, nil] Type value
|
|
338
|
+
def cable_data_type(data, type_key = "type")
|
|
339
|
+
cable_data(data, type_key)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Route incoming data based on type/action
|
|
343
|
+
# @param data [Native] Received data object
|
|
344
|
+
# @param handlers [Hash] Map of type => handler proc
|
|
345
|
+
# @param type_key [String] Key containing type/action name
|
|
346
|
+
# @example
|
|
347
|
+
# on_cable_received(subscription) do |data|
|
|
348
|
+
# cable_route(data, {
|
|
349
|
+
# "message" => -> { handle_message(data) },
|
|
350
|
+
# "typing" => -> { handle_typing(data) },
|
|
351
|
+
# "presence" => -> { handle_presence(data) }
|
|
352
|
+
# })
|
|
353
|
+
# end
|
|
354
|
+
def cable_route(data, handlers, type_key = "type")
|
|
355
|
+
data_type = cable_data_type(data, type_key)
|
|
356
|
+
return unless data_type
|
|
357
|
+
handler = handlers[data_type] || handlers[data_type.to_sym]
|
|
358
|
+
handler&.call
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
private
|
|
362
|
+
|
|
363
|
+
# Generate a unique key for subscription storage
|
|
364
|
+
def cable_subscription_key(channel_name, params)
|
|
365
|
+
"#{channel_name}:#{params.to_a.sort.map { |k, v| "#{k}=#{v}" }.join(',')}"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Get the default (first) subscription
|
|
369
|
+
def default_subscription
|
|
370
|
+
return nil unless @_cable_subscriptions
|
|
371
|
+
@_cable_subscriptions.values.first
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Alias for backward compatibility
|
|
379
|
+
ActionCableHelpers = OpalVite::Concerns::V1::ActionCableHelpers
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# backtick_javascript: true
|
|
2
|
+
|
|
3
|
+
module OpalVite
|
|
4
|
+
module Concerns
|
|
5
|
+
module V1
|
|
6
|
+
# TurboHelpers - Ruby-friendly DSL for Hotwire Turbo integration
|
|
7
|
+
#
|
|
8
|
+
# This module provides methods for interacting with Turbo Drive, Frames, and Streams
|
|
9
|
+
# from Opal/Stimulus controllers.
|
|
10
|
+
#
|
|
11
|
+
# Prerequisites:
|
|
12
|
+
# - @hotwired/turbo package must be installed (npm install @hotwired/turbo)
|
|
13
|
+
# - Turbo must be imported in your JavaScript entry point
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# # In your main.js:
|
|
17
|
+
# import * as Turbo from "@hotwired/turbo"
|
|
18
|
+
# window.Turbo = Turbo
|
|
19
|
+
#
|
|
20
|
+
# # In your Opal controller:
|
|
21
|
+
# class NavigationController < StimulusController
|
|
22
|
+
# include OpalVite::Concerns::V1::TurboHelpers
|
|
23
|
+
#
|
|
24
|
+
# def navigate_to(event)
|
|
25
|
+
# turbo_visit("/dashboard")
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# def refresh_frame
|
|
29
|
+
# reload_turbo_frame("notifications")
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
module TurboHelpers
|
|
33
|
+
# ===== Turbo Drive =====
|
|
34
|
+
|
|
35
|
+
# Navigate to a URL using Turbo Drive
|
|
36
|
+
# @param url [String] URL to navigate to
|
|
37
|
+
# @param options [Hash] Visit options
|
|
38
|
+
# @option options [String] :action ("advance") "advance", "replace", or "restore"
|
|
39
|
+
# @option options [String] :frame Target frame ID (for frame navigation)
|
|
40
|
+
# @example
|
|
41
|
+
# turbo_visit("/users/1")
|
|
42
|
+
# turbo_visit("/login", action: "replace")
|
|
43
|
+
# turbo_visit("/modal-content", frame: "modal")
|
|
44
|
+
def turbo_visit(url, options = {})
|
|
45
|
+
native_options = options.to_n
|
|
46
|
+
if options.empty?
|
|
47
|
+
`window.Turbo.visit(#{url})`
|
|
48
|
+
else
|
|
49
|
+
`window.Turbo.visit(#{url}, #{native_options})`
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Navigate to a URL replacing the current history entry
|
|
54
|
+
# @param url [String] URL to navigate to
|
|
55
|
+
def turbo_replace(url)
|
|
56
|
+
turbo_visit(url, action: "replace")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Clear the Turbo Drive cache
|
|
60
|
+
def turbo_clear_cache
|
|
61
|
+
`window.Turbo.cache.clear()`
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Enable Turbo Drive (if previously disabled)
|
|
65
|
+
def turbo_enable
|
|
66
|
+
`document.documentElement.removeAttribute('data-turbo')`
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Disable Turbo Drive globally
|
|
70
|
+
def turbo_disable
|
|
71
|
+
`document.documentElement.setAttribute('data-turbo', 'false')`
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if Turbo Drive is enabled
|
|
75
|
+
# @return [Boolean] true if enabled
|
|
76
|
+
def turbo_enabled?
|
|
77
|
+
`document.documentElement.getAttribute('data-turbo') !== 'false'`
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Set Turbo Drive progress bar delay
|
|
81
|
+
# @param delay [Integer] Delay in milliseconds
|
|
82
|
+
def turbo_progress_delay(delay)
|
|
83
|
+
`window.Turbo.setProgressBarDelay(#{delay})`
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# ===== Turbo Frames =====
|
|
87
|
+
|
|
88
|
+
# Get a Turbo Frame element by ID
|
|
89
|
+
# @param frame_id [String] Frame ID
|
|
90
|
+
# @return [Native, nil] Frame element or nil
|
|
91
|
+
def get_turbo_frame(frame_id)
|
|
92
|
+
frame = `document.querySelector('turbo-frame#' + #{frame_id})`
|
|
93
|
+
`#{frame} === null` ? nil : frame
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Reload a Turbo Frame
|
|
97
|
+
# @param frame_id [String] Frame ID to reload
|
|
98
|
+
# @param url [String, nil] Optional URL to load (uses src attribute if nil)
|
|
99
|
+
def reload_turbo_frame(frame_id, url = nil)
|
|
100
|
+
frame = get_turbo_frame(frame_id)
|
|
101
|
+
return unless frame
|
|
102
|
+
|
|
103
|
+
if url
|
|
104
|
+
`#{frame}.src = #{url}`
|
|
105
|
+
else
|
|
106
|
+
# Trigger reload by re-setting src
|
|
107
|
+
current_src = `#{frame}.src`
|
|
108
|
+
`#{frame}.src = #{current_src}`
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Set the src attribute of a Turbo Frame
|
|
113
|
+
# @param frame_id [String] Frame ID
|
|
114
|
+
# @param url [String] URL to load
|
|
115
|
+
def set_frame_src(frame_id, url)
|
|
116
|
+
frame = get_turbo_frame(frame_id)
|
|
117
|
+
`#{frame}.src = #{url}` if frame
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Disable a Turbo Frame (prevent loading)
|
|
121
|
+
# @param frame_id [String] Frame ID
|
|
122
|
+
def disable_turbo_frame(frame_id)
|
|
123
|
+
frame = get_turbo_frame(frame_id)
|
|
124
|
+
`#{frame}.setAttribute('disabled', '')` if frame
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Enable a Turbo Frame
|
|
128
|
+
# @param frame_id [String] Frame ID
|
|
129
|
+
def enable_turbo_frame(frame_id)
|
|
130
|
+
frame = get_turbo_frame(frame_id)
|
|
131
|
+
`#{frame}.removeAttribute('disabled')` if frame
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Check if a Turbo Frame is loading
|
|
135
|
+
# @param frame_id [String] Frame ID
|
|
136
|
+
# @return [Boolean] true if loading
|
|
137
|
+
def frame_loading?(frame_id)
|
|
138
|
+
frame = get_turbo_frame(frame_id)
|
|
139
|
+
return false unless frame
|
|
140
|
+
`#{frame}.hasAttribute('busy')`
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Wait for a Turbo Frame to finish loading
|
|
144
|
+
# @param frame_id [String] Frame ID
|
|
145
|
+
# @yield Block to execute when loaded
|
|
146
|
+
def on_frame_loaded(frame_id, &block)
|
|
147
|
+
frame = get_turbo_frame(frame_id)
|
|
148
|
+
return unless frame
|
|
149
|
+
|
|
150
|
+
if frame_loading?(frame_id)
|
|
151
|
+
`#{frame}.addEventListener('turbo:frame-load', function handler() {
|
|
152
|
+
#{frame}.removeEventListener('turbo:frame-load', handler);
|
|
153
|
+
#{block.call};
|
|
154
|
+
})`
|
|
155
|
+
else
|
|
156
|
+
block.call
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Get the target frame from current event
|
|
161
|
+
# @return [Native, nil] Frame element from event
|
|
162
|
+
def event_turbo_frame
|
|
163
|
+
`event.target.closest('turbo-frame')`
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# ===== Turbo Streams =====
|
|
167
|
+
|
|
168
|
+
# Render a Turbo Stream action
|
|
169
|
+
# @param action [Symbol, String] Stream action (:append, :prepend, :replace, :update, :remove, :before, :after)
|
|
170
|
+
# @param target [String] Target element ID
|
|
171
|
+
# @param html [String] HTML content (not needed for :remove)
|
|
172
|
+
# @example
|
|
173
|
+
# turbo_stream(:append, "messages", "<div>New message</div>")
|
|
174
|
+
# turbo_stream(:remove, "message_1")
|
|
175
|
+
def turbo_stream(action, target, html = nil)
|
|
176
|
+
action_s = action.to_s
|
|
177
|
+
template_content = html ? "<template>#{html}</template>" : ""
|
|
178
|
+
|
|
179
|
+
stream_html = %{<turbo-stream action="#{action_s}" target="#{target}">#{template_content}</turbo-stream>}
|
|
180
|
+
render_turbo_stream(stream_html)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Render raw Turbo Stream HTML
|
|
184
|
+
# @param stream_html [String] Full turbo-stream element HTML
|
|
185
|
+
def render_turbo_stream(stream_html)
|
|
186
|
+
`window.Turbo.renderStreamMessage(#{stream_html})`
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Append content to target element via Turbo Stream
|
|
190
|
+
# @param target [String] Target element ID
|
|
191
|
+
# @param html [String] HTML content to append
|
|
192
|
+
def turbo_append(target, html)
|
|
193
|
+
turbo_stream(:append, target, html)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Prepend content to target element via Turbo Stream
|
|
197
|
+
# @param target [String] Target element ID
|
|
198
|
+
# @param html [String] HTML content to prepend
|
|
199
|
+
def turbo_prepend(target, html)
|
|
200
|
+
turbo_stream(:prepend, target, html)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Replace target element via Turbo Stream
|
|
204
|
+
# @param target [String] Target element ID
|
|
205
|
+
# @param html [String] HTML content to replace with
|
|
206
|
+
def turbo_replace_element(target, html)
|
|
207
|
+
turbo_stream(:replace, target, html)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Update target element's content via Turbo Stream
|
|
211
|
+
# @param target [String] Target element ID
|
|
212
|
+
# @param html [String] HTML content to update with
|
|
213
|
+
def turbo_update(target, html)
|
|
214
|
+
turbo_stream(:update, target, html)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Remove target element via Turbo Stream
|
|
218
|
+
# @param target [String] Target element ID
|
|
219
|
+
def turbo_remove(target)
|
|
220
|
+
turbo_stream(:remove, target)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Insert content before target element via Turbo Stream
|
|
224
|
+
# @param target [String] Target element ID
|
|
225
|
+
# @param html [String] HTML content to insert
|
|
226
|
+
def turbo_before(target, html)
|
|
227
|
+
turbo_stream(:before, target, html)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Insert content after target element via Turbo Stream
|
|
231
|
+
# @param target [String] Target element ID
|
|
232
|
+
# @param html [String] HTML content to insert
|
|
233
|
+
def turbo_after(target, html)
|
|
234
|
+
turbo_stream(:after, target, html)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Create multiple Turbo Stream operations
|
|
238
|
+
# @yield Block that builds stream operations
|
|
239
|
+
# @example
|
|
240
|
+
# turbo_streams do |s|
|
|
241
|
+
# s.append("messages", "<div>Message 1</div>")
|
|
242
|
+
# s.prepend("notifications", "<div>Alert!</div>")
|
|
243
|
+
# s.remove("loading-indicator")
|
|
244
|
+
# end
|
|
245
|
+
def turbo_streams(&block)
|
|
246
|
+
builder = TurboStreamBuilder.new
|
|
247
|
+
block.call(builder)
|
|
248
|
+
builder.render
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# ===== Turbo Events =====
|
|
252
|
+
|
|
253
|
+
# Listen for Turbo Drive events
|
|
254
|
+
# @param event_name [String] Event name (without 'turbo:' prefix)
|
|
255
|
+
# @yield [event] Block to execute when event fires
|
|
256
|
+
# @example
|
|
257
|
+
# on_turbo("before-visit") { |e| validate_form }
|
|
258
|
+
# on_turbo("load") { init_components }
|
|
259
|
+
def on_turbo(event_name, &block)
|
|
260
|
+
full_name = event_name.start_with?("turbo:") ? event_name : "turbo:#{event_name}"
|
|
261
|
+
`document.addEventListener(#{full_name}, function(event) { #{block.call(`event`)} })`
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Listen for turbo:before-visit - cancel navigation
|
|
265
|
+
# @yield [event] Block to execute (call event.preventDefault to cancel)
|
|
266
|
+
def on_turbo_before_visit(&block)
|
|
267
|
+
on_turbo("before-visit", &block)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Listen for turbo:visit - navigation started
|
|
271
|
+
# @yield [event] Block to execute
|
|
272
|
+
def on_turbo_visit(&block)
|
|
273
|
+
on_turbo("visit", &block)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Listen for turbo:load - page fully loaded
|
|
277
|
+
# @yield [event] Block to execute
|
|
278
|
+
def on_turbo_load(&block)
|
|
279
|
+
on_turbo("load", &block)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Listen for turbo:render - page rendered
|
|
283
|
+
# @yield [event] Block to execute
|
|
284
|
+
def on_turbo_render(&block)
|
|
285
|
+
on_turbo("render", &block)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Listen for turbo:before-fetch-request - before fetch
|
|
289
|
+
# @yield [event] Block to execute
|
|
290
|
+
def on_turbo_before_fetch(&block)
|
|
291
|
+
on_turbo("before-fetch-request", &block)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Listen for turbo:submit-start - form submission started
|
|
295
|
+
# @yield [event] Block to execute
|
|
296
|
+
def on_turbo_submit_start(&block)
|
|
297
|
+
on_turbo("submit-start", &block)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Listen for turbo:submit-end - form submission ended
|
|
301
|
+
# @yield [event] Block to execute
|
|
302
|
+
def on_turbo_submit_end(&block)
|
|
303
|
+
on_turbo("submit-end", &block)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Listen for turbo:frame-load - frame loaded
|
|
307
|
+
# @yield [event] Block to execute
|
|
308
|
+
def on_turbo_frame_load(&block)
|
|
309
|
+
on_turbo("frame-load", &block)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Listen for turbo:before-stream-render - before stream renders
|
|
313
|
+
# @yield [event] Block to execute
|
|
314
|
+
def on_turbo_before_stream(&block)
|
|
315
|
+
on_turbo("before-stream-render", &block)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# ===== Form Helpers =====
|
|
319
|
+
|
|
320
|
+
# Submit a form via Turbo
|
|
321
|
+
# @param form_element [Native] Form element
|
|
322
|
+
def turbo_submit(form_element)
|
|
323
|
+
el = form_element.respond_to?(:to_n) ? form_element.to_n : form_element
|
|
324
|
+
`#{el}.requestSubmit()`
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Submit a form with custom target frame
|
|
328
|
+
# @param form_element [Native] Form element
|
|
329
|
+
# @param frame_id [String] Target frame ID
|
|
330
|
+
def turbo_submit_to_frame(form_element, frame_id)
|
|
331
|
+
el = form_element.respond_to?(:to_n) ? form_element.to_n : form_element
|
|
332
|
+
`#{el}.setAttribute('data-turbo-frame', #{frame_id})`
|
|
333
|
+
`#{el}.requestSubmit()`
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Disable Turbo on a specific form
|
|
337
|
+
# @param form_element [Native] Form element
|
|
338
|
+
def disable_turbo_form(form_element)
|
|
339
|
+
el = form_element.respond_to?(:to_n) ? form_element.to_n : form_element
|
|
340
|
+
`#{el}.setAttribute('data-turbo', 'false')`
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Enable Turbo on a specific form
|
|
344
|
+
# @param form_element [Native] Form element
|
|
345
|
+
def enable_turbo_form(form_element)
|
|
346
|
+
el = form_element.respond_to?(:to_n) ? form_element.to_n : form_element
|
|
347
|
+
`#{el}.removeAttribute('data-turbo')`
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# ===== Stream over SSE =====
|
|
351
|
+
|
|
352
|
+
# Connect to a Turbo Stream over SSE endpoint
|
|
353
|
+
# @param url [String] SSE endpoint URL
|
|
354
|
+
# @return [Native] EventSource instance
|
|
355
|
+
# @example
|
|
356
|
+
# source = turbo_stream_from("/notifications/stream")
|
|
357
|
+
# # Turbo will automatically process incoming streams
|
|
358
|
+
def turbo_stream_from(url)
|
|
359
|
+
`window.Turbo.connectStreamSource(new EventSource(#{url}))`
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Disconnect a Turbo Stream SSE source
|
|
363
|
+
# @param source [Native] EventSource to disconnect
|
|
364
|
+
def turbo_stream_disconnect(source)
|
|
365
|
+
`window.Turbo.disconnectStreamSource(#{source})`
|
|
366
|
+
`#{source}.close()`
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# ===== Confirmation Dialog =====
|
|
370
|
+
|
|
371
|
+
# Set custom Turbo confirmation handler
|
|
372
|
+
# @yield [message, element, submitter] Block that returns true/false
|
|
373
|
+
# @example
|
|
374
|
+
# turbo_confirm_method do |message, element, submitter|
|
|
375
|
+
# `window.confirm(message)`
|
|
376
|
+
# end
|
|
377
|
+
def turbo_confirm_method(&block)
|
|
378
|
+
`window.Turbo.setConfirmMethod(function(message, element, submitter) {
|
|
379
|
+
return #{block.call(`message`, `element`, `submitter`)};
|
|
380
|
+
})`
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# ===== Morphing (Turbo 8+) =====
|
|
384
|
+
|
|
385
|
+
# Refresh the page using Turbo's morph feature
|
|
386
|
+
# @param options [Hash] Refresh options
|
|
387
|
+
# @option options [Boolean] :request_id Custom request ID
|
|
388
|
+
def turbo_refresh(options = {})
|
|
389
|
+
if options.empty?
|
|
390
|
+
`window.Turbo.visit(window.location.href, { action: 'replace' })`
|
|
391
|
+
else
|
|
392
|
+
native_options = options.merge(action: "replace").to_n
|
|
393
|
+
`window.Turbo.visit(window.location.href, #{native_options})`
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# ===== Utility Methods =====
|
|
398
|
+
|
|
399
|
+
# Check if Turbo is available
|
|
400
|
+
# @return [Boolean] true if Turbo is loaded
|
|
401
|
+
def turbo_available?
|
|
402
|
+
`typeof window.Turbo !== 'undefined'`
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Get current Turbo Drive visit location
|
|
406
|
+
# @return [String, nil] Current URL or nil
|
|
407
|
+
def turbo_current_url
|
|
408
|
+
`window.Turbo.navigator.location?.href`
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Cancel a pending Turbo visit
|
|
412
|
+
def turbo_cancel_visit
|
|
413
|
+
`window.Turbo.navigator.stop()`
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Add loading class during Turbo navigation
|
|
417
|
+
# @param element [Native] Element to add class to
|
|
418
|
+
# @param class_name [String] Class name to toggle
|
|
419
|
+
def turbo_loading_class(element, class_name = "turbo-loading")
|
|
420
|
+
el = element.respond_to?(:to_n) ? element.to_n : element
|
|
421
|
+
|
|
422
|
+
on_turbo("before-fetch-request") { `#{el}.classList.add(#{class_name})` }
|
|
423
|
+
on_turbo("before-fetch-response") { `#{el}.classList.remove(#{class_name})` }
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Builder for creating multiple Turbo Stream operations
|
|
428
|
+
class TurboStreamBuilder
|
|
429
|
+
def initialize
|
|
430
|
+
@streams = []
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def append(target, html)
|
|
434
|
+
@streams << %{<turbo-stream action="append" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def prepend(target, html)
|
|
438
|
+
@streams << %{<turbo-stream action="prepend" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def replace(target, html)
|
|
442
|
+
@streams << %{<turbo-stream action="replace" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def update(target, html)
|
|
446
|
+
@streams << %{<turbo-stream action="update" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def remove(target)
|
|
450
|
+
@streams << %{<turbo-stream action="remove" target="#{target}"></turbo-stream>}
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def before(target, html)
|
|
454
|
+
@streams << %{<turbo-stream action="before" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def after(target, html)
|
|
458
|
+
@streams << %{<turbo-stream action="after" target="#{target}"><template>#{html}</template></turbo-stream>}
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def render
|
|
462
|
+
@streams.each do |stream_html|
|
|
463
|
+
`window.Turbo.renderStreamMessage(#{stream_html})`
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Alias for backward compatibility
|
|
472
|
+
TurboHelpers = OpalVite::Concerns::V1::TurboHelpers
|
|
@@ -9,3 +9,5 @@ require 'opal_vite/concerns/v1/react_helpers'
|
|
|
9
9
|
require 'opal_vite/concerns/v1/uri_helpers'
|
|
10
10
|
require 'opal_vite/concerns/v1/base64_helpers'
|
|
11
11
|
require 'opal_vite/concerns/v1/debug_helpers'
|
|
12
|
+
require 'opal_vite/concerns/v1/action_cable_helpers'
|
|
13
|
+
require 'opal_vite/concerns/v1/turbo_helpers'
|
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.3.
|
|
4
|
+
version: 0.3.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- stofu1234
|
|
@@ -120,6 +120,7 @@ files:
|
|
|
120
120
|
- opal/opal_vite/concerns/storable.rb
|
|
121
121
|
- opal/opal_vite/concerns/toastable.rb
|
|
122
122
|
- opal/opal_vite/concerns/v1.rb
|
|
123
|
+
- opal/opal_vite/concerns/v1/action_cable_helpers.rb
|
|
123
124
|
- opal/opal_vite/concerns/v1/base64_helpers.rb
|
|
124
125
|
- opal/opal_vite/concerns/v1/debug_helpers.rb
|
|
125
126
|
- opal/opal_vite/concerns/v1/dom_helpers.rb
|
|
@@ -128,6 +129,7 @@ files:
|
|
|
128
129
|
- opal/opal_vite/concerns/v1/stimulus_helpers.rb
|
|
129
130
|
- opal/opal_vite/concerns/v1/storable.rb
|
|
130
131
|
- opal/opal_vite/concerns/v1/toastable.rb
|
|
132
|
+
- opal/opal_vite/concerns/v1/turbo_helpers.rb
|
|
131
133
|
- opal/opal_vite/concerns/v1/uri_helpers.rb
|
|
132
134
|
- opal/opal_vite/concerns/v1/vue_helpers.rb
|
|
133
135
|
- opal/opal_vite/concerns/vue_helpers.rb
|