first working attempt for editable_short_text liquid tag + update of the admin ui to reflect the new way of managing editable elements

This commit is contained in:
dinedine 2010-08-27 17:40:03 +02:00
parent eea6d4f8ce
commit 0d72a9f932
16 changed files with 202 additions and 151 deletions

View File

@ -9,6 +9,7 @@ class EditableElement
field :default_content
field :hint
field :disabled, :type => Boolean, :default => false
field :from_parent, :type => Boolean, :default => false
## associations ##
embedded_in :page, :inverse_of => :editable_elements

View File

@ -7,12 +7,23 @@ module Models
included do
embeds_many :editable_elements
accepts_nested_attributes_for :editable_elements
end
module InstanceMethods
def disable_parent_editable_elements(block)
self.editable_elements.each { |el| el.disabled = true if el.from_parent? && el.block == block }
end
def editable_element_blocks
self.editable_elements.collect(&:block)
end
def editable_elements_grouped_by_blocks
self.editable_elements.group_by(&:block)
groups = self.editable_elements.group_by(&:block)
groups.delete_if { |block, elements| elements.all? { |el| el.disabled? } }
end
def find_editable_element(block, slug)
@ -30,6 +41,7 @@ module Models
end
def disable_all_editable_elements
# TODO: only if block != blank
self.editable_elements.each { |el| el.disabled = true }
end
@ -39,10 +51,14 @@ module Models
def merge_editable_elements_from_page(source)
source.editable_elements.each do |el|
next if el.disabled?
existing_el = self.find_editable_element(el.block, el.slug)
if existing_el.nil? # new one from parents
self.editable_elements.build(el.attributes.merge(:disabled => true))
self.editable_elements.build(el.attributes.merge(:from_parent => true))
else
existing_el.disabled = false
end
end
end

View File

@ -58,11 +58,11 @@ module Models
context = default_context.merge(context)
# puts "*** enter context = #{context.object_id}"
puts "*** [#{self.fullpath}] enter context = #{context.object_id} / #{context[:page].fullpath}"
@template = ::Liquid::Template.parse(self.raw_template, context)
# puts "*** exit context = #{context.object_id}"
puts "*** exit context = #{context.object_id}"
self.template_dependencies = context[:templates]
self.snippet_dependencies = context[:snippets]
@ -85,7 +85,7 @@ module Models
# group them by fullpath for better performance
cached = template_descendants.inject({}) { |memo, page| memo[page.fullpath] = page; memo }
# puts "*** [#{self.fullpath}] #{template_descendants.collect(&:fullpath).inspect}"
puts "*** [#{self.fullpath}] #{template_descendants.collect(&:fullpath).inspect}"
self._update_direct_template_descendants(template_descendants, cached)
@ -96,13 +96,13 @@ module Models
end
def _update_direct_template_descendants(template_descendants, cached)
# puts "*** [#{self.fullpath}] _update_direct_template_descendants"
puts "*** [#{self.fullpath}] _update_direct_template_descendants"
direct_descendants = template_descendants.select do |page|
# puts "*** \t\t[#{self.fullpath}] _update_direct_template_descendants (#{page.template_dependencies.inspect})"
puts "*** \t\t[#{self.fullpath}] _update_direct_template_descendants (#{page.template_dependencies.inspect})"
((page.template_dependencies || [])- (self.template_dependencies || [])).size == 1
end
# puts "*** [#{self.fullpath}] direct = #{direct_descendants.inspect}"
puts "*** [#{self.fullpath}] direct = #{direct_descendants.inspect}"
direct_descendants.each do |page|
page.send(:_parse_and_serialize_template, { :cached_parent => self, :cached_pages => cached })

View File

@ -1,6 +1,6 @@
- content_for :head do
= javascript_include_tag 'admin/plugins/codemirror/codemirror', 'admin/plugins/wslide', 'admin/pages', 'admin/page_parts'
= stylesheet_link_tag 'admin/page_parts'
= javascript_include_tag 'admin/plugins/codemirror/codemirror', 'admin/plugins/wslide', 'admin/pages', 'admin/editable_elements'
= stylesheet_link_tag 'admin/editable_elements'
= f.foldable_inputs :name => :information do
@ -21,26 +21,39 @@
= f.input :cache_strategy, :as => :select, :collection => options_for_page_cache_strategy, :include_blank => false
#editable-elements
- grouped_editable_elements = @page.editable_elements_grouped_by_blocks
.nav
- grouped_editable_elements.keys.each_with_index do |name, index|
= link_to content_tag(:span, name.try(:humanize) || t('.default_block')), "#block-#{index}", :id => "block-nav-#{index}", :class => "#{'on' if index == 0}"
.clear
.wrapper
%ul{ :id => "blocks" }
- grouped_editable_elements.keys.each_with_index do |name, index|
- elements = grouped_editable_elements[name]
%li{ :id => "block-#{index}", :class => 'block', :style => "display: #{index == 0 ? 'block' : 'none' }" }
%fieldset.inputs
%ol
- elements.each_with_index do |el, index|
= f.fields_for 'editable_elements', el, :child_index => el._index do |g|
= g.input :content, :label => el.slug.humanize, :hint => el.hint
/
/
/ = f.fields_for :editable_elements do |g|
/ %li
/ = g.label :value, g.object.name, :style => "display:none"
/ %code= g.text_area :value
/ = g.hidden_field :name
/ = g.hidden_field :slug
/ = g.hidden_field :disabled, :class => 'disabled'
= f.foldable_inputs :name => :raw_template do
= f.custom_input :value, :css => 'code full', :with_label => false do
= f.label :raw_template
%code{ :class => 'html' }
= f.text_area :raw_template
/ .more
/ = link_to t('admin.image_picker.link'), admin_theme_assets_path, :id => 'image-picker-link'
/ #page-parts
/ .nav
/ - @page.parts.each_with_index do |part, index|
/ = link_to content_tag(:span, part.name), "#parts-#{index + 1}", :id => "control-part-#{part.slug}", :class => "part-#{index} #{'on' if index == 0}", :style => "#{'display: none' if part.disabled?}"
/ .clear
/
/ .wrapper
/ %ul{ :id => "parts" }
/ = f.fields_for :parts do |g|
/ %li{ :style => "#{'display: none' if g.object.disabled?}" }
/ = g.label :value, g.object.name, :style => "display:none"
/ %code= g.text_area :value
/ = g.hidden_field :name
/ = g.hidden_field :slug
/ = g.hidden_field :disabled, :class => 'disabled'
/ = link_to t('admin.image_picker.link'), admin_theme_assets_path, :id => 'image-picker-link'

View File

@ -99,6 +99,7 @@ en:
help: "The page title can be updated by clicking it."
ask_for_title: "Please type the new page title"
form:
default_block: Default
cache_strategy:
none: None
simple: Simple

View File

@ -99,6 +99,7 @@ fr:
help: "Le titre de la page est modifiable en cliquant dessus."
ask_for_title: "Veuillez entrer le nouveau titre"
form:
default_block: Défaut
cache_strategy:
none: Aucun
simple: Simple

View File

@ -1,5 +1,11 @@
BOARD:
x bug editable_xxx disabled for nil block
- duplicated block name
- refactor slugify method (use parameterize + create a module)
- [content types] the "display column" selector should not include file types

View File

@ -16,6 +16,26 @@ Scenario: Simple short text element
My application says Hello world
"""
Scenario: Updating a page
Given a page named "hello-world" with the template:
"""
My application says {% editable_short_text 'a_sentence' %}Hello world{% endeditable_short_text %}
{% block main %}Main{% endblock %}
"""
When I update the "hello-world" page with the template:
"""
My application says {% editable_short_text 'a_sentence' %}Hello world{% endeditable_short_text %}
{% block main %}Main{% endblock %}
{% block sidebar %}{% editable_short_text 'title' %}Default sidebar title{% endeditable_short_text %}{% endblock %}
"""
And I view the rendered page at "/hello-world"
Then the rendered output should look like:
"""
My application says Hello world
Main
Default sidebar title
"""
Scenario: Modified short text element
Given a page named "hello-world" with the template:
"""
@ -68,4 +88,29 @@ Scenario: Modified short text element inside a block and with page inheritance
Then the rendered output should look like:
"""
My application says Bonjour
"""
Scenario: Combine inheritance and update
Given a page named "hello-world" with the template:
"""
My application says {% editable_short_text 'a_sentence' %}Hello world{% endeditable_short_text %}
{% block main %}Main{% endblock %}
"""
Given a page named "another-hello-world" with the template:
"""
{% extends hello-world %}
{% block main %}Another Main{% endblock %}
"""
When I update the "hello-world" page with the template:
"""
My application says {% editable_short_text 'a_sentence' %}Hello world{% endeditable_short_text %}
{% block main %}Main{% endblock %}
{% block sidebar %}{% editable_short_text 'title' %}Default sidebar title{% endeditable_short_text %}{% endblock %}
"""
And I view the rendered page at "/another-hello-world"
Then the rendered output should look like:
"""
My application says Hello world
Another Main
Default sidebar title
"""

View File

@ -39,9 +39,9 @@ end
# update a page
When /^I update the "([^"]*)" page with the template:$/ do |page_slug, template|
puts "*************"
page = @site.pages.where(:slug => page_slug).first
page.update_attributes :raw_template => template
page.raw_template = template
page.save!
end
# try to render a page by slug

View File

@ -17,13 +17,15 @@ module Locomotive
super
# puts "@nodelist = #{@nodelist.inspect}"
puts "context = #{context.object_id} / #{@context[:page]}"
@context[:page].add_or_update_editable_element({
context[:page].add_or_update_editable_element({
:block => @context[:current_block].try(:name),
:slug => @slug,
:hint => @options[:hint],
:default_content => @nodelist.first.to_s,
:disabled => false
:disabled => false,
:from_parent => false
})
end

View File

@ -12,6 +12,8 @@ module Locomotive
raise SyntaxError.new("Error in tag 'extends' - Valid syntax: extends [template]")
end
puts "** [Extends] #{context[:page].fullpath}"
@context = context
retrieve_parent_page
@ -35,8 +37,9 @@ module Locomotive
blocks = find_blocks(template.root.nodelist)
blocks.each_value do |block|
puts "*** [Extends] merging #{block.name} / #{@context.object_id}"
block.send(:instance_variable_set, :"@context", @context)
block.end_tag
block.send(:register_current_block)
end
@context[:snippets] = page.snippet_dependencies

View File

@ -3,16 +3,56 @@ module Locomotive
module Tags
class InheritedBlock < ::Liquid::InheritedBlock
def end_tag
super
if self.contains_super?(@nodelist) # then enable all editable_elements (coming from the parent block too)
@context[:page].enable_editable_elements(@name)
def initialize(tag_name, markup, tokens, context)
if markup =~ Syntax
@name = $1
else
raise SyntaxError.new("Error in tag 'block' - Valid syntax: block [name]")
end
context[:current_block] = self # for now, no need to push it in a stack
puts "** InheritedBlock[begin] #{context.object_id} / #{@name} / #{context[:page].try(:fullpath)}"
super if tokens
end
def end_tag
puts "** InheritedBlock[end_tag] before super #{@name} / #{@context.object_id}/ #{@context[:page].fullpath}"
self.register_current_block
if !self.contains_super?(@nodelist) # then disable all editable_elements coming from the parent block too and not used
puts "** InheritedBlock[end_tag] disabling_parent_editable_elements... #{@context.object_id}"
@context[:page].disable_parent_editable_elements(@name)
end
puts "** InheritedBlock[end_tag] after super #{@name} / #{@context.object_id}/ #{@context[:page].fullpath}"
end
protected
def register_current_block
@context[:blocks] ||= {}
block = @context[:blocks][@name]
if block
# needed for the block.super statement
# puts "[BLOCK #{@name}|end_tag] nodelist #{@nodelist.inspect}"
block.add_parent(@nodelist)
@parent = block.parent
@nodelist = block.nodelist
# puts "[BLOCK #{@name}|end_tag] direct parent #{block.parent.inspect}"
else
# register it
# puts "[BLOCK #{@name}|end_tag] register it"
@context[:blocks][@name] = self
end
end
def contains_super?(nodelist)
nodelist.any? do |node|
if node.is_a?(String) && node =~ /\{\{\s*block.super\s*\}\}/

View File

@ -12,11 +12,11 @@ module Locomotive
@slug = @template_name.gsub('\'', '')
context[:snippets] << @slug
@context[:snippets] << @slug
snippet = context[:site].snippets.where(:slug => @slug).first
snippet = @context[:site].snippets.where(:slug => @slug).first
self.refresh(snippet, context) if snippet
self.refresh(snippet) if snippet
end
def render(context)
@ -49,7 +49,7 @@ module Locomotive
@partial = nil
else
@snippet_id = snippet.id
@partial = ::Liquid::Template.parse(snippet.template, context)
@partial = ::Liquid::Template.parse(snippet.template, @context.clone)
@partial.root.context.clear
end
end

View File

@ -0,0 +1,15 @@
$(document).ready(function() {
$('#editable-elements .nav a').click(function(e) {
var index = parseInt($(this).attr('href').match(/block-(.+)/)[1]);
$('#editable-elements .wrapper ul li.block').hide();
$('#block-' + index).show();
$(this).parent().find('.on').removeClass('on');
$(this).addClass('on');
e.preventDefault();
});
});

View File

@ -1,88 +0,0 @@
$(document).ready(function() {
// slider
var resetSlider = function() {
$('#page-parts .wrapper ul').wslide({
width: 880,
height: 400,
autolink: false,
duration: 300,
horiz: true
});
}
resetSlider();
// codemirror
$('#parts code textarea').each(function() { addCodeMirrorEditor('liquid', $(this)); });
var refreshParts = function(parts) {
// console.log('refreshParts');
$('#page-parts .nav a').removeClass('enabled');
$(parts).each(function() {
// console.log("iterating..." + this.slug);
var control = $('#control-part-' + this.slug);
// adding missing part
if (control.size() == 0) {
// console.log('adding part');
var nbParts = $('#page-parts .nav a').size();
$('#page-parts .nav .clear').before('<a id="control-part-' + this.slug + '" class="enabled part-' + nbParts + '" href="#parts-' + (nbParts + 1) + '"><span>' + this.name + '</span></a>');
var textareaInput = '<textarea rows="20" name="page[parts_attributes][' + nbParts + '][value]" id="page_parts_attributes_' + nbParts + '_value"/>';
var hiddenInputs = '<input type="hidden" name="page[parts_attributes][' + nbParts + '][name]" value="' + this.name + '" />'
hiddenInputs += '<input type="hidden" name="page[parts_attributes][' + nbParts + '][slug]" value="' + this.slug + '" />'
$('#page-parts .wrapper ul').append('<li id="part-' + nbParts + '" class="new"><code>' + textareaInput + '</code>' + hiddenInputs + '</li>');
resetSlider();
$('#parts li:last code textarea').each(function() { addCodeMirrorEditor('liquid', $(this)); });
} else {
var index = parseInt(control.attr('class').match(/part-(.+)/)[1]) + 1;
var wrapper = $('#parts-' + index);
// updating part
wrapper.find('input.disabled').val(this.disabled == true ? 'true' : 'false');
if (this.disabled == false) {
control.html('<span>' + this.name + '</span>').addClass('enabled').show();
wrapper.show();
} else {
control.hide();
wrapper.hide();
}
}
});
// removing or hiding parts
$('#page-parts .nav a:not(.enabled)').each(function() {
var index = parseInt($(this).attr('class').match(/part-(.+)/)[1]) + 1;
var wrapper = $('#parts-' + index);
if (wrapper.hasClass('new')) {
$(this).remove(); wrapper.remove();
} else {
wrapper.find('input.disabled').val('true');
$(this).hide(); wrapper.hide();
}
});
// go to the first one if we hide the selected wrapper
var selectedNav = $('#page-parts .nav a.on');
if (selectedNav.size() == 1) {
var index = parseInt(selectedNav.attr('class').match(/part-(.+)/)[1]) + 1;
if ($('#parts-' + index + ':visible').size() == 0)
$('#page-parts .nav a:first').click();
} else
$('#page-parts .nav a:first').click();
}
var loadPartsFromLayout = function() {
if ($('#page_layout_id').val() == '')
return ;
var url = $('#page_layout_id').attr('data_url').replace('_id_to_replace_', $('#page_layout_id').val());
$.get(url, '', function(data) { refreshParts(data); }, 'json');
}
$('#page_layout_id').change(loadPartsFromLayout);
});

View File

@ -1,25 +1,26 @@
#page-parts .wrapper {
#editable-elements .wrapper {
background: #ebedf4 url(/images/admin/form/footer.png) no-repeat 0 bottom;
width: 880px;
padding: 20px 20px;
padding: 10px 20px 0px 20px;
border-top: 1px solid #ccced7;
}
#page-parts {
#editable-elements {
background: transparent url(/images/admin/form/header.png) no-repeat 0 0;
margin-bottom: 20px;
}
#page-parts .control {
#editable-elements .control {
height: 30px;
}
#page-parts .nav {
#editable-elements .nav {
position: relative;
top: 1px;
z-index: 990;
}
#page-parts .nav a {
#editable-elements .nav a {
float: left;
display: block;
height: 30px;
@ -31,37 +32,32 @@
outline: none;
}
#page-parts .nav a span {
#editable-elements .nav a span {
display: inline-block;
height: 26px;
padding: 4px 11px 0 0px;
}
#page-parts .nav a:first-child { padding-left: 22px; }
#page-parts .nav a:first-child span { padding-right: 13px; }
#editable-elements .nav a:first-child { padding-left: 22px; }
#editable-elements .nav a:first-child span { padding-right: 13px; }
#page-parts .nav a.on {
#editable-elements .nav a.on {
color: #1e1f26;
font-weight: bold;
background: transparent url(/images/admin/form/header-left-on.png) no-repeat 0 0;
}
#page-parts .nav a.on span {
#editable-elements .nav a.on span {
background: transparent url(/images/admin/form/header-right-on.png) no-repeat right 0;
}
#page-parts .nav a:first-child.on {
#editable-elements .nav a:first-child.on {
background: transparent url(/images/admin/form/header-first-on.png) no-repeat 0 0;
}
#page-parts .wslide-wrap {
border: 1px solid #a6a8b8;
}
#editable-elements .wrapper ul li fieldset { margin-bottom: 0px; }
#editable-elements .wrapper ul li fieldset ol { margin-top: 0px; border-top: 0px; background: #EBEDF4; }
#editable-elements .wrapper ul li fieldset ol li label { padding-left: 0px; padding-right: 3em; }
#editable-elements .wrapper ul li fieldset ol li p.inline-hints { margin-left: 20%; }
#page-parts code { display: block; background: white; height: 400px; }
#page-parts code textarea {
width: 880px;
height: 400px;
background: transparent url(/images/admin/form/field.png) repeat-x 0 0;
border: 0px;
}