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,83 @@
1
+ class Browser::DOM::Element
2
+ def input_type
3
+ `self.native.type`
4
+ end
5
+ end
6
+
7
+ module Hyalite
8
+ class ChangeEventPlugin
9
+ EVENT_TYPES = {
10
+ change: {
11
+ phasedRegistrationNames: {
12
+ bubbled: "onChange",
13
+ captured: "onChangeCapture"
14
+ },
15
+ dependencies: [
16
+ :topBlur,
17
+ :topChange,
18
+ :topClick,
19
+ :topFocus,
20
+ :topInput,
21
+ :topKeyDown,
22
+ :topKeyUp,
23
+ :topSelectionChange
24
+ ]
25
+ }
26
+ }
27
+
28
+ SUPPORTED_INPUT_TYPES = [
29
+ 'color',
30
+ 'date',
31
+ 'datetime',
32
+ 'datetime-local',
33
+ 'email',
34
+ 'month',
35
+ 'number',
36
+ 'password',
37
+ 'range',
38
+ 'search',
39
+ 'tel',
40
+ 'text',
41
+ 'time',
42
+ 'url',
43
+ 'week'
44
+ ]
45
+
46
+ def event_types
47
+ EVENT_TYPES
48
+ end
49
+
50
+ def is_text_input_element(elem)
51
+ node_name = elem.node_name.downcase
52
+ (node_name == 'input' && SUPPORTED_INPUT_TYPES.include?(elem.input_type)) || node_name == 'textarea'
53
+ end
54
+
55
+ def should_use_change_event(elem)
56
+ node_name = elem.node_name.downcase
57
+ node_name == 'select' || (node_name == 'input' && elem.input_type == 'file')
58
+ end
59
+
60
+ def should_use_click_event(elem)
61
+ elem.node_name.downcase == 'input' && %w(checkbox radio).include?(elem.input_type)
62
+ end
63
+
64
+ def extract_event(top_level_type, top_level_target, top_level_target_id, event)
65
+ if should_use_change_event(top_level_target)
66
+ target_id = top_level_target_id if top_level_type == :topChange
67
+ elsif is_text_input_element(top_level_target)
68
+ target_id = top_level_target_id if top_level_type == :topInput
69
+ elsif should_use_click_event(top_level_target)
70
+ target_id = top_level_target_id if top_level_type == :topClick
71
+ end
72
+
73
+ if target_id
74
+ SyntheticEvent.new(event).tap do |synthetic_event|
75
+ InstanceHandles.traverse_two_phase(target_id) do |target_id, upwards|
76
+ listener = BrowserEvent.listener_at_phase(target_id, EVENT_TYPES[:change], upwards ? :bubbled : :captured)
77
+ synthetic_event.add_listener(listener, target_id) if listener
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,49 @@
1
+ module Hyalite
2
+ class EventPluginRegistry
3
+ def initialize(*plugins)
4
+ @registration_name_modules = {}
5
+ @registration_name_dependencies = {}
6
+ @plugins = []
7
+ plugins.each {|plugin| add_plugin(plugin) }
8
+ end
9
+
10
+ def add_plugin(plugin)
11
+ plugin.event_types.each do |key, dispatch_config|
12
+ dispatch_config[:phasedRegistrationNames].each do |phase, registration_name|
13
+ @registration_name_modules[registration_name] = plugin
14
+ @registration_name_dependencies[registration_name] = dispatch_config[:dependencies]
15
+ end
16
+ end
17
+ @plugins << plugin
18
+ end
19
+
20
+ def include?(registration_name)
21
+ @registration_name_modules.has_key? registration_name
22
+ end
23
+
24
+ def dependencies(registration_name)
25
+ @registration_name_dependencies[registration_name]
26
+ end
27
+
28
+ def [](registration_name)
29
+ @registration_name_modules[registration_name]
30
+ end
31
+
32
+ def extract_events(top_level_type, top_level_target, top_level_target_id, event)
33
+ @plugins.each.with_object([]) do |plugin, events|
34
+ synthetic_event = plugin.extract_event(top_level_type, top_level_target, top_level_target_id, event)
35
+ events << synthetic_event if synthetic_event
36
+ end
37
+ end
38
+
39
+ def registration_names
40
+ @registrasion_names ||= Set.new.tap do |names|
41
+ TOP_LEVEL_EVENTS_TO_DISPATCH_CONFIG.each_value do |value|
42
+ value[:phasedRegistrationNames].each_value do |name|
43
+ names << name
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,276 @@
1
+ module Hyalite
2
+ class SimpleEventPlugin
3
+ EVENT_TYPES = {
4
+ blur: {
5
+ phasedRegistrationNames: {
6
+ bubbled: :onBlur,
7
+ captured: :onBlurCapture
8
+ }
9
+ },
10
+ click: {
11
+ phasedRegistrationNames: {
12
+ bubbled: :onClick,
13
+ captured: :onClickCapture
14
+ }
15
+ },
16
+ contextMenu: {
17
+ phasedRegistrationNames: {
18
+ bubbled: :onContextMenu,
19
+ captured: :onContextMenuCapture
20
+ }
21
+ },
22
+ copy: {
23
+ phasedRegistrationNames: {
24
+ bubbled: :onCopy,
25
+ captured: :onCopyCapture
26
+ }
27
+ },
28
+ cut: {
29
+ phasedRegistrationNames: {
30
+ bubbled: :onCut,
31
+ captured: :onCutCapture
32
+ }
33
+ },
34
+ doubleClick: {
35
+ phasedRegistrationNames: {
36
+ bubbled: :onDoubleClick,
37
+ captured: :onDoubleClickCapture
38
+ }
39
+ },
40
+ drag: {
41
+ phasedRegistrationNames: {
42
+ bubbled: :onDrag,
43
+ captured: :onDragCapture
44
+ }
45
+ },
46
+ dragEnd: {
47
+ phasedRegistrationNames: {
48
+ bubbled: :onDragEnd,
49
+ captured: :onDragEndCapture
50
+ }
51
+ },
52
+ dragEnter: {
53
+ phasedRegistrationNames: {
54
+ bubbled: :onDragEnter,
55
+ captured: :onDragEnterCapture
56
+ }
57
+ },
58
+ dragExit: {
59
+ phasedRegistrationNames: {
60
+ bubbled: :onDragExit,
61
+ captured: :onDragExitCapture
62
+ }
63
+ },
64
+ dragLeave: {
65
+ phasedRegistrationNames: {
66
+ bubbled: :onDragLeave,
67
+ captured: :onDragLeaveCapture
68
+ }
69
+ },
70
+ dragOver: {
71
+ phasedRegistrationNames: {
72
+ bubbled: :onDragOver,
73
+ captured: :onDragOverCapture
74
+ }
75
+ },
76
+ dragStart: {
77
+ phasedRegistrationNames: {
78
+ bubbled: :onDragStart,
79
+ captured: :onDragStartCapture
80
+ }
81
+ },
82
+ drop: {
83
+ phasedRegistrationNames: {
84
+ bubbled: :onDrop,
85
+ captured: :onDropCapture
86
+ }
87
+ },
88
+ focus: {
89
+ phasedRegistrationNames: {
90
+ bubbled: :onFocus,
91
+ captured: :onFocusCapture
92
+ }
93
+ },
94
+ input: {
95
+ phasedRegistrationNames: {
96
+ bubbled: :onInput,
97
+ captured: :onInputCapture
98
+ }
99
+ },
100
+ keyDown: {
101
+ phasedRegistrationNames: {
102
+ bubbled: :onKeyDown,
103
+ captured: :onKeyDownCapture
104
+ }
105
+ },
106
+ keyPress: {
107
+ phasedRegistrationNames: {
108
+ bubbled: :onKeyPress,
109
+ captured: :onKeyPressCapture
110
+ }
111
+ },
112
+ keyUp: {
113
+ phasedRegistrationNames: {
114
+ bubbled: :onKeyUp,
115
+ captured: :onKeyUpCapture
116
+ }
117
+ },
118
+ load: {
119
+ phasedRegistrationNames: {
120
+ bubbled: :onLoad,
121
+ captured: :onLoadCapture
122
+ }
123
+ },
124
+ error: {
125
+ phasedRegistrationNames: {
126
+ bubbled: :onError,
127
+ captured: :onErrorCapture
128
+ }
129
+ },
130
+ mouseDown: {
131
+ phasedRegistrationNames: {
132
+ bubbled: :onMouseDown,
133
+ captured: :onMouseDownCapture
134
+ }
135
+ },
136
+ mouseMove: {
137
+ phasedRegistrationNames: {
138
+ bubbled: :onMouseMove,
139
+ captured: :onMouseMoveCapture
140
+ }
141
+ },
142
+ mouseOut: {
143
+ phasedRegistrationNames: {
144
+ bubbled: :onMouseOut,
145
+ captured: :onMouseOutCapture
146
+ }
147
+ },
148
+ mouseOver: {
149
+ phasedRegistrationNames: {
150
+ bubbled: :onMouseOver,
151
+ captured: :onMouseOverCapture
152
+ }
153
+ },
154
+ mouseUp: {
155
+ phasedRegistrationNames: {
156
+ bubbled: :onMouseUp,
157
+ captured: :onMouseUpCapture
158
+ }
159
+ },
160
+ paste: {
161
+ phasedRegistrationNames: {
162
+ bubbled: :onPaste,
163
+ captured: :onPasteCapture
164
+ }
165
+ },
166
+ reset: {
167
+ phasedRegistrationNames: {
168
+ bubbled: :onReset,
169
+ captured: :onResetCapture
170
+ }
171
+ },
172
+ scroll: {
173
+ phasedRegistrationNames: {
174
+ bubbled: :onScroll,
175
+ captured: :onScrollCapture
176
+ }
177
+ },
178
+ submit: {
179
+ phasedRegistrationNames: {
180
+ bubbled: :onSubmit,
181
+ captured: :onSubmitCapture
182
+ }
183
+ },
184
+ touchCancel: {
185
+ phasedRegistrationNames: {
186
+ bubbled: :onTouchCancel,
187
+ captured: :onTouchCancelCapture
188
+ }
189
+ },
190
+ touchEnd: {
191
+ phasedRegistrationNames: {
192
+ bubbled: :onTouchEnd,
193
+ captured: :onTouchEndCapture
194
+ }
195
+ },
196
+ touchMove: {
197
+ phasedRegistrationNames: {
198
+ bubbled: :onTouchMove,
199
+ captured: :onTouchMoveCapture
200
+ }
201
+ },
202
+ touchStart: {
203
+ phasedRegistrationNames: {
204
+ bubbled: :onTouchStart,
205
+ captured: :onTouchStartCapture
206
+ }
207
+ },
208
+ wheel: {
209
+ phasedRegistrationNames: {
210
+ bubbled: :onWheel,
211
+ captured: :onWheelCapture
212
+ }
213
+ }
214
+ }
215
+
216
+ TOP_LEVEL_EVENTS_TO_DISPATCH_CONFIG = {
217
+ topBlur: EVENT_TYPES[:blur],
218
+ topClick: EVENT_TYPES[:click],
219
+ topContextMenu: EVENT_TYPES[:contextMenu],
220
+ topCopy: EVENT_TYPES[:copy],
221
+ topCut: EVENT_TYPES[:cut],
222
+ topDoubleClick: EVENT_TYPES[:doubleClick],
223
+ topDrag: EVENT_TYPES[:drag],
224
+ topDragEnd: EVENT_TYPES[:dragEnd],
225
+ topDragEnter: EVENT_TYPES[:dragEnter],
226
+ topDragExit: EVENT_TYPES[:dragExit],
227
+ topDragLeave: EVENT_TYPES[:dragLeave],
228
+ topDragOver: EVENT_TYPES[:dragOver],
229
+ topDragStart: EVENT_TYPES[:dragStart],
230
+ topDrop: EVENT_TYPES[:drop],
231
+ topError: EVENT_TYPES[:error],
232
+ topFocus: EVENT_TYPES[:focus],
233
+ topInput: EVENT_TYPES[:input],
234
+ topKeyDown: EVENT_TYPES[:keyDown],
235
+ topKeyPress: EVENT_TYPES[:keyPress],
236
+ topKeyUp: EVENT_TYPES[:keyUp],
237
+ topLoad: EVENT_TYPES[:load],
238
+ topMouseDown: EVENT_TYPES[:mouseDown],
239
+ topMouseMove: EVENT_TYPES[:mouseMove],
240
+ topMouseOut: EVENT_TYPES[:mouseOut],
241
+ topMouseOver: EVENT_TYPES[:mouseOver],
242
+ topMouseUp: EVENT_TYPES[:mouseUp],
243
+ topPaste: EVENT_TYPES[:paste],
244
+ topReset: EVENT_TYPES[:reset],
245
+ topScroll: EVENT_TYPES[:scroll],
246
+ topSubmit: EVENT_TYPES[:submit],
247
+ topTouchCancel: EVENT_TYPES[:touchCancel],
248
+ topTouchEnd: EVENT_TYPES[:touchEnd],
249
+ topTouchMove: EVENT_TYPES[:touchMove],
250
+ topTouchStart: EVENT_TYPES[:touchStart],
251
+ topWheel: EVENT_TYPES[:wheel]
252
+ }
253
+
254
+ def initialize
255
+ TOP_LEVEL_EVENTS_TO_DISPATCH_CONFIG.each do |type, dispatch_config|
256
+ dispatch_config[:dependencies] = [type]
257
+ end
258
+ end
259
+
260
+ def event_types
261
+ EVENT_TYPES
262
+ end
263
+
264
+ def extract_event(top_level_type, top_level_target, top_level_target_id, event)
265
+ dispatch_config = TOP_LEVEL_EVENTS_TO_DISPATCH_CONFIG[top_level_type]
266
+ return [] unless dispatch_config
267
+
268
+ SyntheticEvent.new(event).tap do |synthetic_event|
269
+ InstanceHandles.traverse_two_phase(top_level_target_id) do |target_id, upwards|
270
+ listener = BrowserEvent.listener_at_phase(target_id, dispatch_config, upwards ? :bubbled : :captured)
271
+ synthetic_event.add_listener(listener, target_id) if listener
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,94 @@
1
+ require 'json'
2
+ require 'hyalite/linked_value_utils'
3
+ require 'hyalite/dom_operations'
4
+
5
+ module Hyalite
6
+ class InputWrapper
7
+ include LinkedValueUtils
8
+
9
+ def initialize(dom_component)
10
+ @dom_component = dom_component
11
+ end
12
+
13
+ def mount_wrapper
14
+ props = @dom_component.current_element.props
15
+ @wrapper_state = {
16
+ initialChecked: props[:default_checked] || false,
17
+ initialValue: props[:default_value],
18
+ listeners: nil,
19
+ onChange: -> (event) { handle_change(event) }
20
+ }
21
+ end
22
+
23
+ def unmount_wrapper
24
+ end
25
+
26
+ def native_props(inst)
27
+ props = @dom_component.current_element.props
28
+
29
+ props.merge({
30
+ defaultChecked: nil,
31
+ defaultValue: nil,
32
+ value: props[:value] || @wrapper_state[:initialValue],
33
+ checked: props[:checked] || @wrapper_state[:initialChecked],
34
+ onChange: @wrapper_state[:onChange],
35
+ })
36
+ end
37
+
38
+ def force_update_if_mounted(instance)
39
+ if instance.root_node_id
40
+ update_wrapper
41
+ end
42
+ end
43
+
44
+ def update_wrapper
45
+ props = @dom_component.current_element.props
46
+ if props.has_key?(:checked)
47
+ checked = props[:checked]
48
+ node = Mount.node(@dom_component.root_node_id)
49
+ if checked
50
+ node[:checked] = checked
51
+ else
52
+ node.remove_attribute(:checked)
53
+ end
54
+ end
55
+
56
+ value = LinkedValueUtils.value(props)
57
+ if value
58
+ DOMOperations.update_property_by_id(
59
+ @dom_component.root_node_id,
60
+ 'value',
61
+ value.to_s
62
+ )
63
+ end
64
+ end
65
+
66
+ def handle_change(event)
67
+ props = @dom_component.current_element.props
68
+ return_value = execute_on_change(props, event)
69
+
70
+ Hyalite.updates.asap { force_update_if_mounted(@dom_component) }
71
+
72
+ if props[:type] == 'radio' && props[:name]
73
+ root_node = Mount.node(root_node_id)
74
+ query_root = root_node
75
+
76
+ while query_root.parent
77
+ query_root = query_root.parent
78
+ end
79
+
80
+ group = query_root =~ "input[name='#{name.to_json}'][type='radio']"
81
+
82
+ group.each do |other_node|
83
+ next if other_node == root_node || other_node.form != root_node.form
84
+
85
+ other_id = Mount.node_id(other_node)
86
+ other_instance = Mount.instances_by_root_id(other_id)
87
+ Hyalite.updates.asap { force_update_if_mounted(other_instance) }
88
+ end
89
+ end
90
+
91
+ return_value
92
+ end
93
+ end
94
+ end