volt 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +37 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +23 -0
- data/Readme.md +34 -0
- data/VERSION +1 -0
- data/bin/volt +4 -0
- data/docs/GETTING_STARTED.md +7 -0
- data/docs/GUIDE.md +33 -0
- data/lib/volt.rb +15 -0
- data/lib/volt/benchmark/benchmark.rb +25 -0
- data/lib/volt/cli.rb +34 -0
- data/lib/volt/console.rb +19 -0
- data/lib/volt/controllers/model_controller.rb +29 -0
- data/lib/volt/extra_core/array.rb +10 -0
- data/lib/volt/extra_core/blank.rb +88 -0
- data/lib/volt/extra_core/extra_core.rb +7 -0
- data/lib/volt/extra_core/numeric.rb +9 -0
- data/lib/volt/extra_core/object.rb +36 -0
- data/lib/volt/extra_core/string.rb +29 -0
- data/lib/volt/extra_core/stringify_keys.rb +7 -0
- data/lib/volt/extra_core/true_false.rb +44 -0
- data/lib/volt/extra_core/try.rb +31 -0
- data/lib/volt/models.rb +5 -0
- data/lib/volt/models/array_model.rb +37 -0
- data/lib/volt/models/model.rb +210 -0
- data/lib/volt/models/model_wrapper.rb +23 -0
- data/lib/volt/models/params.rb +67 -0
- data/lib/volt/models/url.rb +192 -0
- data/lib/volt/page/url_tracker.rb +36 -0
- data/lib/volt/reactive/array_extensions.rb +13 -0
- data/lib/volt/reactive/event_chain.rb +126 -0
- data/lib/volt/reactive/events.rb +283 -0
- data/lib/volt/reactive/object_tracker.rb +99 -0
- data/lib/volt/reactive/object_tracking.rb +15 -0
- data/lib/volt/reactive/reactive_array.rb +222 -0
- data/lib/volt/reactive/reactive_tags.rb +64 -0
- data/lib/volt/reactive/reactive_value.rb +368 -0
- data/lib/volt/reactive/string_extensions.rb +34 -0
- data/lib/volt/router/routes.rb +83 -0
- data/lib/volt/server.rb +121 -0
- data/lib/volt/server/binding_setup.rb +2 -0
- data/lib/volt/server/channel_handler.rb +31 -0
- data/lib/volt/server/component_handler.rb +88 -0
- data/lib/volt/server/if_binding_setup.rb +29 -0
- data/lib/volt/server/request_handler.rb +16 -0
- data/lib/volt/server/scope.rb +43 -0
- data/lib/volt/server/source_map_server.rb +31 -0
- data/lib/volt/server/template_parser.rb +452 -0
- data/lib/volt/store/mongo.rb +5 -0
- data/lib/volt/templates/attribute_binding.rb +110 -0
- data/lib/volt/templates/base_binding.rb +37 -0
- data/lib/volt/templates/channel.rb +48 -0
- data/lib/volt/templates/content_binding.rb +35 -0
- data/lib/volt/templates/document_events.rb +80 -0
- data/lib/volt/templates/each_binding.rb +115 -0
- data/lib/volt/templates/event_binding.rb +51 -0
- data/lib/volt/templates/if_binding.rb +74 -0
- data/lib/volt/templates/memory_test.rb +26 -0
- data/lib/volt/templates/page.rb +146 -0
- data/lib/volt/templates/reactive_template.rb +38 -0
- data/lib/volt/templates/render_queue.rb +5 -0
- data/lib/volt/templates/sub_context.rb +23 -0
- data/lib/volt/templates/targets/attribute_section.rb +33 -0
- data/lib/volt/templates/targets/attribute_target.rb +18 -0
- data/lib/volt/templates/targets/base_section.rb +14 -0
- data/lib/volt/templates/targets/binding_document/base_node.rb +3 -0
- data/lib/volt/templates/targets/binding_document/component_node.rb +112 -0
- data/lib/volt/templates/targets/binding_document/html_node.rb +11 -0
- data/lib/volt/templates/targets/dom_section.rb +147 -0
- data/lib/volt/templates/targets/dom_target.rb +11 -0
- data/lib/volt/templates/template_binding.rb +159 -0
- data/lib/volt/templates/template_renderer.rb +50 -0
- data/spec/models/event_chain_spec.rb +129 -0
- data/spec/models/model_spec.rb +340 -0
- data/spec/models/old_model_spec.rb +109 -0
- data/spec/models/reactive_array_spec.rb +262 -0
- data/spec/models/reactive_tags_spec.rb +35 -0
- data/spec/models/reactive_value_spec.rb +336 -0
- data/spec/models/string_extensions_spec.rb +57 -0
- data/spec/router/routes_spec.rb +24 -0
- data/spec/server/template_parser_spec.rb +50 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/store/mongo_spec.rb +4 -0
- data/spec/templates/targets/binding_document/component_node_spec.rb +18 -0
- data/spec/templates/template_binding_spec.rb +98 -0
- data/templates/.gitignore +12 -0
- data/templates/Gemfile.tt +8 -0
- data/templates/app/.empty_directory +0 -0
- data/templates/app/home/config/routes.rb +1 -0
- data/templates/app/home/controllers/index_controller.rb +5 -0
- data/templates/app/home/css/.empty_directory +0 -0
- data/templates/app/home/models/.empty_directory +0 -0
- data/templates/app/home/views/index/about.html +9 -0
- data/templates/app/home/views/index/home.html +7 -0
- data/templates/app/home/views/index/index.html +28 -0
- data/templates/config.ru +4 -0
- data/templates/public/css/ansi.css +0 -0
- data/templates/public/css/bootstrap-theme.css +459 -0
- data/templates/public/css/bootstrap.css +7098 -0
- data/templates/public/css/jumbotron.css +79 -0
- data/templates/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/templates/public/fonts/glyphicons-halflings-regular.svg +229 -0
- data/templates/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/templates/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/templates/public/index.html +25 -0
- data/templates/public/js/bootstrap.js +0 -0
- data/templates/public/js/jquery-2.0.3.js +8829 -0
- data/templates/public/js/sockjs-0.2.1.min.js +27 -0
- data/templates/spec/spec_helper.rb +20 -0
- data/volt.gemspec +41 -0
- metadata +412 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
# All url related data is stored in params. This includes the main uri
|
2
|
+
# in addition to any query parameters. The router is responsible for
|
3
|
+
# converting any uri sections into params. Sections in the uri will
|
4
|
+
# override any specified parameters.
|
5
|
+
#
|
6
|
+
# The params value can be updated the same way a model would be, only
|
7
|
+
# the updates will trigger an updated url via the browser history api.
|
8
|
+
# TODO: Support # for browsers without the history api.
|
9
|
+
|
10
|
+
class Params < Model
|
11
|
+
def initialize(*args)
|
12
|
+
super(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def deep_clone
|
16
|
+
new_obj = clone
|
17
|
+
|
18
|
+
new_obj.attributes = new_obj.attributes.dup
|
19
|
+
|
20
|
+
new_obj
|
21
|
+
end
|
22
|
+
|
23
|
+
tag_method(:delete) do
|
24
|
+
destructive!
|
25
|
+
end
|
26
|
+
def delete(*args)
|
27
|
+
super
|
28
|
+
|
29
|
+
value_updated
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(method_name, *args, &block)
|
33
|
+
result = super
|
34
|
+
|
35
|
+
if method_name[0] == '_' && method_name[-1] == '='
|
36
|
+
# Trigger value updated after an assignment
|
37
|
+
self.value_updated
|
38
|
+
end
|
39
|
+
|
40
|
+
return result
|
41
|
+
end
|
42
|
+
|
43
|
+
def value_updated
|
44
|
+
# Once the initial url has been parsed and set into the attributes,
|
45
|
+
# start triggering updates on change events.
|
46
|
+
# TODO: This is a temp solution, we need to make it so value_updated
|
47
|
+
# is called after the reactive_value has been updated.
|
48
|
+
if RUBY_PLATFORM == 'opal'
|
49
|
+
%x{
|
50
|
+
if (window.setTimeout && this.$run_update.bind) {
|
51
|
+
if (window.paramsUpdateTimer) {
|
52
|
+
clearTimeout(window.paramsUpdateTimer);
|
53
|
+
}
|
54
|
+
window.paramsUpdateTimer = setTimeout(this.$run_update.bind(this), 0);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def run_update
|
61
|
+
$page.params.trigger!('child_changed') if Volt.client?
|
62
|
+
end
|
63
|
+
|
64
|
+
def new_model(*args)
|
65
|
+
Params.new(*args)
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# The url class handles parsing and updating the url
|
2
|
+
class URL
|
3
|
+
include ReactiveTags
|
4
|
+
|
5
|
+
# TODO: we need to make it so change events only trigger on changes
|
6
|
+
attr_reader :scheme, :host, :port, :path, :query, :params
|
7
|
+
attr_accessor :router
|
8
|
+
|
9
|
+
def initialize(router=nil)
|
10
|
+
@router = router
|
11
|
+
@params = Params.new({}, 'params')
|
12
|
+
end
|
13
|
+
|
14
|
+
# Parse takes in a url and extracts each sections.
|
15
|
+
# It also assigns and changes to the params.
|
16
|
+
tag_method(:parse) do
|
17
|
+
destructive!
|
18
|
+
end
|
19
|
+
def parse(url)
|
20
|
+
matcher = url.match(/^(https?)[:]\/\/([^\/]+)(.*)$/)
|
21
|
+
@scheme = matcher[1]
|
22
|
+
@host, @port = matcher[2].split(':')
|
23
|
+
@port ||= 80
|
24
|
+
|
25
|
+
@path = matcher[3]
|
26
|
+
@path, @fragment = @path.split('#', 2)
|
27
|
+
@path, @query = @path.split('?', 2)
|
28
|
+
|
29
|
+
assign_query_hash_to_params
|
30
|
+
end
|
31
|
+
|
32
|
+
# Full url rebuilds the url from it's constituent parts
|
33
|
+
def full_url
|
34
|
+
if @port
|
35
|
+
host_with_port = "#{@host}:#{@port}"
|
36
|
+
else
|
37
|
+
host_with_port = @host
|
38
|
+
end
|
39
|
+
|
40
|
+
path, params = @router.url_for_params(@params)
|
41
|
+
|
42
|
+
new_url = "#{@scheme}://#{host_with_port}#{path || @path}"
|
43
|
+
|
44
|
+
unless params.empty?
|
45
|
+
new_url += '?'
|
46
|
+
query_parts = []
|
47
|
+
nested_params_hash(params).each_pair do |key,value|
|
48
|
+
value = value.cur
|
49
|
+
# remove the _ from the front
|
50
|
+
value = `encodeURI(value)`
|
51
|
+
query_parts << "#{key}=#{value}"
|
52
|
+
end
|
53
|
+
|
54
|
+
new_url += query_parts.join('&')
|
55
|
+
end
|
56
|
+
|
57
|
+
return new_url
|
58
|
+
end
|
59
|
+
|
60
|
+
# Called when the state has changed and the url in the
|
61
|
+
# browser should be updated
|
62
|
+
# Called when an attribute changes to update the url
|
63
|
+
tag_method(:update!) do
|
64
|
+
destructive!
|
65
|
+
# TODO: ! methods should default to destructive
|
66
|
+
end
|
67
|
+
def update!
|
68
|
+
new_url = full_url()
|
69
|
+
|
70
|
+
if `(document.location.href != new_url)`
|
71
|
+
`history.pushState(null, null, new_url)`
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
# Assigning the params is tricky since we don't want to trigger changed on
|
77
|
+
# any values that have not changed. So we first loop through all current
|
78
|
+
# url params, removing any not present in the params, while also removing
|
79
|
+
# them from the list of new params as added. Then we loop through the
|
80
|
+
# remaining new parameters and assign them.
|
81
|
+
def assign_query_hash_to_params
|
82
|
+
# Get a nested hash representing the current url params.
|
83
|
+
query_hash = self.query_hash
|
84
|
+
|
85
|
+
# Get the params that are in the route
|
86
|
+
query_hash.merge!(@router.params_for_path(@path))
|
87
|
+
|
88
|
+
# Loop through the .params we already have assigned.
|
89
|
+
assign_from_old(@params, query_hash)
|
90
|
+
assign_new(@params, query_hash)
|
91
|
+
end
|
92
|
+
|
93
|
+
def assign_from_old(params, new_params)
|
94
|
+
queued_deletes = []
|
95
|
+
|
96
|
+
params.cur.attributes.each_pair do |name,old_val|
|
97
|
+
# If there is a new value, see if it has [name]
|
98
|
+
new_val = new_params ? new_params[name] : nil
|
99
|
+
|
100
|
+
if !new_val
|
101
|
+
# Queues the delete until after we finish the each_pair loop
|
102
|
+
queued_deletes << name
|
103
|
+
elsif new_val.is_a?(Hash)
|
104
|
+
assign_from_old(old_val, new_val)
|
105
|
+
else
|
106
|
+
# assign value
|
107
|
+
if old_val != new_val
|
108
|
+
params.send(:"#{name}=", new_val)
|
109
|
+
end
|
110
|
+
new_params.delete(name)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
queued_deletes.each {|name| params.delete(name) }
|
115
|
+
end
|
116
|
+
|
117
|
+
def assign_new(params, new_params)
|
118
|
+
new_params.each_pair do |name, value|
|
119
|
+
if value.is_a?(Hash)
|
120
|
+
assign_new(params.send(name), value)
|
121
|
+
else
|
122
|
+
# assign
|
123
|
+
params.send(:"#{name}=", value)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def query_hash
|
129
|
+
query_hash = {}
|
130
|
+
if @query
|
131
|
+
@query.split('&').reject {|v| v == '' }.each do |part|
|
132
|
+
parts = part.split('=').reject {|v| v == '' }
|
133
|
+
|
134
|
+
# Decode string
|
135
|
+
# parts[0] = `decodeURI(parts[0])`
|
136
|
+
parts[1] = `decodeURI(parts[1])`
|
137
|
+
|
138
|
+
sections = query_key_sections(parts[0])
|
139
|
+
|
140
|
+
hash_part = query_hash
|
141
|
+
sections.each_with_index do |section,index|
|
142
|
+
if index == sections.size-1
|
143
|
+
# Last part, assign the value
|
144
|
+
hash_part[section] = parts[1]
|
145
|
+
else
|
146
|
+
hash_part = (hash_part[section] ||= {})
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
return query_hash
|
153
|
+
end
|
154
|
+
|
155
|
+
# Splits a key from a ?key=value&... parameter into its nested
|
156
|
+
# parts. It also adds back the _'s used to access them in params.
|
157
|
+
# Example:
|
158
|
+
# user[name]=Ryan would parse as [:_user, :_name]
|
159
|
+
def query_key_sections(key)
|
160
|
+
key.split(/\[([^\]]+)\]/).reject(&:empty?).map {|v| :"_#{v}"}
|
161
|
+
end
|
162
|
+
|
163
|
+
# Generate the key for a nested param attribute
|
164
|
+
def query_key(path)
|
165
|
+
i = 0
|
166
|
+
path.map do |v|
|
167
|
+
v = v[1..-1]
|
168
|
+
i += 1
|
169
|
+
if i != 1
|
170
|
+
"[#{v}]"
|
171
|
+
else
|
172
|
+
v
|
173
|
+
end
|
174
|
+
end.join('')
|
175
|
+
end
|
176
|
+
|
177
|
+
def nested_params_hash(params, path=[])
|
178
|
+
results = {}
|
179
|
+
|
180
|
+
params.each_pair do |key,value|
|
181
|
+
if value.cur.is_a?(Params) # TODO: Should be a param
|
182
|
+
# TODO: Broke here somehow for nested
|
183
|
+
results.merge!(nested_params_hash(value, path + [key]))
|
184
|
+
else
|
185
|
+
results[query_key(path + [key])] = value
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
return results
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# The URLTracker is responsible for updating the url when
|
2
|
+
# a param changes, or updating the url model/params when
|
3
|
+
# the browser url changes.
|
4
|
+
class UrlTracker
|
5
|
+
def initialize(page)
|
6
|
+
@page = page
|
7
|
+
|
8
|
+
if Volt.client?
|
9
|
+
page.params.on('child_changed') do
|
10
|
+
@page.url.update!
|
11
|
+
end
|
12
|
+
|
13
|
+
that = self
|
14
|
+
|
15
|
+
# Setup popstate on the dom ready event. Prevents an extra
|
16
|
+
# popstate trigger
|
17
|
+
%x{
|
18
|
+
var first = true;
|
19
|
+
window.addEventListener("popstate", function(e) {
|
20
|
+
if (first === false) {
|
21
|
+
that.$url_updated();
|
22
|
+
}
|
23
|
+
|
24
|
+
first = false;
|
25
|
+
|
26
|
+
return true;
|
27
|
+
});
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def url_updated(first_call=false)
|
33
|
+
@page.url.parse(`document.location.href`)
|
34
|
+
@page.url.update! unless first_call
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
CHAIN_DEBUG = false
|
2
|
+
|
3
|
+
class ChainListener
|
4
|
+
attr_reader :object, :callback
|
5
|
+
|
6
|
+
def initialize(event_chain, object, callback)
|
7
|
+
@event_chain = event_chain
|
8
|
+
@object = object
|
9
|
+
@callback = callback
|
10
|
+
|
11
|
+
if RUBY_PLATFORM == 'opal' && CHAIN_DEBUG
|
12
|
+
`window.chain_listeners = window.chain_listeners || 0;`
|
13
|
+
`window.chain_listeners += 1;`
|
14
|
+
`console.log('chain listeners: ', window.chain_listeners)`
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def remove
|
19
|
+
raise "event chain already removed" if @removed
|
20
|
+
@removed = true
|
21
|
+
@event_chain.remove_object(self)
|
22
|
+
|
23
|
+
# We need to clear these to free memory
|
24
|
+
@event_chain = nil
|
25
|
+
@object = nil
|
26
|
+
@callback = nil
|
27
|
+
|
28
|
+
if RUBY_PLATFORM == 'opal' && CHAIN_DEBUG
|
29
|
+
`window.chain_listeners -= 1;`
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class EventChain
|
35
|
+
def initialize(main_object)
|
36
|
+
@event_chain = {}
|
37
|
+
@main_object = main_object
|
38
|
+
@event_counts = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Register an event listener that chains from object to self
|
42
|
+
def setup_listener(event, chain_listener)
|
43
|
+
return chain_listener.object.on(event, @main_object) do |*args|
|
44
|
+
if callback = chain_listener.callback
|
45
|
+
callback.call(event, *args)
|
46
|
+
else
|
47
|
+
# Trigger on this value, when it happens on the parent
|
48
|
+
@main_object.trigger!(event, *args)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def all_listening_events
|
54
|
+
all_listeners = []
|
55
|
+
all_listeners += @main_object.listeners.keys
|
56
|
+
|
57
|
+
if @main_object
|
58
|
+
@main_object.event_followers.each do |event_follower|
|
59
|
+
all_listeners += event_follower.listeners.keys
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
return all_listeners.uniq
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# We can chain our events to any other object that includes
|
68
|
+
# Events
|
69
|
+
def add_object(object, &block)
|
70
|
+
# puts "ADD OBJECT: #{object.inspect} to #{self.inspect}"
|
71
|
+
|
72
|
+
chain_listener = ChainListener.new(self, object, block)
|
73
|
+
|
74
|
+
listeners = {}
|
75
|
+
|
76
|
+
all_listening_events.uniq.each do |event|
|
77
|
+
# Create a listener for each event
|
78
|
+
listeners[event] = setup_listener(event, chain_listener)
|
79
|
+
end
|
80
|
+
|
81
|
+
@event_chain[chain_listener] = listeners
|
82
|
+
|
83
|
+
return chain_listener
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def remove_object(chain_listener)
|
88
|
+
@event_chain[chain_listener].each_pair do |event,listener|
|
89
|
+
# Unbind each listener
|
90
|
+
listener.remove
|
91
|
+
end
|
92
|
+
|
93
|
+
@event_chain.delete(chain_listener)
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_event(event)
|
97
|
+
unless @event_counts[event]
|
98
|
+
@event_chain.each_pair do |chain_listener,listeners|
|
99
|
+
# Only add if we haven't already chained this event
|
100
|
+
unless listeners[event]
|
101
|
+
listeners[event] = setup_listener(event, chain_listener)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
@event_counts[event] ||= 0
|
107
|
+
@event_counts[event] += 1
|
108
|
+
end
|
109
|
+
|
110
|
+
# Removes the event from all events in all objects
|
111
|
+
def remove_event(event)
|
112
|
+
if @event_counts[event]
|
113
|
+
count = @event_counts[event] -= 1
|
114
|
+
|
115
|
+
if count == 0
|
116
|
+
@event_chain.each_pair do |chain_listener,listeners|
|
117
|
+
listeners[event].remove# if listeners[event]
|
118
|
+
listeners.delete(event)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Also remove the event count
|
122
|
+
@event_counts.delete(event)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,283 @@
|
|
1
|
+
require 'volt/reactive/event_chain'
|
2
|
+
require 'volt/reactive/object_tracker'
|
3
|
+
|
4
|
+
DEBUG = false
|
5
|
+
|
6
|
+
# A listener gets returned when adding an 'on' event listener. It can be
|
7
|
+
# used to clear the event listener.
|
8
|
+
class Listener
|
9
|
+
attr_reader :scope_provider
|
10
|
+
|
11
|
+
def initialize(klass, event, scope_provider, callback)
|
12
|
+
@klass = klass
|
13
|
+
@event = event
|
14
|
+
@scope_provider = scope_provider
|
15
|
+
@callback = callback
|
16
|
+
|
17
|
+
if DEBUG && RUBY_PLATFORM == 'opal'
|
18
|
+
# puts "e: #{event} on #{klass.inspect}"
|
19
|
+
@@all_events ||= []
|
20
|
+
@@all_events << self
|
21
|
+
|
22
|
+
# counts = {}
|
23
|
+
# @@all_events.each do |ev|
|
24
|
+
# scope = (ev.scope_provider && ev.scope_provider.scope) || nil
|
25
|
+
#
|
26
|
+
# # puts `typeof(scope)`
|
27
|
+
# if `typeof(scope) !== 'undefined'`
|
28
|
+
# counts[scope] ||= 0
|
29
|
+
# counts[scope] += 1
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# puts counts.inspect
|
34
|
+
|
35
|
+
`window.total_listeners = window.total_listeners || 0;`
|
36
|
+
`window.total_listeners += 1;`
|
37
|
+
`console.log(window.total_listeners);`
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def internal?
|
42
|
+
@internal
|
43
|
+
end
|
44
|
+
|
45
|
+
def scope
|
46
|
+
@scope_provider && @scope_provider.scope
|
47
|
+
end
|
48
|
+
|
49
|
+
def call(*args)
|
50
|
+
# raise "Triggered on removed: #{@event} on #{@klass2.inspect}" if @removed
|
51
|
+
if @removed
|
52
|
+
puts "Triggered on removed: #{@event}"
|
53
|
+
return
|
54
|
+
end
|
55
|
+
|
56
|
+
# Queue a live value update
|
57
|
+
if @klass.reactive?
|
58
|
+
# We are working with a reactive value. Its receiving an event meaning
|
59
|
+
# something changed. Queue an update of the value it tracks.
|
60
|
+
@klass.object_tracker.queue_update
|
61
|
+
# puts "Queued: #{ObjectTracker.queue.inspect}"
|
62
|
+
end
|
63
|
+
|
64
|
+
@callback.call(*args)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Removes the listener from where ever it was created.
|
68
|
+
def remove
|
69
|
+
# puts "FAIL:" if @removed
|
70
|
+
raise "event #{@event} already removed" if @removed
|
71
|
+
|
72
|
+
# puts "e rem: #{@event} on #{@klass.inspect}"
|
73
|
+
if DEBUG && RUBY_PLATFORM == 'opal'
|
74
|
+
@@all_events.delete(self) if @@all_events
|
75
|
+
|
76
|
+
`window.total_listeners -= 1;`
|
77
|
+
`console.log("Rem", window.total_listeners);`
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
@removed = true
|
82
|
+
@klass.remove_listener(@event, self)
|
83
|
+
|
84
|
+
# puts "Removed Listener for: #{@event} - #{@scope_provider && @scope_provider.scope.inspect} from #{@klass.inspect}"
|
85
|
+
|
86
|
+
# We need to clear these references to free the memory
|
87
|
+
@scope_provider = nil
|
88
|
+
@callback = nil
|
89
|
+
# @klass2 = @klass
|
90
|
+
@klass = nil
|
91
|
+
# @event = nil
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
def inspect
|
96
|
+
"<Listener:#{object_id} event=#{@event} scope=#{scope.inspect}#{' internal' if internal?}>"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
module Events
|
101
|
+
attr_accessor :scope
|
102
|
+
# Add a listener for an event
|
103
|
+
def on(event, scope_provider=nil, &block)
|
104
|
+
# puts "Register: #{event} on #{self.inspect}"
|
105
|
+
event = event.to_sym
|
106
|
+
|
107
|
+
new_listener = Listener.new(self, event, scope_provider, block)
|
108
|
+
|
109
|
+
@listeners ||= {}
|
110
|
+
@listeners[event] ||= []
|
111
|
+
@listeners[event] << new_listener
|
112
|
+
|
113
|
+
first = @listeners[event].size == 1
|
114
|
+
add_event_to_chains(event) if first
|
115
|
+
|
116
|
+
# Let the included class know that an event was registered. (if it cares)
|
117
|
+
if self.respond_to?(:event_added)
|
118
|
+
# call event added passing the event, the scope, and a boolean if it
|
119
|
+
# is the first time this event has been added.
|
120
|
+
self.event_added(event, scope_provider, first)
|
121
|
+
end
|
122
|
+
|
123
|
+
return new_listener
|
124
|
+
end
|
125
|
+
|
126
|
+
def event_chain
|
127
|
+
@event_chain ||= EventChain.new(self)
|
128
|
+
end
|
129
|
+
|
130
|
+
def listeners
|
131
|
+
@listeners || {}
|
132
|
+
end
|
133
|
+
|
134
|
+
# Typically you would call .remove on the listener returned from the .on
|
135
|
+
# method. However, here you can also pass in the original proc to remove
|
136
|
+
# a listener
|
137
|
+
def remove_listener(event, listener)
|
138
|
+
event = event.to_sym
|
139
|
+
|
140
|
+
raise "Unable to delete #{event} from #{self.inspect}" unless @listeners && @listeners[event]
|
141
|
+
|
142
|
+
# if @listeners && @listeners[event]
|
143
|
+
@listeners[event].delete(listener)
|
144
|
+
|
145
|
+
no_more_events = @listeners[event].size == 0
|
146
|
+
if no_more_events
|
147
|
+
remove_event_from_chains(event)
|
148
|
+
|
149
|
+
# No registered listeners now on this event
|
150
|
+
@listeners.delete(event)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Let the class we're included on know that we removed a listener (if it cares)
|
154
|
+
if self.respond_to?(:event_removed)
|
155
|
+
# Pass in the event and a boolean indicating if it is the last event
|
156
|
+
self.event_removed(event, no_more_events)
|
157
|
+
end
|
158
|
+
# end
|
159
|
+
end
|
160
|
+
|
161
|
+
# When events get added, we need to notify event chains so they
|
162
|
+
# can update and chain any new events.
|
163
|
+
def add_event_to_chains(event)
|
164
|
+
# First time this event is added, update any chains
|
165
|
+
event_chain.add_event(event)
|
166
|
+
|
167
|
+
# We need to keep the event chain's updated for any objects we're
|
168
|
+
# following for events.
|
169
|
+
event_followings.each {|ef| ef.event_chain.add_event(event) }
|
170
|
+
|
171
|
+
if event != :changed && !@other_event_listener
|
172
|
+
@other_event_listener = on('changed') { }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# When events are removed, we need to notify any relevent chains so they
|
177
|
+
# can remove any chained events.
|
178
|
+
def remove_event_from_chains(event)
|
179
|
+
event_chain.remove_event(event)
|
180
|
+
|
181
|
+
# We need to keep the event chain's updated for any objects we're
|
182
|
+
# following for events.
|
183
|
+
event_followings.each {|ef| ef.event_chain.remove_event(event) }
|
184
|
+
|
185
|
+
if event != :changed
|
186
|
+
# See if there are any remaining events that aren't changed
|
187
|
+
if listeners.keys.reject {|k| k == :changed }.size == 0
|
188
|
+
@other_event_listener.remove
|
189
|
+
@other_event_listener = nil
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
# Track the current object that we're following.
|
196
|
+
def event_followings
|
197
|
+
@event_followings || []
|
198
|
+
end
|
199
|
+
|
200
|
+
def add_following(object)
|
201
|
+
@event_followings ||= []
|
202
|
+
@event_followings << object
|
203
|
+
|
204
|
+
# Take all of our listeners and add them to the
|
205
|
+
listeners.keys.each do |event|
|
206
|
+
object.event_chain.add_event(event)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def remove_following(object)
|
211
|
+
@event_followings.delete(object)
|
212
|
+
|
213
|
+
listeners.keys.each do |event|
|
214
|
+
object.event_chain.remove_event(event)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Track who's following us
|
219
|
+
def event_followers
|
220
|
+
@event_followers || []
|
221
|
+
end
|
222
|
+
|
223
|
+
def add_event_follower(follower)
|
224
|
+
@event_followers ||= []
|
225
|
+
@event_followers << follower
|
226
|
+
|
227
|
+
follower.add_following(self)
|
228
|
+
end
|
229
|
+
|
230
|
+
def remove_event_follower(follower)
|
231
|
+
if @event_followers
|
232
|
+
@event_followers.delete(follower)
|
233
|
+
|
234
|
+
follower.remove_following(self)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Return all listeners for an event on the current object and any event
|
239
|
+
# following objects.
|
240
|
+
def all_listeners_for(event)
|
241
|
+
# TODO: We dup at the moment because some events unregister events, is there
|
242
|
+
# a better solution than this?
|
243
|
+
all_listeners = []
|
244
|
+
all_listeners += @listeners[event].dup if @listeners && @listeners[event]
|
245
|
+
|
246
|
+
if @event_followers
|
247
|
+
@event_followers.each do |event_follower|
|
248
|
+
ef_listeners = event_follower.listeners
|
249
|
+
all_listeners += ef_listeners[event].dup if ef_listeners[event]
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
return all_listeners
|
254
|
+
end
|
255
|
+
|
256
|
+
def trigger!(event, *args)
|
257
|
+
ObjectTracker.process_queue if !reactive? && !respond_to?(:skip_current_queue_flush)
|
258
|
+
|
259
|
+
event = event.to_sym
|
260
|
+
|
261
|
+
all_listeners_for(event).each do |listener|
|
262
|
+
# Call the event on each listener
|
263
|
+
listener.call(*args)
|
264
|
+
end
|
265
|
+
|
266
|
+
nil
|
267
|
+
end
|
268
|
+
|
269
|
+
# Takes a block, which passes in
|
270
|
+
def trigger_by_scope!(event, *args, &block)
|
271
|
+
ObjectTracker.process_queue if !reactive? && !respond_to?(:skip_current_queue_flush)
|
272
|
+
|
273
|
+
event = event.to_sym
|
274
|
+
|
275
|
+
all_listeners_for(event).each do |listener|
|
276
|
+
# Call the block, pass in the scope
|
277
|
+
if block.call(listener.scope)
|
278
|
+
listener.call(*args)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|