fie 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Fie
2
+ VERSION = "0.1.9"
3
+ end
@@ -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>
@@ -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,5 @@
1
+ module Fie
2
+ module Native
3
+ require_tree './native'
4
+ end
5
+ 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