pakyow-presenter 0.7.2 → 0.8rc1
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.
- 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
|