hyalite 0.0.1

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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +46 -0
  5. data/Rakefile +7 -0
  6. data/client/hyalite.rb +98 -0
  7. data/client/hyalite/adler32.rb +33 -0
  8. data/client/hyalite/browser_event.rb +201 -0
  9. data/client/hyalite/callback_queue.rb +22 -0
  10. data/client/hyalite/component.rb +77 -0
  11. data/client/hyalite/composite_component.rb +237 -0
  12. data/client/hyalite/dom_component.rb +329 -0
  13. data/client/hyalite/dom_operations.rb +17 -0
  14. data/client/hyalite/dom_property.rb +218 -0
  15. data/client/hyalite/dom_property_operations.rb +117 -0
  16. data/client/hyalite/element.rb +17 -0
  17. data/client/hyalite/event_dispatcher.rb +99 -0
  18. data/client/hyalite/event_plugin/change_event_plugin.rb +83 -0
  19. data/client/hyalite/event_plugin/event_plugin_registry.rb +49 -0
  20. data/client/hyalite/event_plugin/simple_event_plugin.rb +276 -0
  21. data/client/hyalite/input_wrapper.rb +94 -0
  22. data/client/hyalite/instance_handles.rb +78 -0
  23. data/client/hyalite/internal_component.rb +11 -0
  24. data/client/hyalite/linked_value_utils.rb +27 -0
  25. data/client/hyalite/mount.rb +285 -0
  26. data/client/hyalite/multi_children.rb +272 -0
  27. data/client/hyalite/reconcile_transaction.rb +41 -0
  28. data/client/hyalite/reconciler.rb +122 -0
  29. data/client/hyalite/short_hand.rb +23 -0
  30. data/client/hyalite/synthetic_event.rb +18 -0
  31. data/client/hyalite/text_component.rb +42 -0
  32. data/client/hyalite/transaction.rb +47 -0
  33. data/client/hyalite/try.rb +7 -0
  34. data/client/hyalite/update_queue.rb +52 -0
  35. data/client/hyalite/updates.rb +129 -0
  36. data/client/hyalite/utils.rb +19 -0
  37. data/example/Gemfile +5 -0
  38. data/example/Gemfile.lock +46 -0
  39. data/example/app/application.rb +27 -0
  40. data/example/config.ru +12 -0
  41. data/example/index.html.haml +8 -0
  42. data/hyalite.gemspec +27 -0
  43. data/lib/hyalite.rb +6 -0
  44. data/lib/hyalite/main.rb +5 -0
  45. data/lib/hyalite/version.rb +3 -0
  46. metadata +145 -0
@@ -0,0 +1,17 @@
1
+ require 'hyalite/dom_property_operations'
2
+
3
+ module Hyalite
4
+ module DOMOperations
5
+ class << self
6
+ def update_property_by_id(id, name, value)
7
+ node = Mount.node(id)
8
+
9
+ if value
10
+ DOMPropertyOperations.set_value_for_property(node, name, value)
11
+ else
12
+ DOMPropertyOperations.delete_value_for_property(node, name)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,218 @@
1
+ module Hyalite
2
+ module DOMProperty
3
+ ID_ATTRIBUTE_NAME = 'data-hyalite-id'
4
+
5
+ MUST_USE_ATTRIBUTE = 1
6
+ MUST_USE_PROPERTY = 2
7
+ HAS_BOOLEAN_VALUE = 4
8
+ HAS_SIDE_EFFECTS = 8
9
+ HAS_NUMERIC_VALUE = 16
10
+ HAS_POSITIVE_NUMERIC_VALUE = 32
11
+ HAS_OVERLOADED_BOOLEAN_VALUE = 64
12
+
13
+ PROPERTIES = {
14
+ ###############################
15
+ ### Standard Properties
16
+ ###############################
17
+ accept: nil,
18
+ acceptCharset: nil,
19
+ accessKey: nil,
20
+ action: nil,
21
+ allowFullScreen: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
22
+ allowTransparency: MUST_USE_ATTRIBUTE,
23
+ alt: nil,
24
+ async: HAS_BOOLEAN_VALUE,
25
+ autoComplete: nil,
26
+ # autoFocus is polyfilled/normalized by AutoFocusUtils
27
+ # autoFocus: HAS_BOOLEAN_VALUE,
28
+ autoPlay: HAS_BOOLEAN_VALUE,
29
+ capture: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
30
+ cellPadding: nil,
31
+ cellSpacing: nil,
32
+ charSet: MUST_USE_ATTRIBUTE,
33
+ challenge: MUST_USE_ATTRIBUTE,
34
+ checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
35
+ classID: MUST_USE_ATTRIBUTE,
36
+ className: MUST_USE_PROPERTY,
37
+ cols: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
38
+ colSpan: nil,
39
+ content: nil,
40
+ contentEditable: nil,
41
+ contextMenu: MUST_USE_ATTRIBUTE,
42
+ controls: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
43
+ coords: nil,
44
+ crossOrigin: nil,
45
+ data: nil, # For `<object />` acts as `src`.
46
+ dateTime: MUST_USE_ATTRIBUTE,
47
+ defer: HAS_BOOLEAN_VALUE,
48
+ dir: nil,
49
+ disabled: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
50
+ download: HAS_OVERLOADED_BOOLEAN_VALUE,
51
+ draggable: nil,
52
+ encType: nil,
53
+ form: MUST_USE_ATTRIBUTE,
54
+ formAction: MUST_USE_ATTRIBUTE,
55
+ formEncType: MUST_USE_ATTRIBUTE,
56
+ formMethod: MUST_USE_ATTRIBUTE,
57
+ formNoValidate: HAS_BOOLEAN_VALUE,
58
+ formTarget: MUST_USE_ATTRIBUTE,
59
+ frameBorder: MUST_USE_ATTRIBUTE,
60
+ headers: nil,
61
+ height: MUST_USE_ATTRIBUTE,
62
+ hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
63
+ high: nil,
64
+ href: nil,
65
+ hrefLang: nil,
66
+ htmlFor: nil,
67
+ httpEquiv: nil,
68
+ icon: nil,
69
+ id: MUST_USE_PROPERTY,
70
+ inputMode: MUST_USE_ATTRIBUTE,
71
+ is: MUST_USE_ATTRIBUTE,
72
+ keyParams: MUST_USE_ATTRIBUTE,
73
+ keyType: MUST_USE_ATTRIBUTE,
74
+ label: nil,
75
+ lang: nil,
76
+ list: MUST_USE_ATTRIBUTE,
77
+ loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
78
+ low: nil,
79
+ manifest: MUST_USE_ATTRIBUTE,
80
+ marginHeight: nil,
81
+ marginWidth: nil,
82
+ max: nil,
83
+ maxLength: MUST_USE_ATTRIBUTE,
84
+ media: MUST_USE_ATTRIBUTE,
85
+ mediaGroup: nil,
86
+ method: nil,
87
+ min: nil,
88
+ minLength: MUST_USE_ATTRIBUTE,
89
+ multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
90
+ muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
91
+ name: nil,
92
+ noValidate: HAS_BOOLEAN_VALUE,
93
+ open: HAS_BOOLEAN_VALUE,
94
+ optimum: nil,
95
+ pattern: nil,
96
+ placeholder: nil,
97
+ poster: nil,
98
+ preload: nil,
99
+ radioGroup: nil,
100
+ readOnly: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
101
+ rel: nil,
102
+ required: HAS_BOOLEAN_VALUE,
103
+ role: MUST_USE_ATTRIBUTE,
104
+ rows: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
105
+ rowSpan: nil,
106
+ sandbox: nil,
107
+ scope: nil,
108
+ scoped: HAS_BOOLEAN_VALUE,
109
+ scrolling: nil,
110
+ seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
111
+ selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
112
+ shape: nil,
113
+ size: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE,
114
+ sizes: MUST_USE_ATTRIBUTE,
115
+ span: HAS_POSITIVE_NUMERIC_VALUE,
116
+ spellCheck: nil,
117
+ src: nil,
118
+ srcDoc: MUST_USE_PROPERTY,
119
+ srcSet: MUST_USE_ATTRIBUTE,
120
+ start: HAS_NUMERIC_VALUE,
121
+ step: nil,
122
+ style: nil,
123
+ tabIndex: nil,
124
+ target: nil,
125
+ title: nil,
126
+ type: nil,
127
+ useMap: nil,
128
+ value: MUST_USE_PROPERTY | HAS_SIDE_EFFECTS,
129
+ width: MUST_USE_ATTRIBUTE,
130
+ wmode: MUST_USE_ATTRIBUTE,
131
+
132
+ ###############################
133
+ ### Non-standard Properties
134
+ ###############################
135
+ # autoCapitalize and autoCorrect are supported in Mobile Safari for
136
+ # keyboard hints.
137
+ autoCapitalize: nil,
138
+ autoCorrect: nil,
139
+ # itemProp, itemScope, itemType are for
140
+ # Microdata support. See http:#schema.org/docs/gs.html
141
+ itemProp: MUST_USE_ATTRIBUTE,
142
+ itemScope: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
143
+ itemType: MUST_USE_ATTRIBUTE,
144
+ # itemID and itemRef are for Microdata support as well but
145
+ # only specified in the the WHATWG spec document. See
146
+ # https:#html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api
147
+ itemID: MUST_USE_ATTRIBUTE,
148
+ itemRef: MUST_USE_ATTRIBUTE,
149
+ # property is supported for OpenGraph in meta tags.
150
+ property: nil,
151
+ # IE-only attribute that specifies security restrictions on an iframe
152
+ # as an alternative to the sandbox attribute on IE<10
153
+ security: MUST_USE_ATTRIBUTE,
154
+ # IE-only attribute that controls focus behavior
155
+ unselectable: MUST_USE_ATTRIBUTE,
156
+ }
157
+
158
+ DOM_ATTRIBUTE_NAMES = {
159
+ acceptCharset: 'accept-charset',
160
+ className: 'class',
161
+ htmlFor: 'for',
162
+ httpEquiv: 'http-equiv'
163
+ }
164
+
165
+ class << self
166
+ def property_info(name)
167
+ return nil unless PROPERTIES.has_key? name.to_sym
168
+
169
+ @property_info ||= {}
170
+ unless @property_info.has_key? name
171
+ property = PROPERTIES[name.to_sym]
172
+
173
+ attribute_name = DOM_ATTRIBUTE_NAMES.has_key?(name) ? DOM_ATTRIBUTE_NAMES[name] : name.downcase
174
+
175
+ @property_info[name] = {
176
+ attribute_name: attribute_name,
177
+ attribute_namespace: nil,
178
+ property_name: name,
179
+ mutation_method: nil,
180
+ must_use_attribute: property && property & MUST_USE_ATTRIBUTE > 0,
181
+ must_use_property: property && property & MUST_USE_PROPERTY > 0,
182
+ has_side_effects: property && property & HAS_SIDE_EFFECTS > 0,
183
+ has_boolean_value: property && property & HAS_BOOLEAN_VALUE > 0,
184
+ has_numeric_value: property && property & HAS_NUMERIC_VALUE > 0,
185
+ has_positive_numeric_value: property && property & HAS_POSITIVE_NUMERIC_VALUE > 0,
186
+ has_overloaded_boolean_value: property && property & HAS_OVERLOADED_BOOLEAN_VALUE > 0
187
+ }
188
+ end
189
+
190
+ @property_info[name]
191
+ end
192
+
193
+ def include?(name)
194
+ PROPERTIES.has_key? name
195
+ end
196
+
197
+ def default_value_for_property(node_name, prop)
198
+ node_defaults = default_value_cache[node_name]
199
+ unless node_defaults
200
+ default_value_cache[node_name] = node_defaults = {}
201
+ end
202
+ unless node_defaults.include? prop
203
+ test_element = $document.create_element(node_name)
204
+ node_defaults[prop] = test_element[prop]
205
+ end
206
+ node_defaults[prop]
207
+ end
208
+
209
+ def default_value_cache
210
+ @default_value_cache ||= {}
211
+ end
212
+
213
+ def is_custom_attribute(attribute_name)
214
+ /^(data|aria)-[a-z_][a-z\d_.\-]*$/ =~ attribute_name
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,117 @@
1
+ require 'hyalite/dom_property'
2
+ require 'hyalite/utils'
3
+
4
+ module Hyalite
5
+ module DOMPropertyOperations
6
+ VALID_ATTRIBUTE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z_\.\-\d]*$/
7
+
8
+ class << self
9
+ def create_markup_for_property(element, name, value)
10
+ property_info = DOMProperty.property_info(name)
11
+ if property_info
12
+ return if should_ignore_value(property_info, value)
13
+
14
+ attribute_name = property_info[:attribute_name]
15
+ if property_info[:has_boolean_value] || property_info[:has_overloaded_boolean_value] && value == true
16
+ element[attribute_name] = ""
17
+ return
18
+ end
19
+
20
+ element[attribute_name]=Hyalite.escape_text_content_for_browser(value)
21
+ elsif DOMProperty.is_custom_attribute(name)
22
+ return if value.nil?
23
+
24
+ element[name]=Hyalite.escape_text_content_for_browser(value)
25
+ end
26
+ end
27
+
28
+ def create_markup_for_styles(element, styles)
29
+ element.style(styles)
30
+ end
31
+
32
+ def create_markup_for_custom_attribute(element, name, value)
33
+ return if (!is_attribute_name_safe(name) || value == null)
34
+
35
+ element[name]=Hyalite.escape_text_content_for_browser(value)
36
+ end
37
+
38
+ def is_attribute_name_safe(attribute_name)
39
+ @illegal_attribute_name_cache ||= {}
40
+ @validated_attribute_name_cache ||= {}
41
+
42
+ return true if @validated_attribute_name_cache.has_key? attribute_name
43
+ return false if @illegal_attribute_name_cache.has_key? attribute_name
44
+
45
+ if VALID_ATTRIBUTE_NAME_REGEX =~ attribute_name
46
+ @validated_attribute_name_cache[attributeName] = true
47
+ return true
48
+ end
49
+
50
+ @illegal_attribute_name_cache[attributeName] = true
51
+ false
52
+ end
53
+
54
+ def set_value_for_property(node, name, value)
55
+ property_info = DOMProperty.property_info(name)
56
+ if property_info
57
+ mutation_method = property_info[:mutation_method]
58
+ if mutation_method
59
+ mutation_method.call(node, value);
60
+ elsif should_ignore_value(property_info, value)
61
+ delete_value_for_property(node, name)
62
+ elsif property_info[:must_use_attribute]
63
+ attribute_name = property_info[:attribute_name]
64
+ namespace = property_info[:attribute_namespace]
65
+ if namespace
66
+ node[attribute_name, {namespace: namespace}] = value.to_s
67
+ elsif property_info[:has_boolean_value] ||
68
+ (property_info[:has_overloaded_boolean_value] && value == true)
69
+ node[attribute_name] = ''
70
+ else
71
+ node[attribute_name] = value.to_s
72
+ end
73
+ else
74
+ prop_name = property_info[:property_name]
75
+ unless property_info[:has_side_effects] && `'' + node[#{prop_name}]` == value.to_s
76
+ `node.native[#{prop_name}] = value`
77
+ end
78
+ end
79
+ elsif DOMProperty.is_custom_attribute(name)
80
+ DOMPropertyOperations.set_value_for_attribute(node, name, value)
81
+ end
82
+ end
83
+
84
+ def delete_value_for_property(node, name)
85
+ property_info = DOMProperty.property_info(name)
86
+ if property_info
87
+ mutation_method = property_info[:mutation_method]
88
+ if mutation_method
89
+ mutation_method.call(node, nil)
90
+ elsif property_info[:must_use_attribute]
91
+ node.remove_attribute(property_info[:attribute_name])
92
+ else
93
+ prop_name = property_info[:property_name]
94
+ default_value = DOMProperty.default_value_for_property(node.node_name, prop_name)
95
+ unless property_info[:has_side_effects] || (node[prop_name].to_s == default_value)
96
+ node[prop_name] = default_value
97
+ end
98
+ end
99
+ elsif DOMProperty.is_custom_attribute(name)
100
+ node.remove_attribute(name)
101
+ end
102
+ end
103
+
104
+ def set_attribute_for_id(node, id)
105
+ node[DOMProperty::ID_ATTRIBUTE_NAME] = id
106
+ end
107
+
108
+ def should_ignore_value(property_info, value)
109
+ value.nil? ||
110
+ (property_info[:has_boolean_value] && !value) ||
111
+ (property_info[:has_numeric_value] && value.nan?) ||
112
+ (property_info[:has_positive_numeric_value] && (value < 1)) ||
113
+ (property_info[:has_overloaded_boolean_value] && value == false)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,17 @@
1
+ module Hyalite
2
+ class ElementObject
3
+ attr_reader :type, :key, :ref, :props, :owner
4
+
5
+ def initialize(type, key, ref, owner, props)
6
+ @type = type
7
+ @key = key
8
+ @ref = ref
9
+ @owner = owner
10
+ @props = props
11
+ end
12
+
13
+ def inspect
14
+ "<#{type} #{props.inspect} />"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,99 @@
1
+ module Hyalite
2
+ class EventDispatcher
3
+ def initialize(&extract_events)
4
+ @enabled = true
5
+ @listener_bank = {}
6
+ @extract_events = extract_events
7
+ @event_queue = []
8
+ end
9
+
10
+ def enabled?
11
+ @enabled
12
+ end
13
+
14
+ def enabled=(enabled)
15
+ @enabled = enabled
16
+ end
17
+
18
+ def trap_bubbled_event(top_level_type, handler_base_name, element)
19
+ return nil unless element
20
+
21
+ element.on handler_base_name do |event|
22
+ dispatch_event(top_level_type, event)
23
+ end
24
+ end
25
+
26
+ def find_parent(node)
27
+ node_id = Mount.node_id(node)
28
+ root_id = InstanceHandles.root_id_from_node_id(node_id)
29
+ container = Mount.container_for_id(root_id)
30
+ Mount.find_first_hyalite_dom(container)
31
+ end
32
+
33
+ def handle_top_level(book_keeping)
34
+ ancestor = Mount.find_first_hyalite_dom(book_keeping.event.target)
35
+ while ancestor
36
+ book_keeping.ancestors << ancestor
37
+ ancestor = find_parent(ancestor)
38
+ end
39
+
40
+ book_keeping.ancestors.each do |top_level_target|
41
+ top_level_target_id = Mount.node_id(top_level_target) || ''
42
+ synthetic_events = @extract_events.call(
43
+ book_keeping.top_level_type,
44
+ top_level_target,
45
+ top_level_target_id,
46
+ book_keeping.event,
47
+ ).flatten
48
+
49
+ synthetic_events.each do |synthetic_event|
50
+ synthetic_event.each_listener do |listener, dom_id|
51
+ target = Mount.node(dom_id);
52
+ listener.call(synthetic_event.event, target)
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def dispatch_event(top_level_type, event)
59
+ return unless @enabled
60
+
61
+ book_keeping = TopLevelCallbackBookKeeping.new(top_level_type, event)
62
+ Hyalite.updates.batched_updates { handle_top_level(book_keeping) }
63
+ end
64
+
65
+ def put_listener(id, registration_name, listener)
66
+ listeners = @listener_bank[registration_name] ||= {}
67
+ listeners[id] = listener
68
+ end
69
+
70
+ def get_listener(id, registration_name)
71
+ @listener_bank[registration_name].try {|listeners| listeners[id] }
72
+ end
73
+
74
+ def delete_listener(id, registration_name)
75
+ if @listener_bank.has_key? registration_name
76
+ yield(id, registration_name)
77
+ @listener_bank[registration_name].delete(id)
78
+ end
79
+ end
80
+
81
+ def delete_all_listeners(id, &block)
82
+ @listener_bank.each do |registration_name, bank|
83
+ next unless bank[id]
84
+ yield(id, registration_name) if block_given?
85
+ bank.delete(id)
86
+ end
87
+ end
88
+
89
+ class TopLevelCallbackBookKeeping
90
+ attr_reader :event, :top_level_type, :ancestors
91
+
92
+ def initialize(top_level_type, event)
93
+ @top_level_type = top_level_type
94
+ @event = event
95
+ @ancestors = []
96
+ end
97
+ end
98
+ end
99
+ end