allow users to use shortcut for theme images when editing snippets / layouts / stylesheets + fix minor ui bugs

This commit is contained in:
dinedine 2010-07-23 00:10:40 +02:00
parent a03b631a71
commit 2608430cae
21 changed files with 461 additions and 359 deletions

View File

@ -19,7 +19,7 @@ h2. Gems
Here is a short list of main gems used in the application.
* Rails 3 (beta 3)
* Rails 3 (beta 4)
* Mongoid
* Liquid
* Devise
@ -30,12 +30,17 @@ h2. Installation
See the "official website":http://www.locomotiveapp.org
h2. Team
* Developers: "Didier Lafforgue":http://www.nocoffee.fr, "Jacques Crocker":http://www.railsjedi.com
* UI Designer: "Sacha Greif":http://www.sachagreif.com
h2. Credits
Many thanks to "Sacha Greif":http://www.sachagreif.com for his great work on the user interface and the LocomotiveApp website front page.
"Rodrigo Alvarez":http://blog.codecaster.es/ for his plugin named Congo which gave us a good starting point and for his availability for (very late) tech discussions.
"Emmanuel Grard":http://www.grardesign.com designed the awesome locomotive illustration in the LocomotiveApp.org landing page.
h2. Contact
Feel free to contact me at didier at nocoffee dot fr.

View File

@ -1,39 +1,43 @@
module Admin
class ThemeAssetsController < BaseController
include ActionView::Helpers::SanitizeHelper
extend ActionView::Helpers::SanitizeHelper::ClassMethods
include ActionView::Helpers::TextHelper
sections 'settings', 'theme_assets'
respond_to :json, :only => [:create, :update]
def index
assets = current_site.theme_assets.all
@non_image_assets = assets.find_all { |a| a.stylesheet? || a.javascript? }
@image_assets = assets.find_all { |a| a.image? }
@flash_assets = assets.find_all { |a| a.movie? }
if request.xhr?
render :action => 'images', :layout => false and return
end
end
def create
params[:theme_asset] = { :source => params[:file] } if params[:file]
create! do |success, failure|
success.json do
render :json => {
:status => 'success',
:name => truncate(@theme_asset.slug, :length => 22),
:url => @theme_asset.source.url,
:vignette_url => @theme_asset.vignette_url
render :json => {
:status => 'success',
:name => truncate(@theme_asset.slug, :length => 22),
:slug => @theme_asset.slug,
:url => @theme_asset.source.url,
:vignette_url => @theme_asset.vignette_url,
:shortcut_url => @theme_asset.shortcut_url
}
end
failure.json { render :json => { :status => 'error' } }
end
end
end
end

View File

@ -1,96 +1,118 @@
class ThemeAsset
include Locomotive::Mongoid::Document
## Extensions ##
## Extensions ##
include Models::Extensions::Asset::Vignette
## fields ##
field :slug
field :content_type
field :width, :type => Integer
field :height, :type => Integer
field :size, :type => Integer
field :plain_text
mount_uploader :source, ThemeAssetUploader
## associations ##
belongs_to_related :site
## callbacks ##
before_validation :sanitize_slug
before_validation :store_plain_text
before_save :set_slug
## validations ##
validate :extname_can_not_be_changed
validates_presence_of :site, :source
validates_presence_of :slug, :if => Proc.new { |a| a.new_record? && a.performing_plain_text? }
validates_uniqueness_of :slug, :scope => [:site_id, :content_type]
validates_integrity_of :source
## accessors ##
attr_accessor :performing_plain_text
## methods ##
%w{movie image stylesheet javascript font}.each do |type|
define_method("#{type}?") do
self.content_type == type
end
end
end
def stylesheet_or_javascript?
self.stylesheet? || self.javascript?
end
def plain_text
@plain_text ||= (if self.stylesheet_or_javascript?
self.source.read
if self.stylesheet_or_javascript?
self.plain_text = self.source.read if read_attribute(:plain_text).blank?
read_attribute(:plain_text)
else
nil
end)
end
end
def plain_text=(source)
self.performing_plain_text = true if self.performing_plain_text.nil?
@plain_text = source
write_attribute(:plain_text, source)
end
def performing_plain_text?
return true if !self.new_record? && self.stylesheet_or_javascript? && self.errors.empty?
!(self.performing_plain_text.blank? || self.performing_plain_text == 'false' || self.performing_plain_text == false)
end
def store_plain_text
return if self.plain_text.blank?
self.source = CarrierWave::SanitizedFile.new({
:tempfile => StringIO.new(self.plain_text),
# replace /theme/<content_type>/<slug> occurences by the real amazon S3 url or local files
sanitized_source = self.plain_text.gsub(/(\/theme\/([a-z]+)\/([a-z_\-0-9]+)\.[a-z]{2,3})/) do |url|
content_type, slug = url.split('/')[2..-1]
content_type = content_type.singularize
slug = slug.split('.').first
if asset = self.site.theme_assets.where(:content_type => content_type, :slug => slug).first
asset.source.url
else
url
end
end
self.source = CarrierWave::SanitizedFile.new({
:tempfile => StringIO.new(sanitized_source),
:filename => "#{self.slug}.#{self.stylesheet? ? 'css' : 'js'}"
})
end
def shortcut_url # ex: /theme/stylesheets/application.css is a shortcut for a theme asset (content_type => stylesheet, slug => 'application')
File.join('/theme', self.content_type.pluralize, "#{self.slug}#{File.extname(self.source_filename)}")
rescue
''
end
def to_liquid
{ :url => self.source.url }.merge(self.attributes)
end
protected
def sanitize_slug
self.slug.slugify!(:underscore => true) if self.slug.present?
end
def set_slug
if self.slug.blank?
self.slug = File.basename(self.source_filename, File.extname(self.source_filename))
self.sanitize_slug
end
end
def extname_can_not_be_changed
return if self.new_record?
if File.extname(self.source.file.original_filename) != File.extname(self.source_filename)
self.errors.add(:source, :extname_changed)
end

View File

@ -1,10 +1,10 @@
- content_for :head do
= javascript_include_tag 'admin/plugins/codemirror/codemirror'
= javascript_include_tag 'admin/plugins/codemirror/codemirror', 'admin/layouts.js'
= image_picker_include_tags
= f.inputs :name => :information do
= f.input :name
= f.inputs :name => :code do
= f.custom_input :value, :css => 'code full', :with_label => false do
%code{ :class => 'html' }

View File

@ -3,7 +3,7 @@
- edit = local_assigns.key?(:edit) ? edit : true
%li{ :class => "#{asset.new_record? ? 'new-asset' : 'asset'} #{'last' if (asset_counter + 1) % per_row == 0}"}
%h4= link_to truncate(asset.slug, :length => 18), edit ? edit_admin_theme_asset_path(asset) : asset.source.url
%h4= link_to truncate(asset.slug, :length => 18), edit ? edit_admin_theme_asset_path(asset) : asset.source.url, :"data-slug" => asset.slug, :"data-shortcut-url" => asset.shortcut_url
.image
.inside
= vignette_tag(asset)

View File

@ -7,6 +7,7 @@
#file-selector{ :class => "selector #{'hidden' if @theme_asset.performing_plain_text?}" }
= f.inputs :name => :information do
= f.input :source
= f.input :slug
- if @theme_asset.new_record? || @theme_asset.stylesheet_or_javascript?
%span.alt

View File

@ -6,7 +6,7 @@
- content_for :buttons do
= admin_button_tag t('admin.theme_assets.index.new'), new_admin_theme_asset_url, :class => 'new'
%p!= t('.help', :url => @theme_asset.source.url)
%p!= t('.help', :url => @theme_asset.source.url, :shortcut_url => @theme_asset.shortcut_url)
= semantic_form_for @theme_asset, :url => admin_theme_asset_url(@theme_asset), :html => { :multipart => true, :class => 'save-with-shortcut' } do |form|

View File

@ -16,9 +16,9 @@ Locomotive::Application.configure do
# Don't care if the mailer can't send
config.action_mailer.raise_delivery_errors = false
# config.action_mailer.default_url_options = { :host => 'localhost:3000' }
# MockSmtp settings
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {

View File

@ -172,7 +172,7 @@ en:
help: "You have the choice to either upload any file or to copy/paste a stylesheet or a javascript in plain text."
edit:
title: "Editing %{file}"
help: "You can use it by copying/pasting the following url: %{url}"
help: "You can insert the following shortcut url in your stylesheets: <strong>%{shortcut_url}</strong> OR use the direct url <strong>%{url}</strong>"
form:
picker_link: Insert a file into the code
choose_file: Choose file

View File

@ -14,14 +14,14 @@ fr:
edit:
title: Changer mon mot de passe
link: "&rarr; Retour page de connexion"
buttons:
login: Se connecter
send_password: Envoyer
change_password: Changer
fr:
admin:
admin:
buttons:
login: Se connecter
send_password: Envoyer
@ -134,7 +134,7 @@ fr:
new: nouveau gabarit
layout:
updated_at: Mis à jour le
snippets:
index:
title: Liste des snippets
@ -149,28 +149,28 @@ fr:
help: "Remplissez le formulaire ci-dessous pour mettre à jour votre snippet."
snippet:
updated_at: Mis à jour le
sites:
new:
title: "Nouveau site"
help: "Remplissez le formulaire ci-dessous pour créer votre nouveau site."
current_sites:
edit:
new_membership: ajouter compte
help: "Le nom du site est modifiable en cliquant dessus."
ask_for_name: "Veuillez entrer le nouveau nom"
memberships:
new:
title: "Ajout d'un compte"
help: "Donnez l'adresse email du compte à ajouter. S'il n'existe pas, vous serez redirigé(e) vers le formulaire de création d'un compte."
accounts:
new:
title: Nouveau compte
help: "Remplissez le formulaire ci-dessous pour ajouter un nouveau compte."
my_accounts:
edit:
help: "Votre nom est modifiable en cliquant dessus."
@ -178,7 +178,7 @@ fr:
en: en Anglais
fr: en Français
ask_for_name: "Veuillez entrer le nouveau nom"
theme_assets:
index:
title: Liste des fichiers du thème
@ -194,14 +194,14 @@ fr:
help: "Vous avez le choix de soit uploader n'importe quel fichier ou bien soit de copier/coller du code css ou javascript."
edit:
title: "Edition %{file}"
help: "Vous pouvez utiliser ce fichier grâce a l'url suivante: %{url}"
help: "Vous pouvez insérer le raccourci suivant dans vos feuilles de style: <strong>%{shortcut_url}</strong> OU utiliser directement l'url : %{url}"
form:
choose_file: Choisir fichier
choose_plain_text: Passer en mode texte
images:
title: Liste des images
no_items: "Il n'y a pas d'images."
asset_collections:
index:
title: Collections
@ -217,7 +217,7 @@ fr:
destroy: supprimer collection
no_items: "Il n'existe pas de médias. Vous pouvez commencer par créer un <a href='%{url}'>ici</a>."
ask_for_name: "Veuillez entrer le nouveau nom"
assets:
new:
title: "Nouveau média"
@ -225,7 +225,7 @@ fr:
edit:
title: "Edition média"
help: "Remplissez le formulaire ci-dessous pour mettre à jour votre média."
content_types:
index:
new: nouveau modèle
@ -241,27 +241,27 @@ fr:
order_by:
updated_at: 'Par date de mise à jour'
position_in_list: Manuellement
contents:
index:
title: 'Liste des "%{type}"'
edit: éditer modèle
destroy: supprimer modèle
download: télécharger éléments
new: nouvel élément
new: nouvel élément
category_noname: "Pas de nom"
lastest_items: "Eléments récents"
updated_at: "Mis à jour le"
list:
no_items: "Il n'existe pas d'éléments. Vous pouvez commencer par créer un <a href='%{url}'>ici</a>"
new:
title: '%{type} &mdash; nouvel élément'
title: '%{type} &mdash; nouvel élément'
edit:
title: '%{type} &mdash; édition élément'
image_picker:
link: Insérer une image dans le code
formtastic:
titles:
information: Informations générales
@ -289,7 +289,7 @@ fr:
custom_fields:
field:
_alias: Alias
hints:
page:
published: "Seuls les administrateurs authentifiés peuvent voir une page non publiée."

View File

@ -1,57 +1,57 @@
# Locomotive::Application.routes.draw do |map|
Rails.application.routes.draw do |map|
constraints(Locomotive::Routing::DefaultConstraint) do
root :to => 'home#show'
end
# admin authentication
devise_for :admin, :class_name => 'Account', :controllers => { :sessions => 'admin/sessions', :passwords => 'admin/passwords' }
# admin interface for each website
namespace 'admin' do
root :to => 'pages#index'
resources :pages do
resources :pages do
put :sort, :on => :member
get :get_path, :on => :collection
end
get :get_path, :on => :collection
end
resources :layouts do
resources :page_parts, :only => :index
end
resources :snippets
resources :site
resource :current_site
resources :accounts
resource :my_account
resources :memberships
resources :theme_assets
resources :asset_collections
resources :assets, :path => 'asset_collections/:collection_id/assets'
resources :assets, :path => 'asset_collections/:collection_id/assets'
resources :content_types
resources :contents, :path => 'content_types/:slug/contents' do
put :sort, :on => :collection
end
resources :api_contents, :path => 'api/:slug/contents', :controller => 'api_contents', :only => [:create]
resources :custom_fields, :path => 'custom/:parent/:slug/fields'
end
# sitemap
match '/sitemap.xml' => 'admin/sitemaps#show', :format => 'xml'
# magic urls
match '/' => 'admin/rendering#show'
match '*path' => 'admin/rendering#show'

View File

@ -1,13 +1,10 @@
BOARD:
- refactor slugify method (use parameterize + create a module)
- send email when new content added thru api
BACKLOG:
- rack app to map pretty asset url to S3
- notify accounts when new instance of models (opt): none, one or many accounts. Used for contact form.
- theme assets: disable version if not image
- new custom field types:
- belongs_to => association
- cucumber features for admin pages
@ -28,6 +25,7 @@ NICE TO HAVE:
- page with regexp url ?
- page redirection (option)
- automatic update !
- import / export site
DONE:
@ -64,4 +62,6 @@ x change action icons according to the right action [Sacha]
x publish event when saving form in ajax (for instance, in order to update account name or site name)
x page templatized (bound to a model)
x theme asset picker when editing layout / snippet
x templatized: do not display content with visible / active set to false
x templatized: do not display content with visible / active set to false
x theme assets: disable version if not image (handled by the new version of Carrierwave)
x rack app to map pretty asset url to S3 => shortcut urls instead

View File

@ -0,0 +1,19 @@
module Locomotive
module Liquid
module Drops
class ThemeImages < ::Liquid::Drop
def initialize(site)
@site = site
end
def before_method(meth)
asset = @site.theme_assets.where(:content_type => 'image', :slug => meth.to_s).first
!asset.nil? ? asset.source.url : nil
end
end
end
end
end

View File

@ -1,40 +1,40 @@
module Locomotive
module Render
extend ActiveSupport::Concern
module InstanceMethods
protected
def render_locomotive_page
@page = locomotive_page
redirect_to application_root_url and return if @page.nil?
output = @page.render(locomotive_context)
prepare_and_set_response(output)
end
def locomotive_page
path = request.fullpath.clone
path.gsub!(/\.[a-zA-Z][a-zA-Z0-9]{2,}$/, '')
path.gsub!(/^\//, '')
path = 'index' if path.blank?
if path != 'index'
dirname = File.dirname(path).gsub(/^\.$/, '') # also look for templatized page path
path = [path, File.join(dirname, 'content_type_template').gsub(/^\//, '')]
end
if page = current_site.pages.any_in(:fullpath => [*path]).first
if not page.published? and current_admin.nil?
page = nil
else
if page.templatized?
@content_instance = page.content_type.contents.where(:_slug => File.basename(path.first)).first
if @content_instance.nil? || (!@content_instance.visible? && current_admin.nil?) # content instance not found or not visible
page = nil
end
@ -44,7 +44,7 @@ module Locomotive
page || current_site.pages.not_found.published.first
end
def locomotive_context
assigns = {
'site' => current_site,
@ -52,35 +52,36 @@ module Locomotive
'asset_collections' => Locomotive::Liquid::Drops::AssetCollections.new(current_site),
'stylesheets' => Locomotive::Liquid::Drops::Stylesheets.new(current_site),
'javascripts' => Locomotive::Liquid::Drops::Javascripts.new(current_site),
'theme_images' => Locomotive::Liquid::Drops::ThemeImages.new(current_site),
'contents' => Locomotive::Liquid::Drops::Contents.new(current_site),
'current_page' => self.params[:page]
}
if @page.templatized? # add instance from content type
assigns['content_instance'] = @content_instance
assigns[@page.content_type.slug.singularize] = @content_instance # just here to help to write readable liquid code
end
registers = { :controller => self, :site => current_site, :page => @page }
::Liquid::Context.new(assigns, registers)
end
def prepare_and_set_response(output)
response.headers['Content-Type'] = 'text/html; charset=utf-8'
if @page.with_cache?
fresh_when :etag => @page, :last_modified => @page.updated_at.utc, :public => true
if @page.cache_strategy != 'simple' # varnish
response.cache_control[:max_age] = @page.cache_strategy
end
end
render :text => output, :layout => false, :status => :ok
end
end
end
end
end

View File

@ -1,43 +1,45 @@
module Locomotive
module Routing
module Routing
module SiteDispatcher
extend ActiveSupport::Concern
included do
before_filter :fetch_site
helper_method :current_site
if self.respond_to?(:before_filter)
before_filter :fetch_site
helper_method :current_site
end
end
module InstanceMethods
protected
def fetch_site
Locomotive.logger "[fetch site] host = #{request.host} / #{request.env['HTTP_HOST']}"
@current_site ||= Site.match_domain(request.host).first
end
def current_site
@current_site || fetch_site
end
def require_site
redirect_to application_root_url and return false if current_site.nil?
end
def validate_site_membership
return if current_site && current_site.accounts.include?(current_admin)
redirect_to application_root_url
end
def application_root_url
root_url(:host => Locomotive.config.default_domain, :port => request.port)
end
end
end
end
end
end
end

View File

@ -0,0 +1,7 @@
$(document).ready(function() {
$('a#image-picker-link').imagepicker({
insertFn: function(link) {
return "{{ theme_images." + link.attr('data-slug') + " }}";
}
});
});

View File

@ -1,69 +1,81 @@
$(document).ready(function() {
var copyLinkToEditor = function(link, event) {
var editor = CodeMirrorEditors[0].editor;
var handle = editor.cursorLine(), position = editor.cursorPosition(handle).character;
$.fn.imagepicker = function(options) {
editor.insertIntoLine(handle, position, link.attr('href'));
var defaults = {
insertFn: null
};
var options = $.extend(defaults, options);
event.stopPropagation();
event.preventDefault();
var copyLinkToEditor = function(link, event) {
var editor = CodeMirrorEditors[0].editor;
var handle = editor.cursorLine(), position = editor.cursorPosition(handle).character;
$.fancybox.close();
}
var value = options.insertFn != null ? options.insertFn(link) : link.attr('href');
var setupUploader = function() {
var multipartParams = {};
multipartParams[$('meta[name=csrf-param]').attr('content')] = $('meta[name=csrf-token]').attr('content');
editor.insertIntoLine(handle, position, value);
var uploader = new plupload.Uploader({
runtimes : (jQuery.browser.webkit == true ? 'flash' : 'html5,flash'),
container: 'theme-images',
browse_button : 'upload-link',
max_file_size : '5mb',
url : $('a#upload-link').attr('href'),
flash_swf_url : '/javascripts/admin/plugins/plupload/plupload.flash.swf',
multipart: true,
multipart_params: multipartParams
});
event.stopPropagation();
event.preventDefault();
uploader.bind('QueueChanged', function() {
uploader.start();
});
$.fancybox.close();
}
uploader.bind('FileUploaded', function(up, file, response) {
var json = JSON.parse(response.response);
var setupUploader = function() {
var multipartParams = {};
multipartParams[$('meta[name=csrf-param]').attr('content')] = $('meta[name=csrf-token]').attr('content');
if (json.status == 'success') {
var asset = $('.asset-picker ul li.new-asset')
.clone()
.insertBefore($('.asset-picker ul li.clear'))
.addClass('asset');
var uploader = new plupload.Uploader({
runtimes : (jQuery.browser.webkit == true ? 'flash' : 'html5,flash'),
container: 'theme-images',
browse_button : 'upload-link',
max_file_size : '5mb',
url : $('a#upload-link').attr('href'),
flash_swf_url : '/javascripts/admin/plugins/plupload/plupload.flash.swf',
multipart: true,
multipart_params: multipartParams
});
asset.find('h4 a').attr('href', json.url).html(json.name).bind('click', function(e) {
copyLinkToEditor($(this), e);
});
asset.find('.image .inside img').attr('src', json.vignette_url);
uploader.bind('QueueChanged', function() {
uploader.start();
});
if ($('.asset-picker ul li.asset').length % 3 == 0)
asset.addClass('last');
uploader.bind('FileUploaded', function(up, file, response) {
var json = JSON.parse(response.response);
asset.removeClass('new-asset');
if (json.status == 'success') {
var asset = $('.asset-picker ul li.new-asset')
.clone()
.insertBefore($('.asset-picker ul li.clear'))
.addClass('asset');
$('.asset-picker p.no-items').hide();
asset.find('h4 a').attr('href', json.url)
.attr('data-slug', json.slug)
.attr('data-shortcut-url', json.shortcut_url)
.html(json.name).bind('click', function(e) {
copyLinkToEditor($(this), e);
});
asset.find('.image .inside img').attr('src', json.vignette_url);
$('.asset-picker ul').scrollTo($('li.asset:last'), 400);
}
});
if ($('.asset-picker ul li.asset').length % 3 == 0)
asset.addClass('last');
uploader.init();
}
asset.removeClass('new-asset');
$('a#image-picker-link').fancybox({
'onComplete': function() {
setupUploader();
$('ul.assets h4 a').bind('click', function(e) { copyLinkToEditor($(this), e); });
}
});
});
$('.asset-picker p.no-items').hide();
$('.asset-picker ul').scrollTo($('li.asset:last'), 400);
}
});
uploader.init();
}
return this.each(function() {
$(this).fancybox({
'onComplete': function() {
setupUploader();
$('ul.assets h4 a').bind('click', function(e) { copyLinkToEditor($(this), e); });
}
});
});
};

View File

@ -1,16 +1,22 @@
$(document).ready(function() {
// automatic slug from snippet name
$('#snippet_name').keypress(function() {
var input = $(this);
var slug = $('#snippet_slug');
if (!slug.hasClass('filled')) {
setTimeout(function() {
slug.val(makeSlug(input.val()));
}, 50);
}
});
$('#snippet_slug').keypress(function() { $(this).addClass('filled'); });
// automatic slug from snippet name
$('#snippet_name').keypress(function() {
var input = $(this);
var slug = $('#snippet_slug');
if (!slug.hasClass('filled')) {
setTimeout(function() {
slug.val(makeSlug(input.val()));
}, 50);
}
});
$('#snippet_slug').keypress(function() { $(this).addClass('filled'); });
$('a#image-picker-link').imagepicker({
insertFn: function(link) {
return "{{ theme_images." + link.attr('data-slug') + " }}";
}
});
});

View File

@ -1,38 +1,43 @@
/* ___ file or text ___ */
var enableFileOrTextToggling = function() {
$('div.hidden').hide();
$('span.alt').click(function(event) {
event.preventDefault();
$('div.hidden').hide();
if ($("div#file-selector").is(":hidden")) {
$("div#text-selector").slideUp("normal", function() {
$("div#file-selector").slideDown();
$("input#theme_asset_performing_plain_text").val(false);
});
$('span.alt').click(function(event) {
event.preventDefault();
if ($("div#file-selector").is(":hidden")) {
$("div#text-selector").slideUp("normal", function() {
$("div#file-selector").slideDown();
$("input#theme_asset_performing_plain_text").val(false);
});
} else {
$("div#file-selector").slideUp("normal", function() {
$("div#text-selector").slideDown();
$("input#theme_asset_performing_plain_text").val(true);
});
$("div#file-selector").slideUp("normal", function() {
$("div#text-selector").slideDown();
$("input#theme_asset_performing_plain_text").val(true);
});
}
});
});
}
$(document).ready(function() {
enableFileOrTextToggling();
$('code.stylesheet textarea').each(function() {
addCodeMirrorEditor(null, $(this), ["tokenizejavascript.js", "parsejavascript.js", "parsecss.js"]);
});
$('code.javascript textarea').each(function() {
addCodeMirrorEditor(null, $(this), ["parsecss.js", "tokenizejavascript.js", "parsejavascript.js"]);
});
$('select#theme_asset_content_type').bind('change', function() {
var editor = CodeMirrorEditors[0].editor;
editor.setParser($(this).val() == 'stylesheet' ? 'CSSParser' : 'JSParser');
});
enableFileOrTextToggling();
$('code.stylesheet textarea').each(function() {
addCodeMirrorEditor(null, $(this), ["tokenizejavascript.js", "parsejavascript.js", "parsecss.js"]);
});
$('code.javascript textarea').each(function() {
addCodeMirrorEditor(null, $(this), ["parsecss.js", "tokenizejavascript.js", "parsejavascript.js"]);
});
$('select#theme_asset_content_type').bind('change', function() {
var editor = CodeMirrorEditors[0].editor;
editor.setParser($(this).val() == 'stylesheet' ? 'CSSParser' : 'JSParser');
});
$('a#image-picker-link').imagepicker({
insertFn: function(link) {
return link.attr('data-shortcut-url');
}
});
});

View File

@ -1,74 +1,74 @@
/* ___ application messages ___ */
div.notice {
background: transparent url(/images/admin/form/growl-notice.png) repeat-x 0 0;
position: relative;
width: 100%;
height: 90px;
background: transparent url(/images/admin/form/growl-notice.png) repeat-x 0 0;
position: relative;
width: 100%;
height: 90px;
}
div.notice.error, div.notice.alert {
background-image: url(/images/admin/form/growl-error.png);
background-image: url(/images/admin/form/growl-error.png);
}
div.notice p {
position: relative;
top: 35px;
margin: 0px;
text-align: center;
font-size: 1.5em;
text-shadow: 1px 1px 1px #333;
color: #fff;
position: relative;
top: 35px;
margin: 0px;
text-align: center;
font-size: 1.5em;
text-shadow: 1px 1px 1px #333;
color: #fff;
}
/* ___ list ___ */
p.no-items {
padding: 15px 0px;
background: transparent url(/images/admin/list/none.png) no-repeat 0 0;
text-align: center;
color: #9d8963 !important;
font-size: 1.1em !important;
padding: 15px 0px;
background: transparent url(/images/admin/list/none.png) no-repeat 0 0;
text-align: center;
color: #9d8963 !important;
font-size: 1.1em !important;
}
p.no-items a {
color: #ff2900;
text-decoration: none;
p.no-items a {
color: #ff2900;
text-decoration: none;
}
p.no-items a:hover {
text-decoration: underline;
text-decoration: underline;
}
ul.list {
list-style: none;
margin: 0px 0 20px 0;
background: white;
list-style: none;
margin: 0px 0 20px 0;
background: white;
}
ul.list li {
height: 31px;
margin-bottom: 10px;
position: relative;
clear: both;
background: transparent url(/images/admin/list/item.png) no-repeat 0 0;
height: 31px;
margin-bottom: 10px;
position: relative;
clear: both;
background: transparent url(/images/admin/list/item.png) no-repeat 0 0;
}
ul.list li em {
display: block;
float: left;
background: transparent url(/images/admin/list/item-left.png) no-repeat left 0;
height: 31px;
width: 18px;
display: block;
float: left;
background: transparent url(/images/admin/list/item-left.png) no-repeat left 0;
height: 31px;
width: 18px;
}
ul.list li strong a {
position: relative;
top: 2px;
left: 15px;
text-decoration: none;
color: #1f82bc;
font-size: 0.9em;
position: relative;
top: 2px;
left: 15px;
text-decoration: none;
color: #1f82bc;
font-size: 0.9em;
}
ul.list.sortable li strong a { left: 10px; }
@ -76,81 +76,81 @@ ul.list.sortable li strong a { left: 10px; }
ul.list li strong a:hover { text-decoration: underline; }
ul.list li div.more {
position: absolute;
top: 3px;
right: 15px;
font-size: 0.7em;
color: #8b8d9a;
position: absolute;
top: 3px;
right: 15px;
font-size: 0.7em;
color: #8b8d9a;
}
ul.list li div.more a {
margin-left: 10px;
position: relative;
top: 4px;
margin-left: 10px;
position: relative;
top: 4px;
}
ul.list li span.handle {
position: relative;
top: 5px;
margin: 0 0 0 15px;
cursor: move;
position: relative;
top: 5px;
margin: 0 0 0 15px;
cursor: move;
}
/* ___ assets ___ */
ul.assets {
list-style: none;
margin: 0px;
padding: 0px;
list-style: none;
margin: 0px;
padding: 0px;
}
ul.assets li.asset {
position: relative;
float: left;
width: 139px;
height: 140px;
background: transparent url(/images/admin/list/thumb.png) no-repeat 0 0;
margin: 0 17px 17px 0;
position: relative;
float: left;
width: 139px;
height: 140px;
background: transparent url(/images/admin/list/thumb.png) no-repeat 0 0;
margin: 0 17px 17px 0;
}
ul.assets li.asset.last {
margin-right: 0px;
margin-right: 0px;
}
ul.assets li.asset h4 { margin: 0px; height: 30px; border-bottom: 1px solid #ccced7; }
ul.assets li.asset h4 { margin: 0px; height: 30px; border-bottom: 1px solid #ccced7; position: relative; }
ul.assets li.asset h4 a {
position: relative;
top: 6px;
left: 12px;
font-weight: bold;
font-size: 0.6em;
color: #1f82bc;
text-decoration: none;
position: absolute;
top: 7px;
left: 12px;
font-weight: bold;
font-size: 0.6em;
color: #1f82bc;
text-decoration: none;
}
ul.assets li.asset h4 a:hover { text-decoration: underline; }
ul.assets li.asset div.image {
width: 80px;
height: 80px;
border: 4px solid #fff;
margin: 10px 0 0 24px;
background: transparent url(/images/admin/list/empty.png) repeat 0 0;
width: 80px;
height: 80px;
border: 4px solid #fff;
margin: 10px 0 0 24px;
background: transparent url(/images/admin/list/empty.png) repeat 0 0;
}
ul.assets li.asset div.image div.inside {
display: table-cell;
vertical-align: middle;
text-align: center;
width: 80px;
height: 80px;
display: table-cell;
vertical-align: middle;
text-align: center;
width: 80px;
height: 80px;
}
ul.assets li.asset div.actions {
position: absolute;
top: 7px;
right: 12px;
position: absolute;
top: 4px;
right: 12px;
}
/* ___ asset collections ___ */
@ -164,73 +164,73 @@ div#uploadAssetsInputQueue { display: none; }
#contents-list li { background: none; }
#contents-list.sortable li em {
background-position: left -31px;
cursor: move;
background-position: left -31px;
cursor: move;
}
#contents-list li strong {
margin-left: 18px;
display: block;
height: 31px;
background: transparent url(/images/admin/list/item-right.png) no-repeat right 0;
margin-left: 18px;
display: block;
height: 31px;
background: transparent url(/images/admin/list/item-right.png) no-repeat right 0;
}
/* ___ pages ___ */
#pages-list {
list-style: none;
margin: 0px 0 20px 0;
background: white;
list-style: none;
margin: 0px 0 20px 0;
background: white;
}
#pages-list ul { list-style: none; margin: 10px 0 10px 40px; padding: 0; }
#pages-list li {
margin-bottom: 10px;
position: relative;
clear: both;
margin-bottom: 10px;
position: relative;
clear: both;
}
#pages-list li em {
display: block;
float: left;
background: transparent url(/images/admin/list/item-left.png) no-repeat left 0;
height: 31px;
width: 18px;
display: block;
float: left;
background: transparent url(/images/admin/list/item-left.png) no-repeat left 0;
height: 31px;
width: 18px;
}
#pages-list ul.folder li em {
background-position: left -31px;
cursor: move;
background-position: left -31px;
cursor: move;
}
#pages-list ul.folder li.templatized em {
background-position: left -62px;
cursor: pointer;
background-position: left -62px;
cursor: pointer;
}
#pages-list li .toggler {
position: absolute;
top: 9px;
left: -15px;
cursor: pointer;
#pages-list li .toggler {
position: absolute;
top: 9px;
left: -15px;
cursor: pointer;
}
#pages-list li strong {
margin-left: 18px;
display: block;
height: 31px;
background: transparent url(/images/admin/list/item-right.png) no-repeat right 0;
margin-left: 18px;
display: block;
height: 31px;
background: transparent url(/images/admin/list/item-right.png) no-repeat right 0;
}
#pages-list li strong a {
position: relative;
top: 3px;
text-decoration: none;
color: #1f82bc;
font-size: 0.9em;
padding-left: 6px;
position: relative;
top: 3px;
text-decoration: none;
color: #1f82bc;
font-size: 0.9em;
padding-left: 6px;
}
#pages-list li strong a:hover { text-decoration: underline; }
@ -238,18 +238,18 @@ div#uploadAssetsInputQueue { display: none; }
#pages-list li.hidden strong a { font-style: italic; font-weight: normal; }
#pages-list li .more {
position: absolute;
top: 6px;
right: 20px;
font-size: 0.7em;
color: #8b8d9a;
position: absolute;
top: 6px;
right: 20px;
font-size: 0.7em;
color: #8b8d9a;
}
#pages-list li .more a {
position: relative;
top: 3px;
margin-left: 10px;
outline: none;
position: relative;
top: 3px;
margin-left: 10px;
outline: none;
}
#pages-list li.not-found { border-top: 1px dotted #bbbbbd; padding-top: 10px; margin-left: 0px; }

View File

@ -64,7 +64,7 @@ describe ThemeAsset do
before(:each) do
ThemeAsset.any_instance.stubs(:site_id).returns('test')
@asset = Factory.build(:theme_asset)
@asset = Factory.build(:theme_asset, :site => Factory.build(:site))
@asset.performing_plain_text = true
@asset.slug = 'a file'
@asset.plain_text = "Lorem ipsum"
@ -84,6 +84,24 @@ describe ThemeAsset do
@asset.source.should_not be_nil
end
context 'shortcut urls' do
before(:each) do
@image = Factory.build(:theme_asset, :source => FixturedAsset.open('5k.png'))
@image.source.stubs(:url).returns('5k.png')
@asset.stubs(:stylesheet?).returns(true)
@asset.site.theme_assets.stubs(:where).returns([@image])
@asset.plain_text = 'body { background-image: url("/theme/images/5k.png"); } h1 { background-image: url("/images/5k.png"); }'
@asset.store_plain_text
end
it 'replaces shortcut url if present' do
@asset.plain_text.should == 'body { background-image: url("/theme/images/5k.png"); } h1 { background-image: url("/images/5k.png"); }'
@asset.source.read.should == 'body { background-image: url("5k.png"); } h1 { background-image: url("/images/5k.png"); }'
end
end
end
end