turbo_reflex 0.0.6 → 0.0.7
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/Gemfile.lock +2 -1
- data/README.md +1 -6
- data/app/assets/builds/turbo_reflex.js +1 -1
- data/app/assets/builds/turbo_reflex.js.map +4 -4
- data/app/controllers/concerns/turbo_reflex/controller.rb +12 -108
- data/app/javascript/activity.js +20 -0
- data/app/javascript/delegates.js +27 -0
- data/app/javascript/drivers/form.js +12 -0
- data/app/javascript/drivers/frame.js +11 -0
- data/app/javascript/drivers/index.js +63 -0
- data/app/javascript/drivers/window.js +70 -0
- data/app/javascript/elements.js +29 -50
- data/app/javascript/index.js +72 -0
- data/app/javascript/lifecycle.js +40 -0
- data/app/javascript/logger.js +34 -0
- data/app/javascript/schema.js +6 -0
- data/app/javascript/turbo.js +24 -0
- data/app/javascript/urls.js +9 -0
- data/app/javascript/uuids.js +10 -0
- data/lib/turbo_reflex/base.rb +59 -9
- data/lib/turbo_reflex/engine.rb +1 -9
- data/lib/turbo_reflex/runner.rb +199 -0
- data/lib/turbo_reflex/version.rb +1 -1
- data/package.json +5 -4
- data/turbo_reflex.gemspec +1 -0
- data/yarn.lock +135 -135
- metadata +30 -11
- data/app/controllers/turbo_reflex/turbo_reflexes_controller.rb +0 -12
- data/app/helpers/turbo_reflex/turbo_reflex_helper.rb +0 -9
- data/app/javascript/event_registry.js +0 -34
- data/app/javascript/frame_sources.js +0 -28
- data/app/javascript/lifecycle_events.js +0 -24
- data/app/javascript/security.js +0 -7
- data/app/javascript/turbo_reflex.js +0 -111
- data/config/routes.rb +0 -5
- data/lib/turbo_reflex/errors.rb +0 -6
data/app/javascript/elements.js
CHANGED
@@ -1,55 +1,14 @@
|
|
1
|
-
import
|
1
|
+
import schema from './schema.js'
|
2
|
+
import lifecycle from './lifecycle'
|
2
3
|
|
3
4
|
function findClosestReflex (element) {
|
4
|
-
return element.closest(
|
5
|
+
return element.closest(`[${schema.reflexAttribute}]`)
|
5
6
|
}
|
6
7
|
|
7
8
|
function findClosestFrame (element) {
|
8
9
|
return element.closest('turbo-frame')
|
9
10
|
}
|
10
11
|
|
11
|
-
function findFrameId (element) {
|
12
|
-
let id = element.dataset.turboFrame
|
13
|
-
if (!id) {
|
14
|
-
const frame = findClosestFrame(element)
|
15
|
-
if (frame) id = frame.id
|
16
|
-
}
|
17
|
-
if (!id) {
|
18
|
-
console.error(
|
19
|
-
`The reflex element does not specify a frame!`,
|
20
|
-
`Please move the reflex element inside a <turbo-frame> or set the 'data-turbo-frame' attribute.`,
|
21
|
-
element
|
22
|
-
)
|
23
|
-
LifecycleEvents.dispatch(LifecycleEvents.missingFrameId, element, {
|
24
|
-
element
|
25
|
-
})
|
26
|
-
}
|
27
|
-
return id
|
28
|
-
}
|
29
|
-
|
30
|
-
function findFrame (id) {
|
31
|
-
const frame = document.getElementById(id)
|
32
|
-
if (!frame) {
|
33
|
-
console.error(`The frame '${id}' does not exist!`)
|
34
|
-
LifecycleEvents.dispatch(LifecycleEvents.missingFrame, document, { id })
|
35
|
-
}
|
36
|
-
return frame
|
37
|
-
}
|
38
|
-
|
39
|
-
function findFrameSrc (frame) {
|
40
|
-
const frameSrc = frame.dataset.turboReflexSrc || frame.src
|
41
|
-
if (!frameSrc) {
|
42
|
-
console.error(
|
43
|
-
`The the 'src' for <turbo-frame id='${frame.id}'> is unknown!`,
|
44
|
-
`TurboReflex uses 'src' to (re)render frame content after the reflex is invoked.`,
|
45
|
-
`Please set the 'src' or 'data-turbo-reflex-src' attribute on the <turbo-frame> element.`,
|
46
|
-
frame
|
47
|
-
)
|
48
|
-
LifecycleEvents.dispatch(LifecycleEvents.missingFrameSrc, frame, { frame })
|
49
|
-
}
|
50
|
-
return frameSrc
|
51
|
-
}
|
52
|
-
|
53
12
|
function assignElementValueToPayload (element, payload = {}) {
|
54
13
|
if (element.tagName.toLowerCase() !== 'select')
|
55
14
|
return (payload.value = element.value)
|
@@ -64,8 +23,16 @@ function assignElementValueToPayload (element, payload = {}) {
|
|
64
23
|
}
|
65
24
|
|
66
25
|
function buildAttributePayload (element) {
|
26
|
+
// truncate long values to optimize payload size
|
27
|
+
// TODO: revisit this decision
|
28
|
+
const maxAttributeLength = 100
|
29
|
+
const maxValueLength = 500
|
30
|
+
|
67
31
|
const payload = Array.from(element.attributes).reduce((memo, attr) => {
|
68
|
-
|
32
|
+
let value = attr.value
|
33
|
+
if (typeof value === 'string' && value.length > maxAttributeLength)
|
34
|
+
value = value.slice(0, maxAttributeLength) + '...'
|
35
|
+
memo[attr.name] = value
|
69
36
|
return memo
|
70
37
|
}, {})
|
71
38
|
|
@@ -74,14 +41,26 @@ function buildAttributePayload (element) {
|
|
74
41
|
payload.disabled = element.disabled
|
75
42
|
assignElementValueToPayload(element, payload)
|
76
43
|
|
44
|
+
if (
|
45
|
+
typeof payload.value === 'string' &&
|
46
|
+
payload.value.length > maxValueLength
|
47
|
+
)
|
48
|
+
payload.value = payload.value.slice(0, maxValueLength) + '...'
|
49
|
+
|
50
|
+
delete payload.class
|
51
|
+
delete payload[schema.reflexAttribute]
|
52
|
+
delete payload[schema.frameAttribute]
|
77
53
|
return payload
|
78
54
|
}
|
79
55
|
|
80
|
-
export {
|
56
|
+
export default {
|
57
|
+
buildAttributePayload,
|
81
58
|
findClosestReflex,
|
82
59
|
findClosestFrame,
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
60
|
+
get metaElement () {
|
61
|
+
return document.getElementById('turbo-reflex')
|
62
|
+
},
|
63
|
+
get metaElementToken () {
|
64
|
+
return document.getElementById('turbo-reflex').getAttribute('content')
|
65
|
+
}
|
87
66
|
}
|
@@ -0,0 +1,72 @@
|
|
1
|
+
import './turbo'
|
2
|
+
import schema from './schema.js'
|
3
|
+
import activity from './activity'
|
4
|
+
import delegates from './delegates'
|
5
|
+
import drivers from './drivers'
|
6
|
+
import elements from './elements'
|
7
|
+
import lifecycle from './lifecycle'
|
8
|
+
import logger from './logger'
|
9
|
+
import urls from './urls'
|
10
|
+
import uuids from './uuids'
|
11
|
+
|
12
|
+
function invokeReflex (event) {
|
13
|
+
let element
|
14
|
+
let payload = {}
|
15
|
+
|
16
|
+
try {
|
17
|
+
element = elements.findClosestReflex(event.target)
|
18
|
+
if (!element) return
|
19
|
+
if (!delegates.isRegistered(event.type, element.tagName)) return
|
20
|
+
|
21
|
+
const driver = drivers.find(element)
|
22
|
+
|
23
|
+
// payload sent to server (also used for lifecycle event.detail)
|
24
|
+
payload = {
|
25
|
+
id: `reflex-${uuids.v4()}`,
|
26
|
+
name: element.dataset.turboReflex,
|
27
|
+
driver: driver.name,
|
28
|
+
src: driver.src,
|
29
|
+
frameId: driver.frame ? driver.frame.id : null,
|
30
|
+
elementId: element.id.length > 0 ? element.id : null,
|
31
|
+
elementAttributes: elements.buildAttributePayload(element),
|
32
|
+
startedAt: new Date().getTime()
|
33
|
+
}
|
34
|
+
|
35
|
+
activity.add(payload)
|
36
|
+
lifecycle.dispatch(lifecycle.events.start, element, payload)
|
37
|
+
|
38
|
+
if (driver.name !== 'form') event.preventDefault()
|
39
|
+
|
40
|
+
switch (driver.name) {
|
41
|
+
case 'form':
|
42
|
+
return driver.invokeReflex(element, payload)
|
43
|
+
case 'frame':
|
44
|
+
return driver.invokeReflex(driver.frame, payload)
|
45
|
+
case 'window':
|
46
|
+
return driver.invokeReflex(payload)
|
47
|
+
}
|
48
|
+
} catch (error) {
|
49
|
+
lifecycle.dispatch(lifecycle.events.clientError, element, {
|
50
|
+
error,
|
51
|
+
...payload
|
52
|
+
})
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
// wire things up and setup defaults for event delegation
|
57
|
+
delegates.handler = invokeReflex
|
58
|
+
delegates.register('change', ['input', 'select', 'textarea'])
|
59
|
+
delegates.register('submit', ['form'])
|
60
|
+
delegates.register('click', ['*'])
|
61
|
+
|
62
|
+
export default {
|
63
|
+
schema,
|
64
|
+
logger,
|
65
|
+
registerEventDelegate: delegates.register,
|
66
|
+
get eventDelegates () {
|
67
|
+
return { ...delegates.events }
|
68
|
+
},
|
69
|
+
get lifecycleEvents () {
|
70
|
+
return [...Object.values(lifecycle.events)]
|
71
|
+
}
|
72
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import activity from './activity'
|
2
|
+
|
3
|
+
const events = {
|
4
|
+
start: 'turbo-reflex:start',
|
5
|
+
success: 'turbo-reflex:success',
|
6
|
+
finish: 'turbo-reflex:finish',
|
7
|
+
abort: 'turbo-reflex:abort',
|
8
|
+
clientError: 'turbo-reflex:client-error',
|
9
|
+
serverError: 'turbo-reflex:server-error'
|
10
|
+
}
|
11
|
+
|
12
|
+
function dispatch (name, target = document, detail = {}, raise = false) {
|
13
|
+
try {
|
14
|
+
target = target || document
|
15
|
+
const event = new CustomEvent(name, {
|
16
|
+
detail,
|
17
|
+
cancelable: false,
|
18
|
+
bubbles: true
|
19
|
+
})
|
20
|
+
target.dispatchEvent(event)
|
21
|
+
} catch (error) {
|
22
|
+
if (raise) throw error
|
23
|
+
dispatch(events.clientError, target, { error, ...detail }, true)
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
function finish (event) {
|
28
|
+
event.detail.endedAt = new Date().getTime()
|
29
|
+
event.detail.milliseconds = event.detail.endedAt - event.detail.startedAt
|
30
|
+
setTimeout(() => dispatch(events.finish, event.target, event.detail), 10)
|
31
|
+
}
|
32
|
+
|
33
|
+
addEventListener(events.serverError, finish)
|
34
|
+
addEventListener(events.success, finish)
|
35
|
+
addEventListener(events.finish, event => activity.remove(event.detail.id), true)
|
36
|
+
|
37
|
+
export default {
|
38
|
+
dispatch,
|
39
|
+
events
|
40
|
+
}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import lifecycle from './lifecycle'
|
2
|
+
|
3
|
+
let currentLevel = 'unknown'
|
4
|
+
|
5
|
+
const logLevels = {
|
6
|
+
debug: Object.values(lifecycle.events),
|
7
|
+
info: Object.values(lifecycle.events),
|
8
|
+
warn: [
|
9
|
+
lifecycle.events.abort,
|
10
|
+
lifecycle.events.clientError,
|
11
|
+
lifecycle.events.serverError
|
12
|
+
],
|
13
|
+
error: [lifecycle.events.clientError, lifecycle.events.serverError],
|
14
|
+
unknown: []
|
15
|
+
}
|
16
|
+
|
17
|
+
Object.values(lifecycle.events).forEach(name => {
|
18
|
+
addEventListener(name, event => {
|
19
|
+
if (logLevels[currentLevel].includes(event.type)) {
|
20
|
+
const level = currentLevel === 'debug' ? 'log' : currentLevel
|
21
|
+
console[level](event.type, event.detail)
|
22
|
+
}
|
23
|
+
})
|
24
|
+
})
|
25
|
+
|
26
|
+
export default {
|
27
|
+
get level () {
|
28
|
+
return currentLevel
|
29
|
+
},
|
30
|
+
set level (value) {
|
31
|
+
if (!Object.keys(logLevels).includes(value)) value = 'unknown'
|
32
|
+
return (currentLevel = value)
|
33
|
+
}
|
34
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import elements from './elements'
|
2
|
+
|
3
|
+
const frameSources = {}
|
4
|
+
|
5
|
+
// fires before making a turbo HTTP request
|
6
|
+
addEventListener('turbo:before-fetch-request', event => {
|
7
|
+
const frame = event.target.closest('turbo-frame')
|
8
|
+
const { fetchOptions } = event.detail
|
9
|
+
fetchOptions.headers['TurboReflex-Token'] = elements.metaElementToken
|
10
|
+
})
|
11
|
+
|
12
|
+
// fires after receiving a turbo HTTP response
|
13
|
+
addEventListener('turbo:before-fetch-response', event => {
|
14
|
+
const frame = event.target.closest('turbo-frame')
|
15
|
+
if (frame) frameSources[frame.id] = frame.src
|
16
|
+
})
|
17
|
+
|
18
|
+
// fires when a frame element is navigated and finishes loading
|
19
|
+
addEventListener('turbo:frame-load', event => {
|
20
|
+
const frame = event.target.closest('turbo-frame')
|
21
|
+
frame.dataset.turboReflexSrc =
|
22
|
+
frameSources[frame.id] || frame.src || frame.dataset.turboReflexSrc
|
23
|
+
delete frameSources[frame.id]
|
24
|
+
})
|
data/lib/turbo_reflex/base.rb
CHANGED
@@ -1,23 +1,77 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Reflex instances have access to the following methods and properties.
|
4
|
+
#
|
5
|
+
# * controller ........ The Rails controller processing the HTTP request
|
6
|
+
# * element ........... A struct that represents the DOM element that triggered the reflex
|
7
|
+
# * hijack_response ... Hijacks the response, must be called last (halts further request handling by the controller)
|
8
|
+
# * params ............ Reflex specific params (frame_id, element, etc.)
|
9
|
+
# * render ............ Renders Rails templates, partials, etc. (doesn't halt controller request handling)
|
10
|
+
# * renderer .......... An ActionController::Renderer
|
11
|
+
# * turbo_stream ...... A Turbo Stream TagBuilder
|
12
|
+
# * turbo_streams ..... A list of Turbo Streams to append to the response
|
13
|
+
#
|
3
14
|
class TurboReflex::Base
|
15
|
+
class << self
|
16
|
+
def response_hijackers
|
17
|
+
@response_hijackers ||= Set.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def hijack_response(options = {})
|
21
|
+
response_hijackers << options.with_indifferent_access
|
22
|
+
end
|
23
|
+
|
24
|
+
def should_hijack_response?(reflex, method_name)
|
25
|
+
method_name = method_name.to_s
|
26
|
+
match = response_hijackers.find do |options|
|
27
|
+
only = options[:only] || []
|
28
|
+
only = [only] unless only.is_a?(Array)
|
29
|
+
only.map!(&:to_s)
|
30
|
+
|
31
|
+
except = options[:except] || []
|
32
|
+
except = [except] unless except.is_a?(Array)
|
33
|
+
except.map!(&:to_s)
|
34
|
+
|
35
|
+
options.blank? || only.include?(method_name) || except.exclude?(method_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
return false if match.nil?
|
39
|
+
|
40
|
+
if match[:if].present?
|
41
|
+
case match[:if]
|
42
|
+
when Symbol then reflex.public_send(match[:if])
|
43
|
+
when Proc then reflex.instance_exec { match[:if].call reflex }
|
44
|
+
end
|
45
|
+
elsif match[:unless].present?
|
46
|
+
case match[:unless]
|
47
|
+
when Symbol then !reflex.public_send(match[:unless])
|
48
|
+
when Proc then !(reflex.instance_exec { match[:unless].call(reflex) })
|
49
|
+
end
|
50
|
+
else
|
51
|
+
true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
4
56
|
attr_reader :controller, :turbo_streams
|
5
57
|
|
6
58
|
delegate :render, to: :renderer
|
59
|
+
delegate :hijack_response, :turbo_stream, to: :@runner
|
7
60
|
|
8
|
-
def initialize(
|
9
|
-
@
|
61
|
+
def initialize(runner)
|
62
|
+
@runner = runner
|
63
|
+
@controller = runner.controller
|
10
64
|
@turbo_streams = Set.new
|
11
65
|
end
|
12
66
|
|
13
67
|
def params
|
14
|
-
|
68
|
+
@runner.reflex_params
|
15
69
|
end
|
16
70
|
|
17
71
|
def element
|
18
72
|
@element ||= begin
|
19
|
-
keys = params[:
|
20
|
-
values = params[:
|
73
|
+
keys = params[:element_attributes].keys.map { |key| key.to_s.parameterize.underscore.to_sym }
|
74
|
+
values = params[:element_attributes].values
|
21
75
|
|
22
76
|
unless keys.include? :value
|
23
77
|
keys << :value
|
@@ -28,10 +82,6 @@ class TurboReflex::Base
|
|
28
82
|
end
|
29
83
|
end
|
30
84
|
|
31
|
-
def turbo_stream
|
32
|
-
@turbo_stream ||= Turbo::Streams::TagBuilder.new(controller.view_context)
|
33
|
-
end
|
34
|
-
|
35
85
|
def renderer
|
36
86
|
ActionController::Renderer.for controller.class, controller.request.env
|
37
87
|
end
|
data/lib/turbo_reflex/engine.rb
CHANGED
@@ -2,23 +2,15 @@
|
|
2
2
|
|
3
3
|
require "turbo-rails"
|
4
4
|
require_relative "version"
|
5
|
-
require_relative "errors"
|
6
5
|
require_relative "sanitizer"
|
6
|
+
require_relative "runner"
|
7
7
|
require_relative "base"
|
8
8
|
|
9
9
|
class TurboReflex::Engine < ::Rails::Engine
|
10
|
-
# isolate_namespace TurboReflex
|
11
|
-
|
12
10
|
config.turbo_reflex = ActiveSupport::OrderedOptions.new
|
13
11
|
initializer "turbo_reflex.configuration" do
|
14
12
|
config.to_prepare do |app|
|
15
13
|
::ActionController::Base.send :include, TurboReflex::Controller
|
16
14
|
end
|
17
|
-
|
18
|
-
config.after_initialize do |app|
|
19
|
-
app.routes.draw do
|
20
|
-
mount TurboReflex::Engine => "/turbo_reflex"
|
21
|
-
end
|
22
|
-
end
|
23
15
|
end
|
24
16
|
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TurboReflex::Runner
|
4
|
+
attr_reader :controller
|
5
|
+
|
6
|
+
delegate_missing_to :controller
|
7
|
+
|
8
|
+
def initialize(controller)
|
9
|
+
@controller = controller
|
10
|
+
end
|
11
|
+
|
12
|
+
def meta_tag
|
13
|
+
masked_token = message_verifier.generate(new_token)
|
14
|
+
options = {
|
15
|
+
id: "turbo-reflex",
|
16
|
+
name: "turbo-reflex",
|
17
|
+
content: masked_token,
|
18
|
+
data: {busy: false}
|
19
|
+
}
|
20
|
+
view_context.tag("meta", options).html_safe
|
21
|
+
end
|
22
|
+
|
23
|
+
def reflex_requested?
|
24
|
+
reflex_params.present?
|
25
|
+
end
|
26
|
+
|
27
|
+
def reflex_valid?
|
28
|
+
return false unless reflex_requested?
|
29
|
+
return false unless valid_client_token?
|
30
|
+
return false unless reflex_instance.is_a?(TurboReflex::Base)
|
31
|
+
reflex_instance.respond_to? reflex_method_name
|
32
|
+
end
|
33
|
+
|
34
|
+
def reflex_params
|
35
|
+
return ActionController::Parameters.new if params[:turbo_reflex].nil?
|
36
|
+
@reflex_params ||= begin
|
37
|
+
payload = parsed_reflex_params.deep_transform_keys(&:underscore)
|
38
|
+
ActionController::Parameters.new(payload).permit!
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def reflex_element
|
43
|
+
return nil if reflex_params.blank?
|
44
|
+
@reflex_element ||= Struct
|
45
|
+
.new(*reflex_params[:element_attributes].keys.map { |key| key.to_s.parameterize.underscore.to_sym })
|
46
|
+
.new(*reflex_params[:element_attributes].values)
|
47
|
+
end
|
48
|
+
|
49
|
+
def reflex_name
|
50
|
+
return nil unless reflex_requested?
|
51
|
+
reflex_params[:name]
|
52
|
+
end
|
53
|
+
|
54
|
+
def reflex_class_name
|
55
|
+
return nil unless reflex_requested?
|
56
|
+
reflex_name.split("#").first
|
57
|
+
end
|
58
|
+
|
59
|
+
def reflex_method_name
|
60
|
+
return nil unless reflex_requested?
|
61
|
+
reflex_name.split("#").last
|
62
|
+
end
|
63
|
+
|
64
|
+
def reflex_class
|
65
|
+
@reflex_class ||= reflex_class_name&.safe_constantize
|
66
|
+
end
|
67
|
+
|
68
|
+
def reflex_instance
|
69
|
+
@reflex_instance ||= reflex_class&.new(self)
|
70
|
+
end
|
71
|
+
|
72
|
+
def reflex_performed?
|
73
|
+
!!@reflex_performed
|
74
|
+
end
|
75
|
+
|
76
|
+
def reflex_errored?
|
77
|
+
!!@reflex_errored
|
78
|
+
end
|
79
|
+
|
80
|
+
def reflex_succeeded?
|
81
|
+
reflex_performed? && !reflex_errored?
|
82
|
+
end
|
83
|
+
|
84
|
+
def run
|
85
|
+
return unless reflex_valid?
|
86
|
+
return if reflex_performed?
|
87
|
+
@reflex_performed = true
|
88
|
+
reflex_instance.public_send reflex_method_name
|
89
|
+
hijack_response if reflex_class.should_hijack_response?(reflex_instance, reflex_method_name)
|
90
|
+
rescue => e
|
91
|
+
response.status = :internal_server_error
|
92
|
+
@reflex_errored = true
|
93
|
+
message = "Error in #{reflex_name}! #{e.inspect}"
|
94
|
+
Rails.logger.error message
|
95
|
+
append_error_event_to_response_body message
|
96
|
+
end
|
97
|
+
|
98
|
+
def hijack_response
|
99
|
+
response.set_header "TurboReflex-Hijacked", true
|
100
|
+
render html: "", layout: false
|
101
|
+
append_to_response
|
102
|
+
end
|
103
|
+
|
104
|
+
def append_to_response
|
105
|
+
append_turbo_streams_to_response_body
|
106
|
+
append_meta_tag_to_response_body
|
107
|
+
append_success_event_to_response_body
|
108
|
+
end
|
109
|
+
|
110
|
+
def turbo_stream
|
111
|
+
@turbo_stream ||= Turbo::Streams::TagBuilder.new(controller.view_context)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def parsed_reflex_params
|
117
|
+
@parsed_reflex_params ||= JSON.parse(params[:turbo_reflex])
|
118
|
+
end
|
119
|
+
|
120
|
+
def message_verifier
|
121
|
+
ActiveSupport::MessageVerifier.new session.id.to_s, digest: "SHA256"
|
122
|
+
end
|
123
|
+
|
124
|
+
def content_sanitizer
|
125
|
+
TurboReflex::Sanitizer.instance
|
126
|
+
end
|
127
|
+
|
128
|
+
def new_token
|
129
|
+
@new_token ||= SecureRandom.urlsafe_base64(32)
|
130
|
+
end
|
131
|
+
|
132
|
+
def server_token
|
133
|
+
session[:turbo_reflex_token]
|
134
|
+
end
|
135
|
+
|
136
|
+
def client_token
|
137
|
+
(request.headers["TurboReflex-Token"] || reflex_params[:token]).to_s
|
138
|
+
end
|
139
|
+
|
140
|
+
def valid_client_token?
|
141
|
+
return false unless client_token.present?
|
142
|
+
return false unless message_verifier.valid_message?(client_token)
|
143
|
+
unmasked_client_token = message_verifier.verify(client_token)
|
144
|
+
unmasked_client_token == server_token
|
145
|
+
end
|
146
|
+
|
147
|
+
def response_type
|
148
|
+
body = response.body.to_s.strip
|
149
|
+
return :body if body.match?(/<\/\s*body.*>/i)
|
150
|
+
return :frame if body.match?(/<\/\s*turbo-frame.*>/i)
|
151
|
+
return :stream if body.match?(/<\/\s*turbo-stream.*>/i)
|
152
|
+
:unknown
|
153
|
+
end
|
154
|
+
|
155
|
+
def append_turbo_streams_to_response_body
|
156
|
+
return unless reflex_succeeded?
|
157
|
+
return unless reflex_instance&.turbo_streams.present?
|
158
|
+
append_to_response_body reflex_instance.turbo_streams.map(&:to_s).join.html_safe
|
159
|
+
end
|
160
|
+
|
161
|
+
def append_meta_tag_to_response_body
|
162
|
+
session[:turbo_reflex_token] = new_token
|
163
|
+
append_to_response_body turbo_stream.replace("turbo-reflex", meta_tag)
|
164
|
+
end
|
165
|
+
|
166
|
+
def append_success_event_to_response_body
|
167
|
+
return unless reflex_succeeded?
|
168
|
+
args = ["turbo-reflex:success", {bubbles: true, cancelable: false, detail: parsed_reflex_params}]
|
169
|
+
event = if reflex_element.try(:id).present?
|
170
|
+
turbo_stream.invoke :dispatch_event, args: args, selector: "##{reflex_element.id}"
|
171
|
+
else
|
172
|
+
turbo_stream.invoke :dispatch_event, args: args
|
173
|
+
end
|
174
|
+
append_to_response_body event
|
175
|
+
end
|
176
|
+
|
177
|
+
def append_error_event_to_response_body(message)
|
178
|
+
return unless reflex_errored?
|
179
|
+
args = ["turbo-reflex:server-error", {bubbles: true, cancelable: false, detail: parsed_reflex_params.merge(error: message)}]
|
180
|
+
event = if reflex_element.try(:id).present?
|
181
|
+
turbo_stream.invoke :dispatch_event, args: args, selector: "##{reflex_element.id}"
|
182
|
+
else
|
183
|
+
turbo_stream.invoke :dispatch_event, args: args
|
184
|
+
end
|
185
|
+
append_to_response_body event
|
186
|
+
end
|
187
|
+
|
188
|
+
def append_to_response_body(content)
|
189
|
+
sanitized_content = content_sanitizer.sanitize(content).html_safe
|
190
|
+
|
191
|
+
return if sanitized_content.blank?
|
192
|
+
|
193
|
+
case response_type
|
194
|
+
when :body then response.body.sub!(/<\/\s*body.*>/i, "#{sanitized_content}</body>")
|
195
|
+
when :frame then response.body.sub!(/<\/\s*turbo-frame.*>/i, "#{sanitized_content}</turbo-frame>")
|
196
|
+
else response.body << sanitized_content
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/lib/turbo_reflex/version.rb
CHANGED
data/package.json
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
{
|
2
2
|
"name": "turbo_reflex",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.6",
|
4
4
|
"description": "Reflexes for Turbo Frames that help you build robust reactive applications",
|
5
|
-
"main": "app/javascript/
|
5
|
+
"main": "app/javascript/index.js",
|
6
6
|
"repository": "https://github.com/hopsoft/turbo_reflex",
|
7
7
|
"author": "Nate Hopkins (hopsoft) <natehop@gmail.com>",
|
8
8
|
"license": "MIT",
|
9
9
|
"peerDependencies": {
|
10
|
-
"@hotwired/turbo-rails": ">= 7.1"
|
10
|
+
"@hotwired/turbo-rails": ">= 7.1",
|
11
|
+
"turbo_ready": ">= 0.1"
|
11
12
|
},
|
12
13
|
"devDependencies": {
|
13
14
|
"esbuild": "^0.15.7",
|
@@ -17,6 +18,6 @@
|
|
17
18
|
"rustywind": "^0.15.1"
|
18
19
|
},
|
19
20
|
"scripts": {
|
20
|
-
"build": "esbuild app/javascript/
|
21
|
+
"build": "esbuild app/javascript/index.js --bundle --minify --sourcemap --format=esm --outfile=app/assets/builds/turbo_reflex.js"
|
21
22
|
}
|
22
23
|
}
|
data/turbo_reflex.gemspec
CHANGED