fie 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +48 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/fie.rb +5 -0
- data/lib/fie/commander.rb +118 -0
- data/lib/fie/commander_closed.rb +1 -0
- data/lib/fie/manipulator.rb +39 -0
- data/lib/fie/pools.rb +23 -0
- data/lib/fie/railtie.rb +13 -0
- data/lib/fie/state.rb +131 -0
- data/lib/fie/state/changelog.rb +48 -0
- data/lib/fie/state/track.rb +167 -0
- data/lib/fie/version.rb +3 -0
- data/lib/layouts/fie.html.erb +12 -0
- data/lib/opal/fie.rb +21 -0
- data/lib/opal/fie/cable.rb +63 -0
- data/lib/opal/fie/commander.rb +43 -0
- data/lib/opal/fie/listeners.rb +109 -0
- data/lib/opal/fie/native.rb +5 -0
- data/lib/opal/fie/native/action_cable_channel.rb +34 -0
- data/lib/opal/fie/native/element.rb +112 -0
- data/lib/opal/fie/native/event.rb +17 -0
- data/lib/opal/fie/native/timeout.rb +13 -0
- data/lib/opal/fie/pool.rb +9 -0
- data/lib/opal/fie/util.rb +23 -0
- data/vendor/javascript/fie.js +1 -0
- metadata +234 -0
@@ -0,0 +1,167 @@
|
|
1
|
+
module Fie
|
2
|
+
module Track
|
3
|
+
def track_changes_in_objects(object)
|
4
|
+
if object.is_a?(Array)
|
5
|
+
track_changes_in_array(object)
|
6
|
+
elsif object.is_a?(Hash)
|
7
|
+
track_changes_in_hash(object)
|
8
|
+
elsif !object.duplicable? || object.is_a?(String) || object.is_a?(Time)
|
9
|
+
nil
|
10
|
+
else
|
11
|
+
track_changes_in_object(object)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def untrack_changes_in_objects(object)
|
16
|
+
if object.is_a? Hash
|
17
|
+
untrack_changes_in_hash(object)
|
18
|
+
elsif object.is_a? Array
|
19
|
+
untrack_changes_in_array(object)
|
20
|
+
elsif !object.duplicable? || object.is_a?(String) || object.is_a?(Time)
|
21
|
+
nil
|
22
|
+
else
|
23
|
+
untrack_changes_in_object(object)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def track_changes_in_array(object)
|
29
|
+
state = self
|
30
|
+
|
31
|
+
unless object.frozen?
|
32
|
+
object.class_eval do
|
33
|
+
alias_method('previous_[]=', '[]=')
|
34
|
+
alias_method('previous_<<', '<<')
|
35
|
+
alias_method('previous_push', 'push')
|
36
|
+
|
37
|
+
define_method('[]=') do |key, value|
|
38
|
+
send('previous_[]=', key, value)
|
39
|
+
state.permeate
|
40
|
+
end
|
41
|
+
|
42
|
+
define_method('<<') do |value|
|
43
|
+
send('previous_<<', value)
|
44
|
+
state.permeate
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
define_method('push') do |value|
|
49
|
+
send('previous_push', value)
|
50
|
+
state.permeate
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
object.each do |value|
|
56
|
+
track_changes_in_objects(value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def track_changes_in_hash(object)
|
61
|
+
state = self
|
62
|
+
|
63
|
+
unless object.frozen?
|
64
|
+
object.class_eval do
|
65
|
+
alias_method('previous_[]=', '[]=')
|
66
|
+
define_method('[]=') do |key, value|
|
67
|
+
send('previous_[]=', key, value)
|
68
|
+
state.permeate
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
object.each do |value|
|
74
|
+
track_changes_in_objects(value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def track_changes_in_object(object)
|
79
|
+
state = self
|
80
|
+
|
81
|
+
object.methods.each do |attribute_name, attribute_value|
|
82
|
+
is_setter = attribute_name.to_s.ends_with?('=') && attribute_name.to_s.match(/[A-Za-z]/)
|
83
|
+
|
84
|
+
if is_setter
|
85
|
+
unless object.frozen?
|
86
|
+
object.class_eval do
|
87
|
+
alias_method("previous_#{attribute_name}", attribute_name)
|
88
|
+
define_method(attribute_name) do |setter_value|
|
89
|
+
send("previous_#{attribute_name}", setter_value)
|
90
|
+
state.permeate
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
getter_name = attribute_name.to_s.chomp('=').to_sym
|
96
|
+
object_has_getter = object.methods.include?(getter_name)
|
97
|
+
if object_has_getter
|
98
|
+
track_changes_in_objects object.send(getter_name)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def untrack_changes_in_hash(hash)
|
105
|
+
unless hash.frozen?
|
106
|
+
hash.class_eval do
|
107
|
+
begin
|
108
|
+
remove_method :'previous_[]='
|
109
|
+
remove_method :'[]='
|
110
|
+
rescue
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
hash.each do |key, value|
|
116
|
+
untrack_changes_in_objects(value)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def untrack_changes_in_array(array)
|
121
|
+
unless array.frozen?
|
122
|
+
array.class_eval do
|
123
|
+
begin
|
124
|
+
remove_method :'previous_[]='
|
125
|
+
remove_method :'previous_<<'
|
126
|
+
remove_method :previous_push
|
127
|
+
remove_method :'[]='
|
128
|
+
remove_method :'<<'
|
129
|
+
remove_method :push
|
130
|
+
rescue
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
array.each do |value|
|
136
|
+
untrack_changes_in_objects(value)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def untrack_changes_in_object(object)
|
141
|
+
object.methods.each do |attribute_name, attribute_value|
|
142
|
+
is_setter =
|
143
|
+
attribute_name.to_s.ends_with?('=') &&
|
144
|
+
attribute_name.to_s.match(/[A-Za-z]/) &&
|
145
|
+
!attribute_name.to_s.start_with?('previous_')
|
146
|
+
|
147
|
+
if is_setter
|
148
|
+
unless object.frozen?
|
149
|
+
object.class_eval do
|
150
|
+
begin
|
151
|
+
remove_method :"previous_#{attribute_name}"
|
152
|
+
remove_method :"#{attribute_name}"
|
153
|
+
rescue
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
getter_name = attribute_name.to_s.chomp('=').to_sym
|
159
|
+
object_has_getter = object.methods.include?(getter_name)
|
160
|
+
if object_has_getter
|
161
|
+
untrack_changes_in_objects object.send(getter_name)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
data/lib/fie/version.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
<span fie-body='true'>
|
2
|
+
<%= yield %>
|
3
|
+
|
4
|
+
<% assigns.symbolize_keys.map do |assigns_name, assigns_value| %>
|
5
|
+
<% unless assigns_value.blank? || assigns_name == :marked_for_same_origin_verification %>
|
6
|
+
<% encryptor = ActiveSupport::MessageEncryptor.new Rails.application.credentials[:secret_key_base] %>
|
7
|
+
<span fie-variable='<%= assigns_name %>' fie-value='<%= encryptor.encrypt_and_sign Marshal.dump(assigns_value) %>'></span>
|
8
|
+
<% end %>
|
9
|
+
<% end %>
|
10
|
+
|
11
|
+
<span fie-controller='<%= params[:controller] || @fie_controller_name %>' fie-action='<%= params[:action] || @fie_action_name %>'></span>
|
12
|
+
</span>
|
data/lib/opal/fie.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'opal'
|
2
|
+
|
3
|
+
module Fie
|
4
|
+
require_tree './fie'
|
5
|
+
|
6
|
+
include Fie::Native
|
7
|
+
|
8
|
+
Native(`window.Fie = {}`)
|
9
|
+
Native \
|
10
|
+
`window.Fie.addEventListener =
|
11
|
+
#{
|
12
|
+
-> (event_name, selector, block) do
|
13
|
+
Element.fie_body.add_event_listener(event_name, selector, &block)
|
14
|
+
end
|
15
|
+
}`
|
16
|
+
|
17
|
+
Element.document.add_event_listener('DOMContentLoaded') do
|
18
|
+
cable = Cable.new
|
19
|
+
Listeners.new(cable)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'fie/native'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Fie
|
6
|
+
class Cable
|
7
|
+
include Fie::Native
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
connection_uuid = SecureRandom.uuid
|
11
|
+
commander_name = "#{ camelize(controller_name) }Commander"
|
12
|
+
|
13
|
+
@commander = Commander.new(channel_name: commander_name, identifier: connection_uuid, cable: self)
|
14
|
+
@pools = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def call_remote_function(element:, event_name:, function_name:, parameters:)
|
18
|
+
log_event(element: element, event_name: event_name, function_name: function_name, parameters: parameters)
|
19
|
+
|
20
|
+
function_parameters = {
|
21
|
+
caller: {
|
22
|
+
id: element.id,
|
23
|
+
class: element.class_name,
|
24
|
+
value: element.value
|
25
|
+
},
|
26
|
+
controller_name: controller_name,
|
27
|
+
action_name: action_name
|
28
|
+
}.merge(parameters)
|
29
|
+
|
30
|
+
@commander.perform(function_name, function_parameters)
|
31
|
+
end
|
32
|
+
|
33
|
+
def subscribe_to_pool(subject)
|
34
|
+
@pools['subject'] = Pool.new(channel_name: 'Fie::Pools', identifier: subject, cable: self)
|
35
|
+
end
|
36
|
+
|
37
|
+
def commander
|
38
|
+
@commander
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def log_event(element:, event_name:, function_name:, parameters:)
|
43
|
+
parameters = parameters.to_json
|
44
|
+
puts "Event #{ event_name } triggered by element #{ element.descriptor } is calling function #{ function_name } with parameters #{ parameters }"
|
45
|
+
end
|
46
|
+
|
47
|
+
def action_name
|
48
|
+
view_name_element['fie-action']
|
49
|
+
end
|
50
|
+
|
51
|
+
def controller_name
|
52
|
+
view_name_element['fie-controller']
|
53
|
+
end
|
54
|
+
|
55
|
+
def view_name_element
|
56
|
+
Element.body.query_selector('[fie-controller]:not([fie-controller=""])');
|
57
|
+
end
|
58
|
+
|
59
|
+
def camelize(string)
|
60
|
+
string.split('_').collect(&:capitalize).join
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'fie/native'
|
2
|
+
require 'diffhtml'
|
3
|
+
|
4
|
+
module Fie
|
5
|
+
class Commander < Fie::Native::ActionCableChannel
|
6
|
+
include Fie::Native
|
7
|
+
|
8
|
+
def connected
|
9
|
+
super
|
10
|
+
|
11
|
+
@cable.call_remote_function \
|
12
|
+
element: Element.body,
|
13
|
+
function_name: 'initialize_state',
|
14
|
+
event_name: 'Upload State',
|
15
|
+
parameters: { view_variables: Util.view_variables }
|
16
|
+
end
|
17
|
+
|
18
|
+
def received(data)
|
19
|
+
process_command(data['command'], data['parameters'])
|
20
|
+
end
|
21
|
+
|
22
|
+
def process_command(command, parameters = {})
|
23
|
+
case command
|
24
|
+
when 'refresh_view'
|
25
|
+
$$.diff.innerHTML(Element.fie_body.unwrapped_element, parameters['html'])
|
26
|
+
@event.dispatch
|
27
|
+
when 'subscribe_to_pools'
|
28
|
+
parameters['subjects'].each do |subject|
|
29
|
+
@cable.subscribe_to_pool(subject)
|
30
|
+
end
|
31
|
+
when 'publish_to_pools'
|
32
|
+
subject = parameters['subject']
|
33
|
+
object = parameters['object']
|
34
|
+
|
35
|
+
perform("pool_#{ subject }_callback", { object: object })
|
36
|
+
when 'execute_function'
|
37
|
+
Util.exec_js(parameters['name'], parameters['arguments'])
|
38
|
+
else
|
39
|
+
console.log("Command: #{ command }, Parameters: #{ parameters }")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'fie/native'
|
2
|
+
|
3
|
+
module Fie
|
4
|
+
class Listeners
|
5
|
+
include Fie::Native
|
6
|
+
|
7
|
+
def initialize(cable)
|
8
|
+
@cable = cable
|
9
|
+
|
10
|
+
initialize_input_elements
|
11
|
+
initialize_fie_events [:click, :submit, :scroll, :keyup, :keydown, :enter]
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def initialize_fie_events(event_names)
|
16
|
+
event_names.each do |fie_event_name|
|
17
|
+
event_name = fie_event_name
|
18
|
+
event_name = :keydown if event_name == :enter
|
19
|
+
|
20
|
+
Element.fie_body.add_event_listener(event_name, "[fie-#{ fie_event_name }]:not([fie-#{ fie_event_name }=''])") do |event|
|
21
|
+
event_is_valid = (fie_event_name == :enter && event.keyCode == 13) || fie_event_name != :enter
|
22
|
+
|
23
|
+
if event_is_valid
|
24
|
+
element = Element.new(element: event.target)
|
25
|
+
remote_function_name = element["fie-#{ fie_event_name }"]
|
26
|
+
function_parameters = JSON.parse(element['fie-parameters'] || {})
|
27
|
+
|
28
|
+
@cable.call_remote_function \
|
29
|
+
element: element,
|
30
|
+
function_name: remote_function_name,
|
31
|
+
event_name: event_name,
|
32
|
+
parameters: function_parameters
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize_input_elements
|
39
|
+
timer = Timeout.new(0) { }
|
40
|
+
|
41
|
+
typing_input_types = ['text', 'password', 'search', 'tel', 'url']
|
42
|
+
|
43
|
+
typing_input_selector = (['textarea'] + typing_input_types).reduce do |selector, input_type|
|
44
|
+
selector += ", input[type=#{ input_type }]"
|
45
|
+
end
|
46
|
+
|
47
|
+
non_typing_input_selector = (['input'] + typing_input_types).reduce do |selector, input_type|
|
48
|
+
selector += ":not([type=#{ input_type }])"
|
49
|
+
end
|
50
|
+
|
51
|
+
Element.fie_body.add_event_listener('keydown', typing_input_selector) do |event|
|
52
|
+
timer.clear
|
53
|
+
|
54
|
+
input_element = Element.new(element: event.target)
|
55
|
+
|
56
|
+
timer = Timeout.new(500) do
|
57
|
+
update_state_using_changelog(input_element)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Element.fie_body.add_event_listener('change', non_typing_input_selector) do |event|
|
62
|
+
input_element = Element.new(element: event.target)
|
63
|
+
update_state_using_changelog(input_element)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def update_state_using_changelog(input_element)
|
68
|
+
objects_changelog = {}
|
69
|
+
|
70
|
+
changed_object_name = Regexp.new('(?:^|])([^[\\]]+)(?:\\[|$)').match(input_element.name)[0][0..-2]
|
71
|
+
changed_object_key_chain = input_element.name.scan(Regexp.new '(?<=\\[).+?(?=\\])')
|
72
|
+
|
73
|
+
is_form_object = !changed_object_key_chain.empty? && !changed_object_name.nil?
|
74
|
+
is_fie_nested_object = Util.view_variables.include? changed_object_name
|
75
|
+
is_fie_form_object = is_form_object && is_fie_nested_object
|
76
|
+
|
77
|
+
is_fie_non_nested_object = Util.view_variables.include? input_element.name
|
78
|
+
|
79
|
+
if is_fie_form_object
|
80
|
+
build_changelog(changed_object_key_chain, changed_object_name, objects_changelog, input_element)
|
81
|
+
elsif is_fie_non_nested_object
|
82
|
+
objects_changelog[input_element.name] = input_element.value;
|
83
|
+
end
|
84
|
+
|
85
|
+
@cable.call_remote_function \
|
86
|
+
element: input_element,
|
87
|
+
function_name: 'modify_state_using_changelog',
|
88
|
+
event_name: 'Input Element Change',
|
89
|
+
parameters: { objects_changelog: objects_changelog }
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_changelog(object_key_chain, object_name, changelog, input_element)
|
93
|
+
is_final_key = -> (key) { key == object_key_chain[-1] }
|
94
|
+
object_final_key_value = input_element.value
|
95
|
+
|
96
|
+
changelog[object_name] = {}
|
97
|
+
changelog = changelog[object_name]
|
98
|
+
|
99
|
+
object_key_chain.each do |key|
|
100
|
+
if is_final_key.call(key)
|
101
|
+
changelog[key] = object_final_key_value
|
102
|
+
else
|
103
|
+
changelog[key] = {}
|
104
|
+
changelog = changelog[key]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'fie/native'
|
2
|
+
|
3
|
+
module Fie
|
4
|
+
module Native
|
5
|
+
class ActionCableChannel
|
6
|
+
def initialize(channel_name:, identifier:, cable:)
|
7
|
+
@channel_name = channel_name
|
8
|
+
@identifier = identifier
|
9
|
+
@cable = cable
|
10
|
+
@event = Event.new('fieChanged')
|
11
|
+
|
12
|
+
@subscription = $$.App.cable.subscriptions.create(
|
13
|
+
{ channel: @channel_name, identifier: @identifier },
|
14
|
+
{
|
15
|
+
connected: -> { connected },
|
16
|
+
received: -> (data) { received Native(`#{data}`) }
|
17
|
+
}
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def responds_to?(t)
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def connected
|
26
|
+
puts "Connected to #{ @channel_name } with identifier #{ @identifier }"
|
27
|
+
end
|
28
|
+
|
29
|
+
def perform(function_name, parameters = {})
|
30
|
+
@subscription.perform(function_name, parameters);
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|