turbo_reflex 0.0.5 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -3
- 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 +2 -10
- 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 +146 -155
- 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
|
-
::
|
16
|
-
end
|
17
|
-
|
18
|
-
config.after_initialize do |app|
|
19
|
-
app.routes.draw do
|
20
|
-
mount TurboReflex::Engine => "/turbo_reflex"
|
21
|
-
end
|
13
|
+
::ActionController::Base.send :include, TurboReflex::Controller
|
22
14
|
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