pakyow-presenter 0.7.2 → 0.8rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/pakyow-presenter/lib/presenter/attributes.rb +92 -0
- data/pakyow-presenter/lib/presenter/base.rb +3 -1
- data/pakyow-presenter/lib/presenter/binder.rb +6 -23
- data/pakyow-presenter/lib/presenter/bindings.rb +103 -0
- data/pakyow-presenter/lib/presenter/configuration/presenter.rb +16 -3
- data/pakyow-presenter/lib/presenter/helpers.rb +8 -0
- data/pakyow-presenter/lib/presenter/presenter.rb +102 -76
- data/pakyow-presenter/lib/presenter/view.rb +441 -244
- data/pakyow-presenter/lib/presenter/view_collection.rb +187 -0
- data/pakyow-presenter/lib/presenter/view_lookup_store.rb +10 -6
- metadata +59 -76
- data/pakyow-presenter/lib/presenter/views.rb +0 -112
@@ -4,141 +4,94 @@ module Pakyow
|
|
4
4
|
class << self
|
5
5
|
attr_accessor :binders, :default_view_path, :default_is_root_view
|
6
6
|
|
7
|
+
def view_store
|
8
|
+
Pakyow.app.presenter.current_view_lookup_store
|
9
|
+
end
|
10
|
+
|
11
|
+
def binder_for_scope(scope, bindable)
|
12
|
+
bindings = Pakyow.app.presenter.bindings(scope)
|
13
|
+
bindings.bindable = bindable
|
14
|
+
return bindings
|
15
|
+
end
|
16
|
+
|
7
17
|
def view_path(dvp, dirv=false)
|
8
18
|
self.default_view_path = dvp
|
9
19
|
self.default_is_root_view = dirv
|
10
20
|
end
|
21
|
+
|
22
|
+
def self_closing_tag?(tag)
|
23
|
+
%w[area base basefont br hr input img link meta].include? tag
|
24
|
+
end
|
25
|
+
|
26
|
+
def form_field?(tag)
|
27
|
+
%w[input select textarea button].include? tag
|
28
|
+
end
|
29
|
+
|
30
|
+
def tag_without_value?(tag)
|
31
|
+
%w[select].include? tag
|
32
|
+
end
|
33
|
+
|
34
|
+
def at_path(view_path)
|
35
|
+
v = self.new(self.view_store.root_path(view_path), true)
|
36
|
+
v.compile(view_path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def root_at_path(view_path)
|
40
|
+
self.new(self.view_store.root_path(view_path), true)
|
41
|
+
end
|
42
|
+
|
11
43
|
end
|
12
44
|
|
13
|
-
attr_accessor :doc
|
45
|
+
attr_accessor :doc, :scoped_as, :scopes
|
46
|
+
attr_writer :bindings
|
14
47
|
|
15
48
|
def dup
|
16
|
-
self.class.new(@doc.dup)
|
49
|
+
v = self.class.new(@doc.dup)
|
50
|
+
v.scoped_as = self.scoped_as
|
51
|
+
v
|
17
52
|
end
|
18
53
|
|
19
54
|
def initialize(arg=nil, is_root_view=false)
|
20
55
|
arg = self.class.default_view_path if arg.nil? && self.class.default_view_path
|
21
56
|
is_root_view = self.class.default_is_root_view if arg.nil? && self.class.default_is_root_view
|
22
|
-
|
23
|
-
if arg.is_a?(Nokogiri::XML::Element) || arg.is_a?(Nokogiri::XML::Document)
|
57
|
+
|
58
|
+
if arg.is_a?(Nokogiri::XML::Element) || arg.is_a?(Nokogiri::XML::Document) || arg.is_a?(Nokogiri::HTML::DocumentFragment)
|
24
59
|
@doc = arg
|
25
|
-
elsif arg.is_a?(Pakyow::Presenter::
|
60
|
+
elsif arg.is_a?(Pakyow::Presenter::ViewCollection)
|
26
61
|
@doc = arg.first.doc.dup
|
27
62
|
elsif arg.is_a?(Pakyow::Presenter::View)
|
28
63
|
@doc = arg.doc.dup
|
29
64
|
elsif arg.is_a?(String)
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
65
|
+
view_path = self.class.view_store.real_path(arg)
|
66
|
+
|
67
|
+
# run parsers
|
68
|
+
format = StringUtils.split_at_last_dot(view_path)[1].to_sym
|
69
|
+
content = parse_content(File.read(view_path), format)
|
70
|
+
|
35
71
|
if is_root_view then
|
36
|
-
@doc = Nokogiri::HTML::Document.parse(
|
72
|
+
@doc = Nokogiri::HTML::Document.parse(content)
|
37
73
|
else
|
38
|
-
@doc = Nokogiri::HTML.fragment(
|
74
|
+
@doc = Nokogiri::HTML.fragment(content)
|
39
75
|
end
|
40
76
|
else
|
41
77
|
raise ArgumentError, "No View for you! Come back, one year."
|
42
78
|
end
|
43
79
|
end
|
44
80
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
if @doc && o = @doc.css("##{container}").first
|
49
|
-
content = content.doc unless content.class == String || content.class == Nokogiri::HTML::DocumentFragment || content.class == Nokogiri::XML::Element
|
50
|
-
o.add_child(content)
|
51
|
-
end
|
81
|
+
def compile(view_path)
|
82
|
+
return unless view_info = self.class.view_store.view_info(view_path)
|
83
|
+
self.populate_view(self, view_info[:views])
|
52
84
|
end
|
53
|
-
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
when :css then '<link href="' + Pakyow::Configuration::Presenter.stylesheets + '/' + resource.to_s + '.css" rel="stylesheet" media="' + (options[:media] || 'screen, projection') + '" type="text/css">'
|
61
|
-
end
|
62
|
-
|
63
|
-
if self.doc.fragment? || self.doc.element?
|
64
|
-
self.doc.add_previous_sibling(content)
|
65
|
-
else
|
66
|
-
self.doc.xpath("//head/*[1]").before(content)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def remove_resource(*args)
|
71
|
-
type, resource, options = args
|
72
|
-
options ||= {}
|
73
|
-
|
74
|
-
case type
|
75
|
-
when :js then self.doc.css("script[src='#{Pakyow::Configuration::Presenter.javascripts}/#{resource}.js']").remove
|
76
|
-
when :css then self.doc.css("link[href='#{Pakyow::Configuration::Presenter.stylesheets}/#{resource}.css']").remove
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def find(element)
|
81
|
-
group = Views.new
|
82
|
-
@doc.css(element).each {|e| group << View.new(e)}
|
83
|
-
|
84
|
-
return group
|
85
|
-
end
|
86
|
-
|
87
|
-
def in_context(&block)
|
88
|
-
ViewContext.new(self).instance_exec(self, &block)
|
89
|
-
end
|
90
|
-
|
91
|
-
def bind(object, opts = {})
|
92
|
-
bind_as = opts[:to] ? opts[:to].to_s : StringUtils.underscore(object.class.name.split('::').last)
|
93
|
-
|
94
|
-
@doc.traverse do |o|
|
95
|
-
if attribute = o.get_attribute('itemprop')
|
96
|
-
selector = attribute
|
97
|
-
elsif attribute = o.get_attribute('name')
|
98
|
-
selector = attribute
|
99
|
-
else
|
100
|
-
next
|
101
|
-
end
|
102
|
-
|
103
|
-
next unless attribute
|
104
|
-
|
105
|
-
type_len = bind_as.length
|
106
|
-
next if selector[0, type_len + 1] != "#{bind_as}["
|
107
|
-
|
108
|
-
attribute = selector[type_len + 1, attribute.length - type_len - 2]
|
109
|
-
|
110
|
-
binding = {
|
111
|
-
:element => o,
|
112
|
-
:attribute => attribute.to_sym,
|
113
|
-
:selector => selector
|
114
|
-
}
|
115
|
-
|
116
|
-
bind_object_to_binding(object, binding, bind_as)
|
85
|
+
|
86
|
+
def parse_content(content, format)
|
87
|
+
begin
|
88
|
+
Pakyow.app.presenter.parser_store[format].call(content)
|
89
|
+
rescue
|
90
|
+
Log.warn("No parser defined for extension #{format}") unless format.to_sym == :html
|
91
|
+
content
|
117
92
|
end
|
118
93
|
end
|
119
94
|
|
120
|
-
def repeat_for(objects, opts = {}, &block)
|
121
|
-
if o = @doc
|
122
|
-
objects.each do |object|
|
123
|
-
view = View.new(self)
|
124
|
-
view.bind(object, opts)
|
125
|
-
ViewContext.new(view).instance_exec(object, view, &block) if block_given?
|
126
|
-
|
127
|
-
o.add_previous_sibling(view.doc)
|
128
|
-
end
|
129
|
-
|
130
|
-
o.remove
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def reset_container(container)
|
135
|
-
return unless @doc
|
136
|
-
return unless o = @doc.css("*[id='#{container}']").first
|
137
|
-
return if o.blank?
|
138
|
-
|
139
|
-
o.inner_html = ''
|
140
|
-
end
|
141
|
-
|
142
95
|
def title=(title)
|
143
96
|
if @doc
|
144
97
|
if o = @doc.css('title').first
|
@@ -158,7 +111,7 @@ module Pakyow
|
|
158
111
|
|
159
112
|
def to_html(container = nil)
|
160
113
|
if container
|
161
|
-
if o = @doc.css('#'
|
114
|
+
if o = @doc.css("*[#{Configuration::Presenter.container_attribute}='#{container}']").first
|
162
115
|
o.inner_html
|
163
116
|
else
|
164
117
|
''
|
@@ -175,15 +128,24 @@ module Pakyow
|
|
175
128
|
#
|
176
129
|
def attributes(*args)
|
177
130
|
if args.empty?
|
178
|
-
|
179
|
-
return self
|
131
|
+
return Attributes.new(self)
|
180
132
|
else
|
181
|
-
|
182
|
-
|
183
|
-
self.send(name.to_sym, value)
|
184
|
-
}
|
133
|
+
#TODO mass assign attributes (if we still want to do this)
|
134
|
+
#TODO use this instead of (or combine with) bind_attributes_to_doc?
|
185
135
|
end
|
136
|
+
|
137
|
+
# if args.empty?
|
138
|
+
# @previous_method = :attributes
|
139
|
+
# return self
|
140
|
+
# else
|
141
|
+
# args[0].each_pair { |name, value|
|
142
|
+
# @previous_method = :attributes
|
143
|
+
# self.send(name.to_sym, value)
|
144
|
+
# }
|
145
|
+
# end
|
186
146
|
end
|
147
|
+
|
148
|
+
alias :attrs :attributes
|
187
149
|
|
188
150
|
def remove
|
189
151
|
self.doc.remove
|
@@ -191,17 +153,18 @@ module Pakyow
|
|
191
153
|
|
192
154
|
alias :delete :remove
|
193
155
|
|
194
|
-
|
195
|
-
|
196
|
-
|
156
|
+
#TODO replace this with a different syntax (?): view.attributes.class.add/remove/has?(:foo)
|
157
|
+
# def add_class(val)
|
158
|
+
# self.doc['class'] = "#{self.doc['class']} #{val}".strip
|
159
|
+
# end
|
197
160
|
|
198
|
-
def remove_class(val)
|
199
|
-
|
200
|
-
end
|
161
|
+
# def remove_class(val)
|
162
|
+
# self.doc['class'] = self.doc['class'].gsub(val.to_s, '').strip if self.doc['class']
|
163
|
+
# end
|
201
164
|
|
202
|
-
def has_class(val)
|
203
|
-
|
204
|
-
end
|
165
|
+
# def has_class(val)
|
166
|
+
# self.doc['class'].include? val
|
167
|
+
# end
|
205
168
|
|
206
169
|
def clear
|
207
170
|
return if self.doc.blank?
|
@@ -221,163 +184,397 @@ module Pakyow
|
|
221
184
|
def content=(content)
|
222
185
|
self.doc.inner_html = Nokogiri::HTML.fragment(content.to_s)
|
223
186
|
end
|
224
|
-
|
187
|
+
|
225
188
|
alias :html= :content=
|
226
189
|
|
227
|
-
def append(
|
228
|
-
self.doc.add_child(
|
190
|
+
def append(view)
|
191
|
+
self.doc.add_child(view.doc)
|
229
192
|
end
|
230
193
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
194
|
+
def after(view)
|
195
|
+
self.doc.after(view.doc)
|
196
|
+
end
|
197
|
+
|
198
|
+
def before(view)
|
199
|
+
self.doc.before(view.doc)
|
200
|
+
end
|
201
|
+
|
202
|
+
def scope(name)
|
203
|
+
name = name.to_sym
|
240
204
|
|
241
|
-
|
242
|
-
|
243
|
-
|
205
|
+
views = ViewCollection.new
|
206
|
+
self.bindings.select{|b| b[:scope] == name}.each{|s|
|
207
|
+
v = self.view_from_path(s[:path])
|
208
|
+
v.bindings = self.bindings_for_child_view(v)
|
209
|
+
v.scoped_as = s[:scope]
|
244
210
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
end
|
250
|
-
else
|
251
|
-
return self.doc[method.to_s]
|
252
|
-
end
|
211
|
+
views << v
|
212
|
+
}
|
213
|
+
|
214
|
+
views
|
253
215
|
end
|
254
216
|
|
255
|
-
def
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
217
|
+
def prop(name)
|
218
|
+
name = name.to_sym
|
219
|
+
|
220
|
+
views = ViewCollection.new
|
221
|
+
self.bindings.each {|binding|
|
222
|
+
binding[:props].each {|prop|
|
223
|
+
if prop[:prop] == name
|
224
|
+
v = self.view_from_path(prop[:path])
|
225
|
+
v.bindings = self.bindings_for_child_view(v)
|
226
|
+
|
227
|
+
views << v
|
228
|
+
end
|
229
|
+
}
|
230
|
+
}
|
231
|
+
|
232
|
+
views
|
261
233
|
end
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
234
|
+
|
235
|
+
# call-seq:
|
236
|
+
# with {|view| block}
|
237
|
+
#
|
238
|
+
# Creates a context in which view manipulations can be performed.
|
239
|
+
#
|
240
|
+
# Unlike previous versions, the context can only be referenced by the
|
241
|
+
# block argument. No `context` method will be available.s
|
242
|
+
#
|
243
|
+
def with
|
244
|
+
yield(self)
|
245
|
+
end
|
246
|
+
|
247
|
+
# call-seq:
|
248
|
+
# for {|view, datum| block}
|
249
|
+
#
|
250
|
+
# Yields a view and its matching dataum. This is driven by the view,
|
251
|
+
# meaning datums are yielded until no more views are available. For
|
252
|
+
# the single View case, only one view/datum pair is yielded.
|
253
|
+
#
|
254
|
+
# (this is basically Bret's `map` function)
|
255
|
+
#
|
256
|
+
def for(data, &block)
|
257
|
+
data = [data] unless data.instance_of?(Array)
|
258
|
+
block.call(self, data[0])
|
259
|
+
end
|
260
|
+
|
261
|
+
# call-seq:
|
262
|
+
# match(data) => ViewCollection
|
263
|
+
#
|
264
|
+
# Returns a ViewCollection object that has been manipulated to match the data.
|
265
|
+
# For the single View case, the ViewCollection collection will consist n copies
|
266
|
+
# of self, where n = data.length.
|
267
|
+
#
|
268
|
+
def match(data)
|
269
|
+
data = [data] unless data.instance_of?(Array)
|
270
|
+
|
271
|
+
views = ViewCollection.new
|
272
|
+
data.each {|datum|
|
273
|
+
d_v = self.doc.dup
|
274
|
+
self.doc.before(d_v)
|
275
|
+
|
276
|
+
v = View.new(d_v)
|
277
|
+
v.bindings = self.bindings
|
278
|
+
#TODO set view scope
|
279
|
+
|
280
|
+
views << v
|
281
|
+
}
|
282
|
+
|
283
|
+
self.remove
|
284
|
+
views
|
285
|
+
end
|
286
|
+
|
287
|
+
# call-seq:
|
288
|
+
# repeat(data) {|view, datum| block}
|
289
|
+
#
|
290
|
+
# Matches self with data and yields a view/datum pair.
|
291
|
+
#
|
292
|
+
def repeat(data, &block)
|
293
|
+
self.match(data).for(data, &block)
|
269
294
|
end
|
270
295
|
|
271
|
-
|
296
|
+
# call-seq:
|
297
|
+
# bind(data)
|
298
|
+
#
|
299
|
+
# Binds data across existing scopes.
|
300
|
+
#
|
301
|
+
def bind(data, bindings = nil, &block)
|
302
|
+
scope = self.bindings.first
|
303
|
+
|
304
|
+
binder = View.binder_for_scope(scope[:scope], data)
|
305
|
+
binder.merge(bindings)
|
306
|
+
|
307
|
+
self.bind_data_to_scope(data, scope, binder)
|
308
|
+
yield(self, data) if block_given?
|
309
|
+
end
|
310
|
+
|
311
|
+
# call-seq:
|
312
|
+
# apply(data)
|
313
|
+
#
|
314
|
+
# Matches self to data then binds data to the view.
|
315
|
+
#
|
316
|
+
def apply(data, bindings = nil, &block)
|
317
|
+
views = self.match(data).bind(data, bindings, &block)
|
318
|
+
end
|
319
|
+
|
320
|
+
def container(name)
|
321
|
+
matches = self.containers.select{|c| c[:name].to_sym == name.to_sym}
|
322
|
+
|
323
|
+
vs = ViewCollection.new
|
324
|
+
matches.each{|m| vs << view_from_path(m[:path])}
|
325
|
+
vs
|
326
|
+
end
|
327
|
+
|
328
|
+
def containers
|
329
|
+
@containers ||= self.find_containers
|
330
|
+
end
|
331
|
+
|
332
|
+
def bindings
|
333
|
+
@bindings ||= self.find_bindings
|
334
|
+
end
|
335
|
+
|
336
|
+
protected
|
337
|
+
|
338
|
+
def add_content_to_container(content, container)
|
339
|
+
content = content.doc unless content.class == String || content.class == Nokogiri::HTML::DocumentFragment || content.class == Nokogiri::XML::Element
|
340
|
+
container.add_child(content)
|
341
|
+
end
|
342
|
+
|
343
|
+
def reset_container(container)
|
344
|
+
container.inner_html = ''
|
345
|
+
end
|
346
|
+
|
347
|
+
|
348
|
+
# populates the root_view using view_store data by recursively building
|
349
|
+
# and substituting in child views named in the structure
|
350
|
+
def populate_view(root_view, view_info)
|
351
|
+
root_view.containers.each {|e|
|
352
|
+
next unless path = view_info[e[:name]]
|
353
|
+
|
354
|
+
v = self.populate_view(View.new(path), view_info)
|
355
|
+
self.reset_container(e[:doc])
|
356
|
+
self.add_content_to_container(v, e[:doc])
|
357
|
+
}
|
358
|
+
root_view
|
359
|
+
end
|
360
|
+
|
361
|
+
# returns an array of hashes, each with the container name and doc
|
362
|
+
def find_containers
|
272
363
|
elements = []
|
273
364
|
@doc.traverse {|e|
|
274
|
-
if e.
|
275
|
-
elements << e
|
365
|
+
if name = e.attr(Configuration::Presenter.container_attribute)
|
366
|
+
elements << { :name => name, :doc => e, :path => path_to(e)}
|
276
367
|
end
|
277
368
|
}
|
278
369
|
elements
|
279
370
|
end
|
280
371
|
|
281
|
-
|
372
|
+
# returns an array of hashes that describe each scope
|
373
|
+
def find_bindings
|
374
|
+
bindings = []
|
375
|
+
breadth_first(@doc) {|o|
|
376
|
+
next unless scope = o[Configuration::Presenter.scope_attribute]
|
282
377
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
378
|
+
# find props
|
379
|
+
props = []
|
380
|
+
breadth_first(o) {|so|
|
381
|
+
# don't go into deeper scopes
|
382
|
+
throw :reject if so != o && so[Configuration::Presenter.scope_attribute]
|
383
|
+
|
384
|
+
next unless prop = so[Configuration::Presenter.prop_attribute]
|
385
|
+
props << {:prop => prop.to_sym, :path => path_to(so)}
|
386
|
+
}
|
387
|
+
|
388
|
+
bindings << {:scope => scope.to_sym, :path => path_to(o), :props => props}
|
389
|
+
}
|
390
|
+
|
391
|
+
# determine nestedness (currently unused; leaving in case needed)
|
392
|
+
# bindings.each {|b|
|
393
|
+
# nested = []
|
394
|
+
# bindings.each {|b2|
|
395
|
+
# b_doc = doc_from_path(b[:path])
|
396
|
+
# b2_doc = doc_from_path(b2[:path])
|
397
|
+
# nested << b2 if b2_doc.ancestors.include? b_doc
|
398
|
+
# }
|
399
|
+
|
400
|
+
# b[:nested_scopes] = nested
|
401
|
+
# }
|
402
|
+
return bindings
|
403
|
+
end
|
404
|
+
|
405
|
+
def bindings_for_child_view(child)
|
406
|
+
child_path = self.path_to(child.doc)
|
407
|
+
child_path_len = child_path.length
|
408
|
+
child_bindings = []
|
409
|
+
|
410
|
+
self.bindings.each {|binding|
|
411
|
+
# we want paths within the child path
|
412
|
+
if (child_path - binding[:path]).empty?
|
413
|
+
# update paths relative to child
|
414
|
+
dup = Marshal.load(Marshal.dump(binding))
|
310
415
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
binding[:element][k.to_s] = v.to_s
|
317
|
-
end
|
416
|
+
[dup].concat(dup[:props]).each{|p|
|
417
|
+
p[:path] = p[:path][child_path_len..-1]
|
418
|
+
}
|
419
|
+
|
420
|
+
child_bindings << dup
|
318
421
|
end
|
319
|
-
|
320
|
-
|
422
|
+
}
|
423
|
+
|
424
|
+
child_bindings
|
425
|
+
end
|
426
|
+
|
427
|
+
def breadth_first(doc)
|
428
|
+
queue = [doc]
|
429
|
+
until queue.empty?
|
430
|
+
node = queue.shift
|
431
|
+
catch(:reject) {
|
432
|
+
yield node
|
433
|
+
queue.concat(node.children)
|
434
|
+
}
|
321
435
|
end
|
322
436
|
end
|
323
437
|
|
324
|
-
def
|
325
|
-
|
326
|
-
if binding[:element].name == 'select'
|
327
|
-
if binder
|
328
|
-
if options = binder.fetch_options_for(binding[:attribute])
|
329
|
-
html = ''
|
330
|
-
is_group = false
|
331
|
-
|
332
|
-
options.each do |opt|
|
333
|
-
if opt.is_a?(Array)
|
334
|
-
if opt.first.is_a?(Array)
|
335
|
-
opt.each do |opt2|
|
336
|
-
html << '<option value="' + opt2[0].to_s + '">' + opt2[1].to_s + '</option>'
|
337
|
-
end
|
338
|
-
else
|
339
|
-
html << '<option value="' + opt[0].to_s + '">' + opt[1].to_s + '</option>'
|
340
|
-
end
|
341
|
-
else
|
342
|
-
html << "</optgroup>" if is_group
|
343
|
-
html << '<optgroup label="' + opt.to_s + '">'
|
344
|
-
is_group = true
|
345
|
-
end
|
346
|
-
end
|
438
|
+
def path_to(child)
|
439
|
+
path = []
|
347
440
|
|
348
|
-
|
441
|
+
return path if child == @doc
|
349
442
|
|
350
|
-
|
351
|
-
|
352
|
-
|
443
|
+
child.ancestors.each {|a|
|
444
|
+
# since ancestors goes all the way to doc root, stop when we get to the level of @doc
|
445
|
+
break if a.children.include?(@doc)
|
353
446
|
|
354
|
-
|
355
|
-
|
356
|
-
|
447
|
+
path.unshift(a.children.index(child))
|
448
|
+
child = a
|
449
|
+
}
|
450
|
+
|
451
|
+
return path
|
452
|
+
end
|
453
|
+
|
454
|
+
def doc_from_path(path)
|
455
|
+
o = @doc
|
456
|
+
|
457
|
+
# if path is empty we're at self
|
458
|
+
return o if path.empty?
|
459
|
+
|
460
|
+
path.each {|i|
|
461
|
+
if child = o.children[i]
|
462
|
+
o = child
|
357
463
|
else
|
358
|
-
|
464
|
+
break
|
359
465
|
end
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
466
|
+
}
|
467
|
+
|
468
|
+
return o
|
469
|
+
end
|
470
|
+
|
471
|
+
def view_from_path(path)
|
472
|
+
View.new(doc_from_path(path))
|
473
|
+
end
|
474
|
+
|
475
|
+
def bind_data_to_scope(data, scope, binder = nil)
|
476
|
+
return unless data
|
477
|
+
|
478
|
+
# handle root binding
|
479
|
+
if binder && v = binder.value_for_prop(:_root)
|
480
|
+
v.is_a?(Hash) ? self.bind_attributes_to_doc(v, self.doc) : self.bind_value_to_doc(v, self.doc)
|
481
|
+
end
|
482
|
+
|
483
|
+
scope[:props].each {|p|
|
484
|
+
k = p[:prop]
|
485
|
+
v = binder ? binder.value_for_prop(k) : data[k]
|
486
|
+
|
487
|
+
doc = doc_from_path(p[:path])
|
488
|
+
|
489
|
+
# handle form field
|
490
|
+
self.bind_to_form_field(doc, scope, k, v, binder) if View.form_field?(doc.name)
|
491
|
+
|
492
|
+
# bind attributes or value
|
493
|
+
v.is_a?(Hash) ? self.bind_attributes_to_doc(v, doc) : self.bind_value_to_doc(v, doc)
|
494
|
+
}
|
495
|
+
end
|
496
|
+
|
497
|
+
def bind_value_to_doc(value, doc)
|
498
|
+
return unless value
|
499
|
+
|
500
|
+
tag = doc.name
|
501
|
+
return if View.tag_without_value?(tag)
|
502
|
+
View.self_closing_tag?(tag) ? doc['value'] = value : doc.inner_html = value
|
503
|
+
end
|
504
|
+
|
505
|
+
def bind_attributes_to_doc(attrs, doc)
|
506
|
+
attrs.each do |attr, v|
|
507
|
+
if attr == :content
|
508
|
+
v = v.call(doc.inner_html) if v.is_a?(Proc)
|
509
|
+
bind_value_to_doc(v, doc)
|
510
|
+
next
|
365
511
|
end
|
366
|
-
|
367
|
-
|
368
|
-
|
512
|
+
|
513
|
+
attr = attr.to_s
|
514
|
+
v = v.call(doc[attr]) if v.is_a?(Proc)
|
515
|
+
v.nil? ? doc.remove_attribute(attr) : doc[attr] = v.to_s
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
#TODO refactor to use new options_for
|
520
|
+
def bind_to_form_field(doc, scope, prop, value, binder)
|
521
|
+
return unless !doc['name'] || doc['name'].empty?
|
522
|
+
|
523
|
+
# set name on form element
|
524
|
+
doc['name'] = "#{scope[:scope]}[#{prop}]"
|
525
|
+
|
526
|
+
# special binding for checkboxes and radio buttons
|
527
|
+
if doc.name == 'input' && (doc[:type] == 'checkbox' || doc[:type] == 'radio')
|
528
|
+
if value == true || (doc[:value] && doc[:value] == value.to_s)
|
529
|
+
doc[:checked] = 'checked'
|
369
530
|
else
|
370
|
-
|
531
|
+
doc.delete('checked')
|
371
532
|
end
|
372
|
-
|
373
|
-
|
533
|
+
|
534
|
+
# coerce to string since booleans are often used
|
535
|
+
# and fail when binding to a view
|
536
|
+
value = value.to_s
|
537
|
+
# special binding for selects
|
538
|
+
elsif doc.name == 'select' && binder && options = binder.options_for_prop(prop)
|
539
|
+
option_nodes = Nokogiri::HTML::DocumentFragment.parse ""
|
540
|
+
Nokogiri::HTML::Builder.with(option_nodes) do |h|
|
541
|
+
until options.length == 0
|
542
|
+
catch :optgroup do
|
543
|
+
options.each_with_index { |o,i|
|
544
|
+
|
545
|
+
# an array containing value/content
|
546
|
+
if o.is_a?(Array)
|
547
|
+
h.option o[1], :value => o[0]
|
548
|
+
options.delete_at(i)
|
549
|
+
# likely an object (e.g. string); start a group
|
550
|
+
else
|
551
|
+
h.optgroup(:label => o) {
|
552
|
+
options.delete_at(i)
|
553
|
+
|
554
|
+
options[i..-1].each_with_index { |o2,i2|
|
555
|
+
# starting a new group
|
556
|
+
throw :optgroup if !o2.is_a?(Array)
|
557
|
+
|
558
|
+
h.option o2[1], :value => o2[0]
|
559
|
+
options.delete_at(i)
|
560
|
+
}
|
561
|
+
}
|
562
|
+
end
|
563
|
+
|
564
|
+
}
|
565
|
+
end
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
doc.add_child(option_nodes)
|
570
|
+
end
|
571
|
+
|
572
|
+
# select appropriate option
|
573
|
+
if o = doc.css('option[value="' + value.to_s + '"]').first
|
574
|
+
o[:selected] = 'selected'
|
374
575
|
end
|
375
576
|
end
|
376
|
-
|
377
|
-
def self_closing_tag?(tag)
|
378
|
-
%w[area base basefont br hr input img link meta].include? tag
|
379
|
-
end
|
380
|
-
|
577
|
+
|
381
578
|
end
|
382
579
|
end
|
383
580
|
end
|