# ===========================================================================
# Project: Abbot - SproutCore Build Tools
# Copyright: ©2009 Apple, Inc.
# portions copyright @2006-2009 Sprout Systems, Inc.
# and contributors
# ===========================================================================
module SproutCore
# The PageHelper is a singleton object that can render the Page javascript
# object.
module PageHelper
@@render_contexts = []
@@outlets = []
@@styles = []
@@defines = []
# This is the current helper state used when rendering the HTML. When
# a view helper is rendered, it may add itself as an outlet to the current
# helper state instead of to the page helper.
def self.current_render_context
@@render_contexts.last
end
def self.push_render_context(state)
@@render_contexts.push(state)
end
def self.pop_render_context
@@render_contexts.pop
end
# reset the page helper.
def self.reset!
@@render_contexts = []
@@outlets = []
@@styles = []
@@defines = []
end
def self.set_define(key, opts = {})
@@defines << [key, opts]
end
def self.set_outlet(key,opts = {})
@@outlets << [key, opts]
end
def self.add_styles(styles)
@@styles << styles
end
# renders the page object for the current page. If you include a prefix
# that will be used to create a separate page object. Otherwise, the
# object will live in the SC namespace. If you provide a bundle that
# is configured to minify sources the output will be compressed.
#
# returns the text to insert into the HTML.
def self.render_js(prefix = 'SC', bundle = nil)
outlets = []
@@outlets.each do | key, opts |
outlet_path = opts[:outlet_path] || "##{opts[:id] || key}"
outlets << %{ #{key}: #{opts[:class] || 'SC.View'}.extend({\n #{ opts[:properties].gsub("\n","\n ") }\n }).outletFor("#{outlet_path}") }
end
# defines let you define classes to include in your UI.
ret = @@defines.each do | key, opts |
%{#{key} = #{opts[:class] || 'SC.View'}.extend({\n #{ opts[:properties] }\n});}
end
ret << %{#{prefix}.page = SC.Page.create({\n#{ outlets * ",\n\n" }\n}); }
bundle ? SproutCore::BuildTools::JavaScriptResourceBuilder.new(nil, nil, bundle, nil).join(ret) : ret*"\n"
end
def self.render_css
if @@styles.size > 0
%()
else
''
end
end
end
module ViewHelperSupport
@@helpers = {}
def self.find_helper(helper_name)
@@helpers[helper_name.to_sym] || @@helpers[:view]
end
def self.set_helper(helper_name,obj)
@@helpers[helper_name.to_sym] = obj
end
class RenderContext
# options passed in from the view helper
attr_reader :view_helper_id
attr_reader :item_id
attr_accessor :outlet
attr_accessor :define
attr_accessor :current_helper
attr_accessor :client_builder
attr_reader :render_source
attr_reader :parent_context
attr_reader :child_contexts
def initialize(view_helper_id, item_id, opts={}, client_builder = nil, render_source=nil)
@_options = opts.dup
@bindings = (@_options[:bind] || {}).dup
@outlets = []
@prototypes = {}
@view_helper_id = view_helper_id
@item_id = item_id
@outlet = opts[:outlet]
@define = opts[:define]
@client_builder = client_builder
@render_source = render_source
@parent_context = SproutCore::PageHelper.current_render_context
@parent_context.child_contexts << self if @parent_context
@child_contexts = []
@attributes = (@_options[:attributes] || {}).dup
@_properties = {}
if @_options[:properties]
@_options[:properties].each do | key, value |
@_properties[key.to_s.camelize(:lower)] = prepare_for_javascript(value)
end
end
end
def to_s
"RenderContext #{view_helper_id}[#{item_id}]"
end
def options
@_options
end
def set_outlet(key,opts = {})
@outlets << [key, opts]
end
def prepare_bindings
@bindings.each do | k,v |
key = k.to_s.camelize(:lower) + 'Binding'
@_properties[key] = v.include?('(') ? v : prepare_for_javascript(v)
end
end
def prepare_outlets
return if @outlets.size == 0
outlets = []
@outlets.each do | key, opts |
outlet_key = key.to_s.camelize(:lower)
outlets << outlet_key unless opts[:lazy]
outlet_path = opts[:outlet_path] || ".#{opts[:id] || key }?"
str = %{#{opts[:class] || 'SC.View'}.extend({\n#{ opts[:properties] }\n}).outletFor("#{outlet_path}")}
@_properties[outlet_key] = str
end
@_properties['outlets'] = outlets
end
def parent_helper(opts = {})
if @current_helper && @current_helper.parent_helper
@_options.merge! opts
@current_helper.parent_helper.prepare_context(self)
end
end
### RENDER METHODS
def render_content
@attributes[:id] = @item_id if @item_id && !(@outlet || @define)
old_client_builder = self.client_builder
self.client_builder = @content_render_client_builder unless @content_render_client_builder.nil?
ret = _do_render(@content_render)
self.client_builder = old_client_builder
return ret
end
def render_view
prepare_bindings
prepare_outlets
_do_render(@view_render)
end
def view_class
@view_class
end
def render_styles
_do_render(@styles_render)
end
### BASIC CONFIG METHODS
# These are called in the helper's prepare_context method.
# This method must be called to configure the view.
def view(view_class,text = nil, &block)
@view_class = view_class
@view_render = text || block if (text || block)
end
# this method must be called to configure the HTML.
# also captures the client builder in use at the time it is called.
def content(text = nil, &block)
@content_render_client_builder = self.client_builder
@content_render = text || block if (text || block)
end
# this method may be called to add CSS styles
def styles(text = nil, &block)
@styles_render = text || block if (text || block)
end
def static_url(resource_name, opts = {})
opts[:language] ||= @language
entry = @client_builder.find_resource_entry(resource_name, opts)
entry.nil? ? '' : entry.cacheable_url
end
def blank_url
static_url('blank.gif')
end
### HTML HELPER METHODS
# This will extract the specified value and put it into an ivar you can
# access later during rendering. For example:
#
# var :label, 'Default label'
#
# will now be accessible in your code via @label
#
# Parameters:
# option_key: (req) the option to map.
# default_value: (opt) if passed, this will be used as the default value
# if the option is not passed in.
#
# :key => (opt) the name of the resulting ivar. defaults to the option
# key.
#
# :optional => (opt) if true, then the attribute will not be included if
# it is not explicitly passed in the options. if no default value is
# specified, then this will default to true, otherwise defaults to
# false.
#
# :constant => (opt) if true, then any passed in options will be ignored
# for this key and the default you specify will be used instead.
# Defaults to false
#
# you may also pass a block that will be used to compute the value at
# render time. Expect a single parameter which is the initial value.
#
def var(option_key, default_value=:__UNDEFINED__, opts={}, &block)
ret = _pair(option_key, default_value, opts, &block)
return if ret[2] # ignore
instance_variable_set("@#{ret[0]}".to_sym, ret[1])
ret[1]
end
# returns the standard attributes for the HTML. This will automatically
# include the item id. You can also declare added attributes using the
# attribute param.
def attributes
final_class_names = css_class_names
final_styles = css_styles
ret = @attributes.map do |key,value|
# if the css class or css style is declared, replace the current
# set coming from the view_helper
if key.to_sym == :class && value
final_class_names = value
nil
elsif key.to_sym == :style && value
final_styles = value
nil
else
value ? %(#{key}="#{value}") : nil
end
end
# add in class names
final_class_names = [final_class_names].flatten
final_class_names << @item_id
final_class_names.compact!
unless final_class_names.empty?
ret << %(class="#{final_class_names.uniq * ' '}")
end
# add in styles
unless final_styles.nil?
final_styles = [final_styles].flatten
final_styles.compact!
ret << %(style="#{final_styles.uniq * ' '}") unless final_styles.empty?
end
ret.compact * ' '
end
# Your view helper can add text to by appended to the styles attribute
# by adding to this array.
def css_styles
@css_styles ||= []
end
def css_styles=(new_ary)
@css_styles = new_ary
end
# Your view helper can add css classes to be appended to the classes
# attribute by adding to this array.
def css_class_names
@css_class_names ||= []
end
def css_class_names=(new_ary)
@css_class_names = new_ary
end
# This does the standard open tag with the default tag and attributes. Usually
# you can use this.
def open_tag
%{<#{@tag} #{attributes}>}
end
alias_method :ot, :open_tag
def close_tag
%{#{@tag}>}
end
alias_method :ct, :close_tag
# Call this method in your view helper definition to map an option to
# an attribute. This attribute can then be rendered with attributes.
# This method takes the same options as var
def attribute(option_key, default_value=:__UNDEFINED__, opts={}, &block)
ret = _pair(option_key, default_value, opts, &block)
return if ret[2] # ignore
@attributes[ret[0]] = ret[1]
end
# returns all the JS properties specified by the property method.
def properties
keys = @_properties.keys
ret = []
# example element, if there is one
if @define
@_properties['emptyElement'] = %($sel("#resources? .#{@item_id}:1:1"))
ret << _partial_properties(['emptyElement'])
end
# outlets first
if keys.include?('outlets')
outlets = @_properties['outlets']
@_properties['outlets'] = '["' + (outlets * '","') + '"]'
ret << _partial_properties(['outlets'])
ret << _partial_properties(outlets,",\n\n")
keys.reject! { |k| outlets.include?(k) || (k == 'outlets') }
end
bindings = keys.reject { |k| !k.match(/Binding$/) }
if bindings.size > 0
ret << _partial_properties(bindings)
keys.reject! { |k| bindings.include?(k) }
end
if keys.size > 0
ret << _partial_properties(keys)
end
ret = ret * ",\n\n"
' ' + ret.gsub("\n","\n ")
end
def _partial_properties(keys,join = ",\n")
ret = keys.map do |key|
value = @_properties[key]
next if value.nil?
%(#{key}: #{value})
end
ret * join
end
# Call this method to make a binding available or to set a default
# binding. You can use this for properties you want to allow a
# binding for but don't want to take as a fully property.
def bind(option_key, default_value=:__UNDEFINED__, opts={})
key, v, ignore = _pair(option_key, default_value, opts, false)
# always look for the option key in the bindings passed by the user.
# if present, this should override whatever we set
if found = @bindings[option_key.to_sym] || @bindings[option_key.to_s]
v = found
ignore = false
@bindings.delete option_key.to_sym
@bindings.delete option_key.to_s
end
# finally, set the binding value.
unless ignore
v = v.include?('(') ? v : prepare_for_javascript(v)
@_properties["#{key.camelize(:lower)}Binding"] = v
end
end
# Call this method in your view helper to specify a property you want
# added to the javascript declaration. This methos take the same
# options as var. Note that normally the type of value returned here
# will be marshalled into the proper type for JavaScript. If you
# provide a block to compute the property, however, the value will be
# inserted directly.
def property(option_key, default_value=:__UNDEFINED__, opts={}, &block)
ret = _pair(option_key, default_value, opts, &block)
key = ret[0].camelize(:lower)
unless ret[2] # ignore
value = ret[1]
value = prepare_for_javascript(value) unless block_given?
@_properties[key] = value
end
# also look for a matching binding and set it needed.
if v = @bindings[option_key.to_sym] || @bindings[option_key.to_s]
v = v.include?('(') ? v : prepare_for_javascript(v)
@_properties["#{key}Binding"] = v
@bindings.delete option_key.to_sym
@bindings.delete option_key.to_s
end
end
def prepare_for_javascript(value)
return 'null' if value.nil?
case value
when Array:
"[#{value.map { |v| prepare_for_javascript(v) } * ','}]"
when Hash:
items = value.map do |k,v|
[prepare_for_javascript(k),prepare_for_javascript(v)] * ': '
end
"{ #{items * ', '} }"
when FalseClass:
"false"
when TrueClass:
"true"
else
%("#{ value.to_s.gsub('"','\"').gsub("\n",'\n') }")
end
end
### INTERNAL SUPPORT
private
def _do_render(render_item)
if render_item.nil?
''
elsif render_item.instance_of?(Proc)
render_item.call
else
render_item
end
end
def _pair(option_key, default_value, opts, look_for_key = true)
if default_value.instance_of?(Hash)
opts = default_value
default_value = :__UNDEFINED__
end
# get the attribute value. possibly return if no value and optional.
optional = opts.has_key?(:optional) ? opts[:optional] : (default_value == :__UNDEFINED__)
if opts[:constant] == true
value = default_value
elsif look_for_key && options.has_key?(option_key.to_sym)
value = options[option_key.to_sym]
elsif look_for_key && options.has_key?(option_key.to_s)
value = options[option_key.to_s]
else
value = default_value
end
if (optional==true) && value == :__UNDEFINED__
ignore = true
value = nil
else
ignore = false
value = nil if value == :__UNDEFINED__
value = yield(value) if block_given?
end
attr_key = (opts[:key] || option_key).to_s
[attr_key, value, ignore]
end
end
class HelperState
attr_reader :name
attr_reader :parent_helper
attr_reader :prepare_block
def initialize(helper_name, opts={}, &block)
@name = helper_name
@prepare_block = block
unless helper_name == :view
@parent_helper = SproutCore::ViewHelperSupport.find_helper(opts[:wraps] || opts[:extends] || :view)
end
@extends = opts[:wraps].nil?
end
def prepare_context(render_context)
# automatically call parent helper if extends was used.
if parent_helper && @extends
parent_helper.prepare_context(render_context)
else
render_context.current_helper = self
end
render_context.instance_eval &prepare_block
render_context.current_helper = nil
end
end
extend SproutCore::Helpers::CaptureHelper
extend SproutCore::Helpers::TextHelper
# :outlet => define if you want this to be used as an outlet.
# :prototype => define if you want this to be used as a prototype.
def self.render_view(view_helper_id, item_id, opts={}, client_builder=nil, render_source=nil, &block)
# item_id is optional. If it is not a symbol or string, then generate
# an item_id
if item_id.instance_of?(Hash)
opts = item_id; item_id = nil
end
item_id = render_source.dom_id! if item_id.nil?
# create the new render context and set it.
client_builder = opts[:client] if opts[:client]
rc = RenderContext.new(view_helper_id, item_id, opts, client_builder, render_source)
hs = find_helper(view_helper_id)
# render the inner_html using the block, if one is given.
SproutCore::PageHelper.push_render_context(rc)
rc.options[:inner_html] = render_source.capture(&block) if block_given?
# now, use the helper state to prepare the render context. This will
# extract the properties from the options and setup the render procs.
hs.prepare_context(rc) unless hs.nil?
# now have the render context render the HTML content. This may also
# make changes to the other items to render.
ret = rc.render_content
SproutCore::PageHelper.pop_render_context
# get the JS. Save as an outlet or in the page.
cur_rc = opts[:current_context] || SproutCore::PageHelper.current_render_context
view_class = opts[:view] || rc.view_class
unless view_class.nil?
view_settings = { :id => item_id, :class => view_class, :properties => rc.render_view, :lazy => opts[:lazy], :outlet_path => opts[:outlet_path] }
# if an outlet item is passed, then register this as an outlet.
outlet = opts[:outlet]
if outlet.nil?
outlet = opts[:field].nil? ? !cur_rc.nil? : [opts[:field].to_s, 'field'].join('_').to_sym
end
define = opts[:define]
if outlet && cur_rc
outlet = item_id if outlet == true
cur_rc.set_outlet(outlet, view_settings)
elsif define
define = define.to_s.camelize.gsub('::','.')
SproutCore::PageHelper.set_define(define, view_settings)
# otherwise, add it to the page-wide setting.
else
prop = item_id.to_s.camelize(:lower)
SproutCore::PageHelper.set_outlet(prop, view_settings)
end
end
# get the styles, if any
styles = rc.render_styles
SproutCore::PageHelper.add_styles(styles) if styles && styles.size > 0
# done. return the generated HTML
render_source.concat(ret,block) if block_given?
return ret
end
end
module ViewHelpers
def view_helper(helper_name,opts={},&prepare_block)
hs = SproutCore::ViewHelperSupport::HelperState.new(helper_name,opts,&prepare_block)
SproutCore::ViewHelperSupport.set_helper(helper_name, hs)
## install the helper method
eval %{
def #{helper_name}(item_id=nil, opts={}, &block)
SproutCore::ViewHelperSupport.render_view(:#{helper_name}, item_id, opts, bundle, self, &block)
end }
end
def render_page_views(prefix = 'SC')
ret = %()
SproutCore::PageHelper.reset!
return ret
end
# Call this method to load a helper. This will get the file contents
# and eval it.
def require_helpers(helper_name, bundle=nil)
# save bundle for future use
unless bundle.nil?
old_helper_bundle = @helper_bundle
@helper_bundle = bundle
end
# Get all the helper paths we want to load
if helper_name.nil?
paths = @helper_bundle.helper_paths
else
paths = [@helper_bundle.helper_for(helper_name)]
end
paths.compact!
# Create list of loaded helper paths
@loaded_helpers ||= []
# If a helper path was found, load it. May require other helpers
paths.each do |path|
next if @loaded_helpers.include?(path)
@loaded_helpers << path
eval(@helper_bundle.helper_contents_for(path))
end
# restore old bundle helper.
unless bundle.nil?
@helper_bundle = old_helper_bundle
end
end
end
end