diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c40133 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.sass-cache/ \ No newline at end of file diff --git a/app/controllers/admin/facets_controller.rb b/app/controllers/admin/facets_controller.rb index 46cff9f..2791b22 100644 --- a/app/controllers/admin/facets_controller.rb +++ b/app/controllers/admin/facets_controller.rb @@ -38,5 +38,11 @@ def destroy @page.destroy end + def tokens + @facets = Facet.with_globalize.where("title like ?", "%#{params[:q]}%") + respond_to do |format| + format.json { render :json => @facets } + end + end end end diff --git a/app/controllers/admin/products_controller.rb b/app/controllers/admin/products_controller.rb index ea74e58..0c71f1a 100644 --- a/app/controllers/admin/products_controller.rb +++ b/app/controllers/admin/products_controller.rb @@ -37,5 +37,12 @@ def destroy @page = Product.find params[:id] @page.destroy end + + def tokens + @products = Product.with_globalize.where("title like ? and parent_id is null", "%#{params[:q]}%") + respond_to do |format| + format.json { render :json => @products } + end + end end end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb new file mode 100644 index 0000000..25d38e2 --- /dev/null +++ b/app/controllers/pages_controller.rb @@ -0,0 +1,43 @@ +class PagesController < ApplicationController + + # This action is usually accessed with the root path, normally '/' + def home + error_404 unless (@page = Page.where(:link_url => '/').first).present? + end + + # This action can be accessed normally, or as nested pages. + # Assuming a page named "mission" that is a child of "about", + # you can access the pages with the following URLs: + # + # GET /pages/about + # GET /about + # + # GET /pages/mission + # GET /about/mission + # + def show + + @page = Page.find("#{params[:path]}/#{params[:id]}".split('/').last) + + if @page.type == "Facet" + @products = @page.products + @facets = @page.children.empty? ? @page.parent.children : @page.children + #@products.map{ |product| product.facets }.flatten.uniq.reject{ |f| facet_ids.include?(f.id) } + # if @facets.empty? + # @products = Sunspot.search(Product){ with(:facet_ids).all_of [facet_ids.first] }.results + # @facets = @products.map{ |product| product.facets }.flatten.uniq + # end + elsif @page.try(:live?) || (refinery_user? && current_user.authorized_plugins.include?("refinery_pages")) + # if the admin wants this to be a "placeholder" page which goes to its first child, go to that instead. + if @page.skip_to_first_child && (first_live_child = @page.children.order('lft ASC').live.first).present? + redirect_to first_live_child.url and return + elsif @page.link_url.present? + redirect_to @page.link_url and return + end + else + error_404 + end + + end + +end diff --git a/app/models/facet.rb b/app/models/facet.rb index 496da5d..3e2c40f 100644 --- a/app/models/facet.rb +++ b/app/models/facet.rb @@ -1,3 +1,16 @@ class Facet < Page has_and_belongs_to_many :products, :join_table => "facets_products", :foreign_key => "facet_id" + attr_accessible :product_tokens, :set + attr_reader :product_tokens + scope :age, where(:set => 'age') + scope :kind, where(:set => 'kind') + scope :interest, where(:set => 'interest') + def product_tokens=(ids) + self.product_ids = ids.split(",") + end + + def as_json(options={}) + { :id => self.id, :name => self.title } + end + end \ No newline at end of file diff --git a/app/models/product.rb b/app/models/product.rb index 432e53b..6dc7a2b 100644 --- a/app/models/product.rb +++ b/app/models/product.rb @@ -1,3 +1,11 @@ class Product < Page has_and_belongs_to_many :facets, :join_table => "facets_products", :foreign_key => "product_id" + attr_accessible :facet_tokens + attr_reader :facet_tokens + def facet_tokens=(ids) + self.facet_ids = ids.split(",") + end + def as_json(options={}) + { :id => self.id, :name => self.title } + end end diff --git a/app/views/admin/pages/_form.html.erb b/app/views/admin/pages/_form.html.erb new file mode 100644 index 0000000..75283e2 --- /dev/null +++ b/app/views/admin/pages/_form.html.erb @@ -0,0 +1,74 @@ +<% content_for :after_javascript_libraries do %> + <%= javascript_include_tag 'jquery.autoresize' %> + <%= javascript_include_tag 'jquery.textchange.min' %> + <%= javascript_include_tag "jquery.tokeninput.js" %> + <%= javascript_include_tag "products-admin" %> +<% end %> +<% content_for :stylesheets do %> + <%= stylesheet_link_tag "token-input" %> + <%= stylesheet_link_tag "catalog" %> +<% end %> + +<% url_opts = action_name == 'edit' ? {:url => admin_page_path(@page.id)} : {} %> +<%= form_for [:admin, @page], url_opts.merge({:as => :page}) do |f| %> + + <%= render :partial => "/shared/admin/error_messages", + :locals => { + :object => @page, + :include_object_name => true + } %> + + <%= render :partial => "locale_picker", + :locals => { + :current_locale => Thread.current[:globalize_locale] + } if ::Refinery.i18n_enabled? %> + +
+ +
+ <%= f.label :title %> + <%= f.text_field :title, :class => "larger widest" %> +
+ + <%= render :partial => "form_fields_after_title", + :locals => { + :f => f + } %> + +
+ <%= render :partial => "form_page_parts", + :locals => { + :f => f + } %> +
+ + <%= render :partial => "form_advanced_options", + :locals => { + :f => f + } %> + + <%= render :partial => "/shared/admin/form_actions", + :locals => { + :f => f, + :continue_editing => true, + :delete_title => t('delete', :scope => 'admin.pages'), + :delete_confirmation => t('message', :scope => 'shared.admin.delete', :title => @page.title) + } %> + + <%= render :partial => "form_new_page_parts", + :locals => { + :f => f + } if RefinerySetting.find_or_set(:new_page_parts, false) %> +<% end %> + +<% content_for :javascripts do %> + +<% end %> \ No newline at end of file diff --git a/app/views/admin/pages/_form_fields_after_title.html.erb b/app/views/admin/pages/_form_fields_after_title.html.erb new file mode 100644 index 0000000..63fa6a2 --- /dev/null +++ b/app/views/admin/pages/_form_fields_after_title.html.erb @@ -0,0 +1,4 @@ +<% if f.object.type == "Facet" and f.object.parent.nil? %> + <%= f.label :set, t('facet_set') %> + <%= f.select :set, options_for_select(RefinerySetting.get(:facet_sets).map{ |set| [t(set), set] }) %> +<% end %> \ No newline at end of file diff --git a/app/views/admin/pages/_page.html.erb b/app/views/admin/pages/_page.html.erb new file mode 100644 index 0000000..b5c5c2b --- /dev/null +++ b/app/views/admin/pages/_page.html.erb @@ -0,0 +1,42 @@ +
  • +
    + <% if page.children.present? %> + + <% else %> + + <% end %> + + <%= page.title_with_meta.html_safe %> + <% if ::Refinery.i18n_enabled? and ::Refinery::I18n.frontend_locales.many? and + (locales = page.translations.map(&:locale)).present? %> + + <% if page.type=="Facet" && page.set.present? %> + <%= t(page.set) %> + <% end %> + <% ([page.translation.try(:locale)] | locales).each do |locale| %> + <%= link_to locale, + edit_admin_page_path(page.id, :switch_locale => locale), + :class => 'locale' %> + <% end %> + + <% end %> + + + <%= link_to refinery_icon_tag('application_go.png'), page.url, + :target => "_blank", + :title => t('.view_live_html') %> + <%= link_to refinery_icon_tag('application_edit.png'), edit_admin_page_path(page.id), + :class => 'edit', + :title => t('edit', :scope => 'admin.pages') %> + <%= link_to refinery_icon_tag('delete.png'), eval("admin_#{page.type.present? ? page.type.tableize : 'page'}_path(page.id)"), + :class => "cancel confirm-delete", + :title => t('delete', :scope => 'admin.pages'), + :confirm => t('message', :scope => 'shared.admin.delete', :title => page.title_with_meta.gsub(/\ ?.*<\/em>/, "")), + :remote => true, + :method => :delete if page.deletable? %> + +
    + +
  • diff --git a/app/views/admin/pages/index.html.erb b/app/views/admin/pages/index.html.erb new file mode 100644 index 0000000..414ee35 --- /dev/null +++ b/app/views/admin/pages/index.html.erb @@ -0,0 +1,15 @@ +<% content_for :stylesheets do %> + <%= stylesheet_link_tag "catalog" %> +<% end %> +
    + <% caching = RefinerySetting.find_or_set(:cache_pages_backend, false) %> + <% cache_if(caching, [Refinery.base_cache_key, "pages_backend", Globalize.locale].join('_')) do %> + <%= render :partial => 'records' %> + <% end %> +
    +
    + <%= render :partial => 'actions' %> +
    +<%= render :partial => "/shared/admin/make_sortable", + :locals => {:tree => true} if @pages.many? -%> + diff --git a/app/views/admin/pages/tabs/_architecture.html.erb b/app/views/admin/pages/tabs/_architecture.html.erb new file mode 100644 index 0000000..be99300 --- /dev/null +++ b/app/views/admin/pages/tabs/_architecture.html.erb @@ -0,0 +1,14 @@ +
    +
    +
    +
    + <% if @page.type == "Product" %> + <%= f.label :facet_tokens, t('.facets') %> + <%= f.text_field :facet_tokens, "data-pre" => @page.facets.map(&:as_json).to_json %> + <% elsif @page.type = "Facet" %> + <%= f.label :product_tokens, t('.products') %> + <%= f.text_field :product_tokens, "data-pre" => @page.products.map(&:as_json).to_json %> + <% end %> +
    + +
    diff --git a/config/locales/en.yml b/config/locales/en.yml index 721368a..095801c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,4 +1,7 @@ en: + age: Age + kind: Kind + interest: Interest shared: admin: image_picker: @@ -12,6 +15,10 @@ en: title: Facets admin: pages: + tabs: + architecture: + facets: Facetas + products: Productos new: title: Título de la página parent_page: Página madre diff --git a/config/locales/es.yml b/config/locales/es.yml index fc71d4f..b9ebeb6 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1,4 +1,7 @@ es: + age: Edad + kind: Tipo + interest: Interés shared: admin: image_picker: @@ -12,6 +15,10 @@ es: title: Facetas admin: pages: + tabs: + architecture: + facets: Facetas + products: Productos new: page_name: Título de la página page_name_help: El título que se mostrará en los menús y encabezados del frontend diff --git a/config/routes.rb b/config/routes.rb index 8fd7431..08e6404 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,9 @@ collection do post :update_positions end + member do + get :children + end end end @@ -14,6 +17,10 @@ resources :products, :except => :show do collection do post :update_positions + get :tokens + end + member do + get :children end end end @@ -23,8 +30,12 @@ resources :facets, :except => :show do collection do post :update_positions + get :tokens + end + member do + get :children end end end - + end diff --git a/db/migrate/4_add_set_to_facets.rb b/db/migrate/4_add_set_to_facets.rb new file mode 100644 index 0000000..ae3f26d --- /dev/null +++ b/db/migrate/4_add_set_to_facets.rb @@ -0,0 +1,11 @@ +class AddSetToFacets < ActiveRecord::Migration + def self.up + add_column :pages, :set, :string + RefinerySetting.set :facet_sets, ['age', 'kind', 'interest'].to_yaml + end + + def self.down + remove_column :pages, :set + RefinerySetting.delete :facet_sets + end +end \ No newline at end of file diff --git a/lib/refinerycms-products.rb b/lib/refinerycms-products.rb index e51bd2f..0d68c32 100644 --- a/lib/refinerycms-products.rb +++ b/lib/refinerycms-products.rb @@ -22,7 +22,15 @@ class Engine < Rails::Engine end end + def self.register(tab) + tab.name = "architecture" + tab.partial = "/admin/pages/tabs/architecture" + end + config.after_initialize do + ::Refinery::Pages::Tab.register do |tab| + register tab + end Refinery::Plugin.register do |plugin| plugin.name = "products" plugin.pathname = root diff --git a/public/javascripts/jquery.tokeninput.js b/public/javascripts/jquery.tokeninput.js new file mode 100755 index 0000000..87641a5 --- /dev/null +++ b/public/javascripts/jquery.tokeninput.js @@ -0,0 +1,860 @@ +/* + * jQuery Plugin: Tokenizing Autocomplete Text Entry + * Version 1.6.0 + * + * Copyright (c) 2009 James Smith (http://loopj.com) + * Licensed jointly under the GPL and MIT licenses, + * choose which one suits your project best! + * + */ + +(function ($) { +// Default settings +var DEFAULT_SETTINGS = { + // Search settings + method: "GET", + contentType: "json", + queryParam: "q", + searchDelay: 300, + minChars: 1, + propertyToSearch: "name", + jsonContainer: null, + + // Display settings + hintText: "Type in a search term", + noResultsText: "No results", + searchingText: "Searching...", + deleteText: "×", + animateDropdown: true, + + // Tokenization settings + tokenLimit: null, + tokenDelimiter: ",", + preventDuplicates: false, + + // Output settings + tokenValue: "id", + + // Prepopulation settings + prePopulate: null, + processPrePopulate: false, + + // Manipulation settings + idPrefix: "token-input-", + + // Formatters + resultsFormatter: function(item){ return "
  • " + item[this.propertyToSearch]+ "
  • " }, + tokenFormatter: function(item) { return "
  • " + item[this.propertyToSearch] + "

  • " }, + + // Callbacks + onResult: null, + onAdd: null, + onDelete: null, + onReady: null +}; + +// Default classes to use when theming +var DEFAULT_CLASSES = { + tokenList: "token-input-list", + token: "token-input-token", + tokenDelete: "token-input-delete-token", + selectedToken: "token-input-selected-token", + highlightedToken: "token-input-highlighted-token", + dropdown: "token-input-dropdown", + dropdownItem: "token-input-dropdown-item", + dropdownItem2: "token-input-dropdown-item2", + selectedDropdownItem: "token-input-selected-dropdown-item", + inputToken: "token-input-input-token" +}; + +// Input box position "enum" +var POSITION = { + BEFORE: 0, + AFTER: 1, + END: 2 +}; + +// Keys "enum" +var KEY = { + BACKSPACE: 8, + TAB: 9, + ENTER: 13, + ESCAPE: 27, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + NUMPAD_ENTER: 108, + COMMA: 188 +}; + +// Additional public (exposed) methods +var methods = { + init: function(url_or_data_or_function, options) { + var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); + + return this.each(function () { + $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings)); + }); + }, + clear: function() { + this.data("tokenInputObject").clear(); + return this; + }, + add: function(item) { + this.data("tokenInputObject").add(item); + return this; + }, + remove: function(item) { + this.data("tokenInputObject").remove(item); + return this; + }, + get: function() { + return this.data("tokenInputObject").getTokens(); + } +} + +// Expose the .tokenInput function to jQuery as a plugin +$.fn.tokenInput = function (method) { + // Method calling and initialization logic + if(methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else { + return methods.init.apply(this, arguments); + } +}; + +// TokenList class for each input +$.TokenList = function (input, url_or_data, settings) { + // + // Initialization + // + + // Configure the data source + if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") { + // Set the url to query against + settings.url = url_or_data; + + // If the URL is a function, evaluate it here to do our initalization work + var url = computeURL(); + + // Make a smart guess about cross-domain if it wasn't explicitly specified + if(settings.crossDomain === undefined) { + if(url.indexOf("://") === -1) { + settings.crossDomain = false; + } else { + settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]); + } + } + } else if(typeof(url_or_data) === "object") { + // Set the local data to search through + settings.local_data = url_or_data; + } + + // Build class names + if(settings.classes) { + // Use custom class names + settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes); + } else if(settings.theme) { + // Use theme-suffixed default class names + settings.classes = {}; + $.each(DEFAULT_CLASSES, function(key, value) { + settings.classes[key] = value + "-" + settings.theme; + }); + } else { + settings.classes = DEFAULT_CLASSES; + } + + + // Save the tokens + var saved_tokens = []; + + // Keep track of the number of tokens in the list + var token_count = 0; + + // Basic cache to save on db hits + var cache = new $.TokenList.Cache(); + + // Keep track of the timeout, old vals + var timeout; + var input_val; + + // Create a new text input an attach keyup events + var input_box = $("") + .css({ + outline: "none" + }) + .attr("id", settings.idPrefix + input.id) + .focus(function () { + if (settings.tokenLimit === null || settings.tokenLimit !== token_count) { + show_dropdown_hint(); + } + }) + .blur(function () { + hide_dropdown(); + $(this).val(""); + }) + .bind("keyup keydown blur update", resize_input) + .keydown(function (event) { + var previous_token; + var next_token; + + switch(event.keyCode) { + case KEY.LEFT: + case KEY.RIGHT: + case KEY.UP: + case KEY.DOWN: + if(!$(this).val()) { + previous_token = input_token.prev(); + next_token = input_token.next(); + + if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) { + // Check if there is a previous/next token and it is selected + if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) { + deselect_token($(selected_token), POSITION.BEFORE); + } else { + deselect_token($(selected_token), POSITION.AFTER); + } + } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) { + // We are moving left, select the previous token if it exists + select_token($(previous_token.get(0))); + } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) { + // We are moving right, select the next token if it exists + select_token($(next_token.get(0))); + } + } else { + var dropdown_item = null; + + if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) { + dropdown_item = $(selected_dropdown_item).next(); + } else { + dropdown_item = $(selected_dropdown_item).prev(); + } + + if(dropdown_item.length) { + select_dropdown_item(dropdown_item); + } + return false; + } + break; + + case KEY.BACKSPACE: + previous_token = input_token.prev(); + + if(!$(this).val().length) { + if(selected_token) { + delete_token($(selected_token)); + hidden_input.change(); + } else if(previous_token.length) { + select_token($(previous_token.get(0))); + } + + return false; + } else if($(this).val().length === 1) { + hide_dropdown(); + } else { + // set a timeout just long enough to let this function finish. + setTimeout(function(){do_search();}, 5); + } + break; + + case KEY.TAB: + case KEY.ENTER: + case KEY.NUMPAD_ENTER: + case KEY.COMMA: + if(selected_dropdown_item) { + add_token($(selected_dropdown_item).data("tokeninput")); + hidden_input.change(); + return false; + } + break; + + case KEY.ESCAPE: + hide_dropdown(); + return true; + + default: + if(String.fromCharCode(event.which)) { + // set a timeout just long enough to let this function finish. + setTimeout(function(){do_search();}, 5); + } + break; + } + }); + + // Keep a reference to the original input box + var hidden_input = $(input) + .hide() + .val("") + .focus(function () { + input_box.focus(); + }) + .blur(function () { + input_box.blur(); + }); + + // Keep a reference to the selected token and dropdown item + var selected_token = null; + var selected_token_index = 0; + var selected_dropdown_item = null; + + // The list to store the token items in + var token_list = $("