browserio 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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