module Pakyow
module Presenter
class View
class << self
attr_accessor :binders, :cache, :default_view_path, :default_is_root_view
def view_path(dvp, dirv=false)
self.default_view_path = dvp
self.default_is_root_view = dirv
end
end
attr_accessor :doc
def initialize(arg=nil, is_root_view=false)
arg = self.class.default_view_path if arg.nil? && self.class.default_view_path
is_root_view = self.class.default_is_root_view if arg.nil? && self.class.default_is_root_view
if arg.is_a?(Nokogiri::XML::Element)
@doc = arg
elsif arg.is_a?(Pakyow::Presenter::Views)
@doc = arg.first.doc.dup
elsif arg.is_a?(Pakyow::Presenter::View)
@doc = arg.doc.dup
elsif arg.is_a?(String)
if arg[0, 1] == '/'
view_path = "#{Configuration::Presenter.view_dir}#{arg}"
else
view_path = "#{Configuration::Presenter.view_dir}/#{arg}"
end
# Only load one time if view caching is enabled
self.class.cache ||= {}
if !self.class.cache.has_key?(view_path) || !Configuration::Base.presenter.view_caching
if is_root_view then
self.class.cache[view_path] = Nokogiri::HTML::Document.parse(File.read(view_path))
else
self.class.cache[view_path] = Nokogiri::HTML.fragment(File.read(view_path))
end
end
@doc = self.class.cache[view_path].dup
else
raise ArgumentError, "No View for you! Come back, one year."
end
end
def add_content_to_container(content, container)
# TODO This .css call works but the equivalent .xpath call doesn't
# Need to investigate why since the .css call is internally turned into a .xpath call
if @doc && o = @doc.css("##{container}").first
content = content.doc unless content.class == String || content.class == Nokogiri::HTML::DocumentFragment || content.class == Nokogiri::XML::Element
o.add_child(content)
end
end
def add_resource(*args)
type, resource, options = args
options ||= {}
content = case type
when :js then ''
when :css then ''
end
if self.doc.fragment? || self.doc.element?
self.doc.add_previous_sibling(content)
else
self.doc.xpath("//head/*[1]").before(content)
end
end
def remove_resource(*args)
type, resource, options = args
options ||= {}
case type
when :js then self.doc.css("script[src='#{Pakyow::Configuration::Presenter.javascripts}/#{resource}.js']").remove
when :css then self.doc.css("link[href='#{Pakyow::Configuration::Presenter.stylesheets}/#{resource}.css']").remove
end
end
def find(element)
group = Views.new
@doc.css(element).each {|e| group << View.new(e)}
return group
end
def in_context(&block)
ViewContext.new(self).instance_eval(&block)
end
def bind(object, type = nil)
type = type || StringUtils.underscore(object.class.name)
@doc.traverse do |o|
if attribute = o.get_attribute('itemprop')
selector = attribute
elsif attribute = o.get_attribute('name')
selector = attribute
else
next
end
next unless attribute
if selector.include?('[')
type_len = type.length
object_type = selector[0,type_len]
attribute = selector[type_len + 1, attribute.length - type_len - 2]
else
object_type = nil
attribute = selector
end
next if !object_type.nil? && object_type != type
binding = {
:element => o,
:attribute => attribute.to_sym,
:selector => selector
}
bind_object_to_binding(object, binding, object_type.nil?)
end
end
def repeat_for(objects, &block)
if o = @doc
objects.each do |object|
view = View.new(self)
view.bind(object)
ViewContext.new(view).instance_exec(object, &block) if block_given?
o.add_previous_sibling(view.doc)
end
o.remove
end
end
def reset_container(container)
return unless @doc
return unless o = @doc.css("*[id='#{container}']").first
return if o.blank?
o.inner_html = ''
end
def title=(title)
if @doc
if o = @doc.css('title').first
o.inner_html = Nokogiri::HTML::fragment(title)
else
if o = @doc.css('head').first
o.add_child(Nokogiri::HTML::fragment("
#{title}"))
end
end
end
end
def title
o = @doc.css('title').first
o.inner_html if o
end
def to_html(container = nil)
if container
if o = @doc.css('#' + container.to_s).first
o.inner_html
else
''
end
else
@doc.to_html
end
end
alias :to_s :to_html
# Allows multiple attributes to be set at once.
# root_view.find(selector).attributes(:class => my_class, :style => my_style)
#
def attributes(*args)
if args.empty?
@previous_method = :attributes
return self
else
args[0].each_pair { |name, value|
@previous_method = :attributes
self.send(name.to_sym, value)
}
end
end
def remove
self.doc.remove
end
alias :delete :remove
def add_class(val)
self.doc['class'] = "#{self.doc['class']} #{val}".strip
end
def remove_class(val)
self.doc['class'] = self.doc['class'].gsub(val.to_s, '').strip if self.doc['class']
end
def has_class(val)
self.doc['class'].include? val
end
def clear
return if self.doc.blank?
self.doc.inner_html = ''
end
def text
self.doc.inner_text
end
def content
self.doc.inner_html
end
alias :html :content
def content=(content)
return unless content
self.doc.inner_html = Nokogiri::HTML.fragment(content.to_s)
end
alias :html= :content=
def append(content)
self.doc.add_child(Nokogiri::HTML.fragment(content.to_s))
end
alias :render :append
def +(value)
if @previous_method
append_value(val)
else
super
end
end
def <<(value)
if @previous_method
append_value(val)
else
super
end
end
def method_missing(method, *args)
return unless @previous_method == :attributes
@previous_method = nil
if method.to_s.include?('=')
attribute = method.to_s.gsub('=', '')
value = args[0]
self.doc[attribute] = value
else
return self.doc[method.to_s]
end
end
def class(*args)
if @previous_method == :attributes
method_missing(:class, *args)
else
super
end
end
def id
if @previous_method == :attributes
method_missing(:id)
else
super
end
end
def elements_with_ids
elements = []
@doc.traverse {|e|
if e.has_attribute?("id")
elements << e
end
}
elements
end
protected
def append_value(value_to_append)
case @previous_method
when :content
append(value_to_append)
end
@previous_method = nil
end
def bind_object_to_binding(object, binding, wild = false)
binder = nil
# fetch value
if object.is_a? Hash
value = object[binding[:attribute]]
else
if View.binders
b = View.binders[object.class.to_s.to_sym] and binder = b.new(object, binding[:element])
end
if binder && binder.class.method_defined?(binding[:attribute])
value = binder.send(binding[:attribute])
else
if wild && !object.class.method_defined?(binding[:attribute])
return
elsif Configuration::Base.app.dev_mode == true && !object.class.method_defined?(binding[:attribute])
Log.warn("Attempting to bind object to #{binding[:html_tag]}#{binding[:selector].gsub('*', '').gsub('\'', '')} but #{object.class.name}##{binding[:attribute]} is not defined.")
return
else
value = object.send(binding[:attribute])
end
end
end
if value.is_a? Hash
value.each do |k, v|
if k == :content
bind_value_to_binding(v, binding, binder)
else
binding[:element][k.to_s] = v.to_s
end
end
else
bind_value_to_binding(value, binding, binder)
end
end
def bind_value_to_binding(value, binding, binder)
if !self.self_closing_tag?(binding[:element].name)
if binding[:element].name == 'select'
if binder
if options = binder.fetch_options_for(binding[:attribute])
html = ''
is_group = false
options.each do |opt|
if opt.is_a?(Array)
if opt.first.is_a?(Array)
opt.each do |opt2|
html << ''
end
else
html << ''
end
else
html << "" if is_group
html << '" if is_group
binding[:element].inner_html = Nokogiri::HTML::fragment(html)
end
end
if opt = binding[:element].css('option[value="' + value.to_s + '"]').first
opt['selected'] = 'selected'
end
else
binding[:element].inner_html = Nokogiri::HTML.fragment(value.to_s)
end
elsif binding[:element].name == 'input' && binding[:element][:type] == 'checkbox'
if value == true || binding[:element].attributes['value'].value == value.to_s
binding[:element]['checked'] = 'checked'
else
binding[:element].delete('checked')
end
elsif binding[:element].name == 'input' && binding[:element][:type] == 'radio'
if binding[:element].attributes['value'].value == value.to_s
binding[:element]['checked'] = 'checked'
else
binding[:element].delete('checked')
end
else
binding[:element]['value'] = value.to_s
end
end
def self_closing_tag?(tag)
%w[area base basefont br hr input img link meta].include? tag
end
end
end
end