module Alchemy
# This helpers are useful to render elements from pages.
#
# The most important helper for frontend developers is the {#render_elements} helper.
#
module ElementsHelper
include Alchemy::EssencesHelper
include Alchemy::UrlHelper
include Alchemy::ElementsBlockHelper
# Renders all elements from current page
#
# == Examples:
#
# === Render only certain elements:
#
#
# <%= render_elements only: ['header', 'claim'] %>
#
#
# <%= render_elements except: ['header', 'claim'] %>
#
#
# === Render elements from global page:
#
#
#
# === Render elements from cell:
#
#
#
# === Fallback to elements from global page:
#
# You can use the fallback option as an override for elements that are stored on another page.
# So you can take elements from a global page and only if the user adds an element on current page the
# local one gets rendered.
#
# 1. You have to pass the the name of the element the fallback is for as for key.
# 2. You have to pass a page_layout name or {Alchemy::Page} from where the fallback elements is taken from as from key.
# 3. You can pass the name of element to fallback with as with key. This is optional (the element name from the for key is taken as default).
#
# <%= render_elements(fallback: {
# for: 'contact_teaser',
# from: 'sidebar',
# with: 'contact_teaser'
# }) %>
#
# @param [Hash] options
# Additional options.
#
# @option options [Number] :count
# The amount of elements to be rendered (begins with first element found)
# @option options [Array or String] :except ([])
# A list of element names not to be rendered.
# @option options [Hash] :fallback
# Define elements that are rendered from another page.
# @option options [Alchemy::Cell or String] :from_cell
# The cell the elements are rendered from. You can pass a {Alchemy::Cell} name String or a {Alchemy::Cell} object.
# @option options [Alchemy::Page or String] :from_page (@page)
# The page the elements are rendered from. You can pass a page_layout String or a {Alchemy::Page} object.
# @option options [Array or String] :only ([])
# A list of element names only to be rendered.
# @option options [Boolean] :random
# Randomize the output of elements
# @option options [Boolean] :reverse
# Reverse the rendering order
# @option options [String] :sort_by
# The name of a {Alchemy::Content} to sort the elements by
# @option options [String] :separator
# A string that will be used to join the element partials. Default nil
#
def render_elements(options = {})
options = {
from_page: @page,
render_format: 'html'
}.update(options)
pages = pages_holding_elements(options.delete(:from_page))
if pages.blank?
warning('No page to get elements from was found')
return
end
elements = collect_elements_from_pages(pages, options)
if options[:sort_by].present?
elements = sort_elements_by_content(elements, options.delete(:sort_by))
end
render_element_view_partials(elements, options)
end
# This helper renders a {Alchemy::Element} partial.
#
# A element has always two partials:
#
# 1. A view partial (This is the view presented to the website visitor)
# 2. A editor partial (This is the form presented to the website editor while in page edit mode)
#
# The partials are located in app/views/alchemy/elements.
#
# == View partial naming
#
# The partials have to be named after the name of the element as defined in the elements.yml file and has to be suffixed with the partial part.
#
# === Example
#
# Given a headline element
#
# # elements.yml
# - name: headline
# contents:
# - name: text
# type: EssenceText
#
# Then your element view partials has to be named like:
#
# app/views/alchemy/elements/_headline_editor.html.erb
# app/views/alchemy/elements/_headline_view.html.erb
#
# === Element partials generator
#
# You can use this handy generator to let Alchemy generate the partials for you:
#
# $ rails generate alchemy:elements --skip
#
# == Usage
#
# <%= render_element(Alchemy::Element.published.named(:headline).first) %>
#
# @param [Alchemy::Element] element
# The element you want to render the view for
# @param [Symbol] part
# The type of element partial (:editor or :view) you want to render
# @param [Hash] options
# Additional options
# @param [Number] counter
# a counter
#
# @note If the view partial is not found alchemy/elements/_view_not_found.html.erb
# or alchemy/elements/_editor_not_found.html.erb gets rendered.
#
def render_element(element, part = :view, options = {}, counter = 1)
if element.nil?
warning('Element is nil')
render "alchemy/elements/#{part}_not_found", {name: 'nil'}
return
end
element.store_page(@page) if part.to_sym == :view
render "alchemy/elements/#{element.name}_#{part}", {
element: element,
counter: counter,
options: options
}.merge(options.delete(:locals) || {})
rescue ActionView::MissingTemplate => e
warning(%(
Element #{part} partial not found for #{element.name}.\n
#{e}
))
render "alchemy/elements/#{part}_not_found", {
name: element.name,
error: "Element #{part} partial not found. Use rails generate alchemy:elements to generate it."
}
end
# Returns a string for the id attribute of a html element for the given element
def element_dom_id(element)
return "" if element.nil?
"#{element.name}_#{element.id}".html_safe
end
# Renders the HTML tag attributes required for preview mode.
def element_preview_code(element)
tag_options(element_preview_code_attributes(element))
end
# Returns a hash containing the HTML tag attributes required for preview mode.
def element_preview_code_attributes(element)
return {} unless element.present? && @preview_mode && element.page == @page
{ :'data-alchemy-element' => element.id }
end
# Returns the element's tags information as a string. Parameters and options
# are equivalent to {#element_tags_attributes}.
#
# @see #element_tags_attributes
#
# @return [String]
# HTML tag attributes containing the element's tag information.
#
def element_tags(element, options = {})
tag_options(element_tags_attributes(element, options))
end
# Returns the element's tags information as an attribute hash.
#
# @param [Alchemy::Element] element The {Alchemy::Element} you want to render the tags from.
#
# @option options [Proc] :formatter
# ('lambda { |tags| tags.join(' ') }')
# Lambda converting array of tags to a string.
#
# @return [Hash]
# HTML tag attributes containing the element's tag information.
#
def element_tags_attributes(element, options = {})
options = {
formatter: lambda { |tags| tags.join(' ') }
}.merge(options)
return {} if !element.taggable? || element.tag_list.blank?
{ :'data-element-tags' => options[:formatter].call(element.tag_list) }
end
# Sort given elements by content.
#
# @param [Array] elements - The elements you want to sort
# @param [String] content_name - The name of the content you want to sort by
#
# @return [Array]
#
def sort_elements_by_content(elements, content_name)
elements.sort_by do |element|
content = element.content_by_name(content_name)
content ? content.ingredient.to_s : ''
end
end
private
def pages_holding_elements(page)
case page
when String
Language.current.pages.where(
page_layout: page,
restricted: false
).to_a
when Page
page
end
end
def collect_elements_from_pages(page, options)
if page.is_a? Array
elements = page.collect { |p| p.find_elements(options) }.flatten
else
elements = page.find_elements(options)
end
if fallback_required?(elements, options)
elements += fallback_elements(options)
end
elements
end
def fallback_required?(elements, options)
options[:fallback] && elements.detect { |e| e.name == options[:fallback][:for] }.nil?
end
def fallback_elements(options)
fallback_options = options.delete(:fallback)
case fallback_options[:from]
when String
page = Language.current.pages.find_by(
page_layout: fallback_options[:from],
restricted: false
)
when Page
page = fallback_options[:from]
end
return [] if page.blank?
page.elements.named(fallback_options[:with].blank? ? fallback_options[:for] : fallback_options[:with])
end
def render_element_view_partials(elements, options = {})
buff = []
elements.each_with_index do |element, i|
buff << render_element(element, :view, options, i + 1)
end
buff.join(options[:separator]).html_safe
end
end
end