browserio 0.0.1 → 0.0.2

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.
@@ -0,0 +1,113 @@
1
+ require 'ostruct'
2
+ require 'browserio/events'
3
+
4
+ module BrowserIO
5
+ class Config
6
+ include Methods
7
+
8
+ # Stores the options for the config
9
+ #
10
+ # @return [OpenStruct]
11
+ attr_accessor :opts
12
+
13
+ # Setup initial opts values
14
+ #
15
+ # @param opts [Hash] The initial params for #opts.
16
+ def initialize(opts = {})
17
+ opts = {
18
+ tmpl: IndifferentHash.new,
19
+ scope: false,
20
+ loaded: false,
21
+ requires: [],
22
+ on: [],
23
+ object_events: {},
24
+ plugins: []
25
+ }.merge opts
26
+
27
+ @opts = OpenStruct.new(opts)
28
+ end
29
+
30
+ # Set the unique name of the component
31
+ #
32
+ # @param name [<String, Symbol>, #to_sym]
33
+ def name(name)
34
+ opts.name = name.to_sym
35
+ BrowserIO.components ||= {}
36
+ BrowserIO.components[opts.name] = opts
37
+ end
38
+
39
+ %w(scope assets_url).each do |m|
40
+ define_method m do |v|
41
+ opts[m] = v
42
+ end
43
+ end
44
+
45
+ # Used to set and update the dom
46
+ def dom
47
+ if server?
48
+ yield
49
+ end
50
+ end
51
+
52
+ # Set the raw html
53
+ # @param html [String]
54
+ def html(html)
55
+ unless RUBY_ENGINE == 'opal'
56
+ opts.html = begin
57
+ File.read html
58
+ rescue
59
+ html
60
+ end.strip
61
+ end
62
+ end
63
+
64
+ def requires(*args)
65
+ unless RUBY_ENGINE == 'opal'
66
+ args.each do |a|
67
+ if a.to_s[/_plugin$/]
68
+ require "browserio/plugins/#{a.to_s.gsub(/_plugin$/, '')}"
69
+ end
70
+ opts.requires << a
71
+ end
72
+ end
73
+ end
74
+
75
+ def opts_dup
76
+ opts.to_h.inject({}) {|copy, (key, value)| copy[key] = value.dup rescue value; copy}
77
+ end
78
+
79
+ def plugin(name)
80
+ unless RUBY_ENGINE == 'opal'
81
+ require "browserio/plugins/#{name}"
82
+ end
83
+ end
84
+
85
+ def get_requires(requires = false, previous_requires = [])
86
+ list = []
87
+
88
+ unless requires
89
+ requires ||= opts.requires.dup
90
+ previous_requires << opts.name.to_sym
91
+ end
92
+
93
+ previous_requires.each { |p| requires.delete(p) }
94
+
95
+ requires.each do |r|
96
+ klass = BrowserIO.components[r.to_sym].klass
97
+ o = klass.client_bio_opts.select do |k, v|
98
+ %w(path_name name assets_url requires).include? k.to_s
99
+ end
100
+
101
+ # We don't want to get a stack limit error so we stop something
102
+ # requiring itself
103
+ pr = previous_requires.dup << o[:name].to_sym
104
+
105
+ o[:requires] = get_requires o[:requires].dup, pr if o[:requires].present?
106
+
107
+ list << o
108
+ end
109
+
110
+ list
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,139 @@
1
+ module BrowserIO
2
+ class DOM
3
+ include Methods
4
+
5
+ attr_accessor :dom, :raw_html
6
+
7
+ class << self
8
+ # Shortcut for creating dom
9
+ # @param html [String]
10
+ # @return dom [DOM]
11
+ def [] html
12
+ new html
13
+ end
14
+ end
15
+
16
+ def initialize html
17
+ @raw_html = html
18
+
19
+ if server?
20
+ @dom = raw_html.is_a?(String) ? HTML[raw_html.dup] : raw_html
21
+ else
22
+ @dom = raw_html.is_a?(String) ? Element[raw_html.dup] : raw_html
23
+ end
24
+ end
25
+
26
+ def find string, &block
27
+ if client?
28
+ node = DOM.new dom.find(string)
29
+ elsif server?
30
+ if block_given?
31
+ node = DOM.new dom.css(string)
32
+ else
33
+ node = DOM.new dom.at(string)
34
+ end
35
+ end
36
+
37
+ if block_given?
38
+ node.each_with_index do |n, i|
39
+ block.call DOM.new(n), i
40
+ end
41
+ end
42
+
43
+ node
44
+ end
45
+
46
+ if RUBY_ENGINE == 'ruby'
47
+ def data key = false, value = false
48
+ d = Hash[node.xpath("@*[starts-with(name(), 'data-')]").map{|a| [a.name, a.value]}]
49
+
50
+ if !key
51
+ d
52
+ elsif key && !value
53
+ d[key]
54
+ else
55
+ node["data-#{key}"] = value
56
+ end
57
+ end
58
+
59
+ def val value
60
+ node.content = value
61
+ end
62
+
63
+ def add_class classes
64
+ classes = (classes || '').split ' ' unless classes.is_a? Array
65
+ new_classes = ((node.attr('class') || '').split(' ') << classes).uniq.join(' ')
66
+ node['class'] = new_classes
67
+ end
68
+
69
+ def remove_class classes
70
+ classes = (classes || '').split ' ' unless classes.is_a? Array
71
+ (node.attr('class') || '').split(' ').reject { |n| n =~ /active|asc|desc/i }.join(' ')
72
+ end
73
+
74
+ def attr key, value = false
75
+ if value
76
+ value = value.join ' ' if value.is_a? Array
77
+ node[key] = value
78
+ else
79
+ super key
80
+ end
81
+ end
82
+ end
83
+
84
+ def html= content
85
+ if server?
86
+ node.inner_html = content
87
+ else
88
+ content = content.dom if content.is_a? BrowserIO::DOM
89
+ node.html content
90
+ end
91
+
92
+ node
93
+ end
94
+
95
+ if RUBY_ENGINE == 'opal'
96
+ # make it supply the jquery element so it will use that as it doesn't
97
+ # know how to handle the DOM element.
98
+ %w(append prepend replace_with after before).each do |meth|
99
+ define_method meth do |obj|
100
+ obj = obj.dom if obj.is_a? BrowserIO::DOM
101
+ super obj
102
+ end
103
+ end
104
+
105
+ def to_html
106
+ @dom ||= DOM.new '<div>'
107
+ el = dom.first
108
+ DOM.new('<div>').append(el).html
109
+ end
110
+ end
111
+
112
+ def html content = false
113
+ if !content
114
+ if server?
115
+ node.inner_html
116
+ else
117
+ node ? node.html : dom.html
118
+ end
119
+ else
120
+ self.html = content
121
+ end
122
+ end
123
+
124
+ def node
125
+ @node || dom
126
+ end
127
+
128
+ # This allows you to use all the nokogiri or opal jquery methods if a
129
+ # global one isn't set
130
+ def method_missing method, *args, &block
131
+ # respond_to?(symbol, include_all=false)
132
+ if dom.respond_to? method, true
133
+ dom.send method, *args, &block
134
+ else
135
+ super
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,136 @@
1
+ module BrowserIO
2
+ class Events
3
+ attr_accessor :scope, :browser_events, :object_events
4
+
5
+ VIP_ORDER_LIST = %w(history_change)
6
+
7
+ def initialize
8
+ @browser_events = []
9
+ @object_events = {}
10
+ end
11
+
12
+ def add(*args, &block)
13
+ event = {
14
+ name: args.shift,
15
+ block: block,
16
+ options: {}
17
+ }
18
+
19
+ args.each do |arg|
20
+ if arg.is_a?(String)
21
+ event[:selector] = arg
22
+ elsif arg.is_a? Class
23
+ event[:klass] = arg
24
+ else
25
+ event[:options] = event[:options].merge(arg).indifferent.to_h
26
+ end
27
+ end
28
+
29
+ if event[:name].to_s == 'ready' || event[:name] =~ /[:\s]/ || event[:selector]
30
+ browser_events << event
31
+ else
32
+ event[:component] = scope.bio_opts.name
33
+
34
+ if !scope.class.bio_opts.added_class_events && for_component = event[:options].delete(:for)
35
+ bio_opts = BrowserIO.components[for_component].klass.bio_opts
36
+ events = bio_opts.object_events[event.delete(:name)] ||= []
37
+ events << event
38
+ else
39
+ events = object_events[event.delete(:name)] ||= []
40
+ events << event
41
+ end
42
+ end
43
+ end
44
+
45
+ def trigger(name, options = {})
46
+ name = name.to_s
47
+ options = options.indifferent
48
+
49
+ case name
50
+ when 'browser_events'
51
+ # We make sure anything in the VIP_ORDER_LIST goes first
52
+ (browser_events.sort_by do |x|
53
+ [VIP_ORDER_LIST.index(x[:name]) || VIP_ORDER_LIST.length, browser_events.index(x)]
54
+ end).each do |event|
55
+ trigger_browser_event event
56
+ end
57
+ else
58
+ events = object_events[name] || []
59
+ events.each do |event|
60
+ BrowserIO[event[:component]].instance_exec options, &event[:block]
61
+ end
62
+ end
63
+ end
64
+
65
+ def trigger_browser_event event
66
+ comp = BrowserIO[scope.bio_opts.name]
67
+
68
+ case event[:name]
69
+ when 'ready'
70
+ el = Element.find(event[:selector] != '' ? event[:selector] : 'body')
71
+
72
+ comp.instance_exec el, &event[:block]
73
+ when 'history_change'
74
+ $window.history.change do |he|
75
+ comp.instance_exec he, &event[:block]
76
+ end
77
+ when 'form'
78
+ warn 'missing form class option' unless event[:klass]
79
+
80
+ Document.on :submit, event[:selector] do |evt|
81
+ el = evt.current_target
82
+ evt.prevent_default
83
+
84
+ params = {}
85
+
86
+ # loop through all the forum values
87
+ el.serialize_array.each do |row|
88
+ field, _ = row
89
+
90
+ # we need to make it native to access it like ruby
91
+ field = Native(field)
92
+ name = field['name']
93
+ value = field['value']
94
+
95
+ params[name] = value
96
+ end
97
+
98
+ params_obj = {}
99
+
100
+ params.each do |param, value|
101
+ keys = param.gsub(/[^a-z0-9_]/, '|').gsub(/\|\|/, '|').gsub(/\|$/, '').split('|')
102
+ params_obj = params_obj.deep_merge keys.reverse.inject(value) { |a, n| { n => a } }
103
+ end
104
+
105
+ opts[:dom] = el
106
+
107
+ if opts && key = opts[:key]
108
+ form = event[:klass].new params_obj[key], opts
109
+ else
110
+ form = event[:klass].new params_obj, opts
111
+ end
112
+
113
+ el.find(opts[:error_selector] || '.field-error').remove
114
+
115
+ comp.instance_exec form, evt.current_target, evt, &event[:block]
116
+ end
117
+ else
118
+ args = [event[:name]]
119
+
120
+ if selector = event[:selector]
121
+ args << selector
122
+ end
123
+
124
+ Document.on(*args) do |evt|
125
+ el = evt.current_target
126
+ comp.instance_exec el, evt, &event[:block]
127
+ end
128
+
129
+ if event[:name] =~ /ready/
130
+ el = Element.find(event[:selector] != '' ? event[:selector] : 'body')
131
+ comp.instance_exec el, &event[:block]
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,29 @@
1
+ module BrowserIO
2
+ module HTML
3
+ include Methods
4
+
5
+ class << self
6
+ # Parse HTML into a Nokogiri object
7
+ # @param raw_html [String]
8
+ # @return parsed_html [Nokogiri]
9
+ def [](raw_html)
10
+ return unless server?
11
+
12
+ # remove all the starting and trailing whitespace
13
+ raw_html = raw_html.strip
14
+
15
+ if raw_html[/\A<!DOCTYPE/] || raw_html[/\A<html/]
16
+ Nokogiri::HTML(raw_html)
17
+ else
18
+ parsed_html = Nokogiri::HTML.fragment(raw_html)
19
+
20
+ if parsed_html.children.length >= 1
21
+ parsed_html.children.first
22
+ else
23
+ parsed_html
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ require 'opal'
2
+ require 'opal-jquery'
3
+
4
+ if RUBY_ENGINE == 'ruby'
5
+ module Opal
6
+ class Builder
7
+ # @return [String] Compiled javascript.
8
+ def javascript
9
+ to_s
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ module BrowserIO
16
+ # Create our own opal instance.
17
+ Opal = ::Opal.dup
18
+ end
@@ -0,0 +1,343 @@
1
+ require 'browserio/plugins/validations'
2
+ require 'forwardable'
3
+
4
+ module BrowserIO
5
+ module Plugins
6
+ class Form < Component
7
+ config.name :form_plugin
8
+
9
+ include Methods
10
+ include Validations
11
+
12
+ module Delegates
13
+ def _delegates(*names)
14
+ accessors = Module.new do
15
+ extend Forwardable # DISCUSS: do we really need Forwardable here?
16
+ names.each do |name|
17
+ delegate [name, "#{name}="] => :_attributes
18
+ end
19
+ end
20
+ include accessors
21
+ end
22
+ end
23
+
24
+ extend Delegates
25
+
26
+ class Attributes
27
+ def set_values(atts)
28
+ @_attributes = []
29
+
30
+ atts.each do |key, val|
31
+ if respond_to?("#{key}=")
32
+ send(:"#{key}=", val)
33
+ @_attributes << key
34
+ end
35
+ end
36
+ end
37
+
38
+ def set_attr_accessors attrs
39
+ attrs.each do |attr|
40
+ define_singleton_method "#{attr}=" do |value|
41
+ value = value.to_obj if value.is_a? Hash
42
+ instance_variable_set(:"@#{attr}", value)
43
+ @_attributes ||= []
44
+ @_attributes << attr
45
+ end
46
+
47
+ define_singleton_method attr do
48
+ instance_variable_get(:"@#{attr}")
49
+ end
50
+ end
51
+ end
52
+
53
+ def _attributes
54
+ @_attributes ||= []
55
+ end
56
+
57
+ def empty?
58
+ _attributes.empty?
59
+ end
60
+ end
61
+
62
+ # Initialize with a hash of attributes and values.
63
+ # If extra attributes are sent, a NoMethodError exception will be raised.
64
+ #
65
+ # @example
66
+ #
67
+ # class EditPost < Scrivener
68
+ # attr_accessor :title
69
+ # attr_accessor :body
70
+ #
71
+ # def validate
72
+ # assert_present :title
73
+ # assert_present :body
74
+ # end
75
+ # end
76
+ #
77
+ # edit = EditPost.new(title: "Software Tools")
78
+ #
79
+ # edit.valid? #=> false
80
+ #
81
+ # edit.errors[:title] #=> []
82
+ # edit.errors[:body] #=> [:not_present]
83
+ #
84
+ # edit.body = "Recommended reading..."
85
+ #
86
+ # edit.valid? #=> true
87
+ #
88
+ # # Now it's safe to initialize the model.
89
+ # post = Post.new(edit.attributes)
90
+ # post.save
91
+ def initialize(atts = {}, options = {})
92
+ @_data = atts
93
+ @_data = atts.to_obj if atts.is_a? Hash
94
+ @_options = options
95
+
96
+ # @_attributes = Class.new(Attributes).new
97
+ @_attributes = Attributes.new
98
+ @_attributes.set_attr_accessors _attr_accessors
99
+ @_attributes.set_values _data
100
+
101
+ _data.each do |key, val|
102
+ send("#{key}=", val)
103
+ end
104
+
105
+ _form.each do |key, klass|
106
+ opts = {}
107
+ opts[key] = klass.new(_data.send(key)) if _data.respond_to?(key)
108
+ @_attributes.set_values opts
109
+
110
+ send("#{key}=", opts[key])
111
+ end
112
+ end
113
+
114
+ def self.attr_accessor(*vars)
115
+ @_attr_accessors ||= []
116
+ @_form ||= {}
117
+
118
+ vars.each do |v|
119
+ if !v.is_a? Hash
120
+ @_attr_accessors << v unless @_attr_accessors.include? v
121
+ else
122
+ v = v.first
123
+
124
+ unless @_attr_accessors.include? v.first
125
+ @_attr_accessors << v.first
126
+ @_form[v.first] = v.last
127
+ end
128
+ end
129
+ end
130
+
131
+ _delegates(*_attr_accessors)
132
+ end
133
+
134
+ def method_missing method, *args, &block
135
+ # respond_to?(symbol, include_all=false)
136
+ if _data.respond_to? method, true
137
+ _data.send method, *args, &block
138
+ else
139
+ return if method[/\=\z/]
140
+
141
+ super
142
+ end
143
+ end
144
+
145
+ # Return hash of attributes and values.
146
+ def attributes
147
+ Hash.new.tap do |atts|
148
+ _attributes.instance_variables.each do |ivar|
149
+ # todo: figure out why it's setting @constructor and @toString
150
+ next if ivar == :@constructor || ivar == :@toString || ivar == :@_attributes || ivar == :@_data || ivar == :@_forms
151
+
152
+ att = ivar[1..-1].to_sym
153
+ atts[att] = _attributes.send(att)
154
+
155
+ if klass = _form[att.to_s.to_sym]
156
+ atts[att] = klass.new(atts[att]).attributes
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ def model_attributes data = attributes
163
+ hash = {}
164
+
165
+ data.each do |k, v|
166
+ if klass = _form[k.to_s.to_sym]
167
+ d = data[k]
168
+ d = d.attributes if d.is_a?(Form)
169
+
170
+ f = klass.new d
171
+ k = "#{k}_attributes"
172
+ dt = f.model_attributes
173
+
174
+ hash[k] = model_attributes dt
175
+ elsif v.is_a? Hash
176
+ hash[k] = model_attributes data[k]
177
+ else
178
+ hash[k] = v
179
+ end
180
+ end
181
+
182
+ hash
183
+ end
184
+
185
+ def slice(*keys)
186
+ Hash.new.tap do |atts|
187
+ keys.each do |att|
188
+ atts[att] = send(att)
189
+ # atts[att] = _attributes.send(att)
190
+ end
191
+ end
192
+ end
193
+
194
+ def display_errors options = {}, &block
195
+ dom = options.delete(:dom) || _dom
196
+ d_errors = errors
197
+
198
+ if override_errors = options[:override_errors]
199
+ d_errors = override_errors
200
+ end
201
+
202
+ keys = options.delete(:keys) || (_options[:key] ? [_options[:key]] : [])
203
+
204
+ if extra_errors = options.delete(:errors)
205
+ extra_errors.each do |key, value|
206
+ d_errors[key] = value
207
+ end
208
+ end
209
+
210
+ d_errors.each do |key, error|
211
+ d_keys = (keys.dup << key)
212
+
213
+ error = error.first
214
+
215
+ if error.is_a?(Hash)
216
+ d_options = options.dup
217
+ d_options[:keys] = d_keys
218
+ d_options[:override_errors] = d_errors[key].first
219
+
220
+ display_errors d_options, &block
221
+ elsif !block_given? || block.call(d_keys, error) == false
222
+ name = d_keys.each_with_index.map do |field, i|
223
+ i != 0 ? "[#{field}]" : field
224
+ end.join
225
+
226
+ if tmpl = options[:tmpl]
227
+ if client?
228
+ field_error_dom = DOM.new(`#{tmpl.dom}[0].outerHTML`)
229
+ else
230
+ field_error_dom = DOM.new(tmpl.dom.to_html)
231
+ end
232
+ else
233
+ field_error_dom = DOM.new('<span class="field-error"><span>')
234
+ end
235
+
236
+ field_error_dom.html _error_name(key, error)
237
+
238
+ field = dom.find("[name='#{name}']")
239
+ field.before field_error_dom.dom
240
+ end
241
+ end
242
+ end
243
+ alias_method :render_errors, :display_errors
244
+
245
+ def render_values dom = false, key = false, data = false
246
+ dom = _options[:dom] unless dom
247
+ key = _options[:key] if !key && _options.key?(:key)
248
+
249
+ dom.find('input, select, textarea') do |element|
250
+ name = element['name']
251
+ name = name.gsub(/\A#{key}/, '') if key
252
+ keys = name.gsub(/\A\[/, '').gsub(/[^a-z0-9_]/, '|').gsub(/\|\|/, '|').gsub(/\|$/, '').split('|')
253
+ value = false
254
+
255
+ keys.each do |k|
256
+ begin
257
+ value = value ? value.send(k) : send(k)
258
+ rescue
259
+ value = ''
260
+ end
261
+ end
262
+
263
+ case element.name
264
+ when 'select'
265
+ element.find('option') do |x|
266
+ x['selected'] = true if x['value'] == value.to_s
267
+ end
268
+ when 'input'
269
+ if %w(radio checkbox).include? element['type']
270
+ if element['value'] == value.to_s
271
+ element['checked'] = true
272
+ else
273
+ element.delete 'checked'
274
+ end
275
+ else
276
+ value = sprintf('%.2f', value) if value.is_a? BigDecimal
277
+ element['value'] = value.to_s
278
+ end
279
+ when 'textarea'
280
+ element.val value.to_s
281
+ end
282
+ end
283
+ end
284
+
285
+ def _attributes
286
+ @_attributes ||= {}
287
+ end
288
+
289
+ def validate_msg error, column
290
+ false
291
+ end
292
+
293
+ protected
294
+
295
+ def _data
296
+ @_data ||= {}
297
+ end
298
+
299
+ def self._attr_accessors
300
+ @_attr_accessors ||= []
301
+ end
302
+
303
+ def self._form
304
+ @_form || {}
305
+ end
306
+
307
+ def _form
308
+ self.class._form
309
+ end
310
+
311
+ def _attr_accessors
312
+ self.class._attr_accessors
313
+ end
314
+
315
+ def _options
316
+ @_options
317
+ end
318
+
319
+ def _dom
320
+ @_dom ||= @_options[:dom]
321
+ end
322
+
323
+ def _error_name key, error
324
+ validate_msg(error.to_sym, key.to_sym) || case error.to_s.to_sym
325
+ when :not_email
326
+ 'Email Isn\'t Valid.'
327
+ when :not_present
328
+ 'Required.'
329
+ when :not_equal
330
+ 'Password does not match.'
331
+ else
332
+ !error[/\s/] ? error.to_s.gsub(/_/, ' ').titleize : error
333
+ end
334
+ end
335
+
336
+ def empty?
337
+ _attributes.empty?
338
+ end
339
+ end
340
+ end
341
+ end
342
+
343
+ BrowserIO::Form = BrowserIO::Plugins::Form