├── .rspec
├── test_app
├── lib
│ └── tasks
│ │ ├── .gitkeep
│ │ └── cron.rake
├── public
│ ├── favicon.ico
│ ├── robots.txt
│ ├── 422.html
│ ├── 404.html
│ └── 500.html
├── vendor
│ └── plugins
│ │ └── .gitkeep
├── app
│ ├── assets
│ │ ├── stylesheets
│ │ │ ├── .gitkeep
│ │ │ ├── scaffold.css
│ │ │ ├── style.css.erb
│ │ │ └── jquery-ui-1.8.16.custom.css.erb
│ │ ├── images
│ │ │ ├── no.png
│ │ │ ├── yes.png
│ │ │ ├── red_pen.png
│ │ │ ├── ui-icons_222222_256x240.png
│ │ │ ├── ui-icons_228ef1_256x240.png
│ │ │ ├── ui-icons_ef8c08_256x240.png
│ │ │ ├── ui-icons_ffd27a_256x240.png
│ │ │ ├── ui-icons_ffffff_256x240.png
│ │ │ ├── ui-bg_flat_10_000000_40x100.png
│ │ │ ├── ui-bg_glass_100_f6f6f6_1x400.png
│ │ │ ├── ui-bg_glass_100_fdf5ce_1x400.png
│ │ │ ├── ui-bg_glass_65_ffffff_1x400.png
│ │ │ ├── ui-bg_gloss-wave_35_f6a828_500x100.png
│ │ │ ├── ui-bg_highlight-soft_75_ffe45c_1x100.png
│ │ │ ├── ui-bg_diagonals-thick_18_b81900_40x40.png
│ │ │ ├── ui-bg_diagonals-thick_20_666666_40x40.png
│ │ │ └── ui-bg_highlight-soft_100_eeeeee_1x100.png
│ │ └── javascripts
│ │ │ └── application.js
│ ├── helpers
│ │ ├── application_helper.rb
│ │ └── users_helper.rb
│ ├── views
│ │ ├── users
│ │ │ ├── email_field.html.erb
│ │ │ ├── new.html.erb
│ │ │ ├── edit.html.erb
│ │ │ ├── show_ajax.html.erb
│ │ │ ├── index.html.erb
│ │ │ ├── _form.html.erb
│ │ │ ├── double_init.html.erb
│ │ │ └── show.html.erb
│ │ ├── cuca
│ │ │ └── cars
│ │ │ │ └── show.html.erb
│ │ ├── layouts
│ │ │ └── application.html.erb
│ │ └── admin
│ │ │ └── users
│ │ │ └── show.html.erb
│ ├── models
│ │ ├── cuca
│ │ │ └── car.rb
│ │ └── user.rb
│ └── controllers
│ │ ├── application_controller.rb
│ │ ├── admin
│ │ └── users_controller.rb
│ │ ├── cuca
│ │ └── cars_controller.rb
│ │ └── users_controller.rb
├── config
│ ├── initializers
│ │ ├── countries.rb
│ │ ├── default_date_format.rb
│ │ ├── mime_types.rb
│ │ ├── inflections.rb
│ │ ├── backtrace_silencers.rb
│ │ ├── session_store.rb
│ │ └── secret_token.rb
│ ├── environment.rb
│ ├── locales
│ │ └── en.yml
│ ├── boot.rb
│ ├── routes.rb
│ ├── database.yml
│ ├── environments
│ │ ├── development.rb
│ │ ├── test.rb
│ │ └── production.rb
│ └── application.rb
├── test
│ ├── unit
│ │ ├── helpers
│ │ │ └── users_helper_test.rb
│ │ └── user_test.rb
│ ├── performance
│ │ └── browsing_test.rb
│ ├── fixtures
│ │ └── users.yml
│ ├── test_helper.rb
│ └── functional
│ │ └── users_controller_test.rb
├── db
│ ├── migrate
│ │ ├── 20111224181356_add_money_to_user.rb
│ │ ├── 20120620165212_add_height_to_user.rb
│ │ ├── 20111217215935_add_birth_date_to_users.rb
│ │ ├── 20120616170454_add_money_proc_to_users.rb
│ │ ├── 20111210084202_add_favorite_color_to_users.rb
│ │ ├── 20111210084251_add_favorite_books_to_users.rb
│ │ ├── 20120607172609_add_favorite_movie_to_users.rb
│ │ ├── 20130213224102_add_favorite_locale_to_users.rb
│ │ ├── 20120513003308_create_cars.rb
│ │ ├── 20101212170114_add_receive_email_to_user.rb
│ │ ├── 20110115204441_add_description_to_user.rb
│ │ └── 20101206205922_create_users.rb
│ ├── schema.rb
│ └── seeds.rb
├── config.ru
├── doc
│ └── README_FOR_APP
├── Rakefile
├── script
│ └── rails
├── Gemfile
└── README
├── lib
├── best_in_place
│ ├── version.rb
│ ├── railtie.rb
│ ├── engine.rb
│ ├── check_version.rb
│ ├── utils.rb
│ ├── controller_extensions.rb
│ ├── test_helpers.rb
│ ├── display_methods.rb
│ └── helper.rb
├── best_in_place.rb
└── assets
│ └── javascripts
│ ├── best_in_place.purr.js
│ ├── jquery.purr.js
│ └── best_in_place.js
├── .gitignore
├── Rakefile
├── Gemfile
├── .travis.yml
├── spec
├── support
│ └── retry_on_timeout.rb
├── spec_helper.rb
├── integration
│ ├── text_area_spec.rb
│ ├── double_init_spec.rb
│ ├── live_spec.rb
│ └── js_spec.rb
└── helpers
│ └── best_in_place_spec.rb
├── best_in_place.gemspec
├── CHANGELOG.md
└── README.md
/.rspec:
--------------------------------------------------------------------------------
1 | --colour
2 |
--------------------------------------------------------------------------------
/test_app/lib/tasks/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test_app/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test_app/vendor/plugins/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test_app/app/assets/stylesheets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test_app/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
--------------------------------------------------------------------------------
/lib/best_in_place/version.rb:
--------------------------------------------------------------------------------
1 | module BestInPlace
2 | VERSION = "2.1.0"
3 | end
4 |
--------------------------------------------------------------------------------
/test_app/app/views/users/email_field.html.erb:
--------------------------------------------------------------------------------
1 | <%= best_in_place @user, :email %>
2 |
--------------------------------------------------------------------------------
/test_app/app/models/cuca/car.rb:
--------------------------------------------------------------------------------
1 | module Cuca
2 | class Car < ActiveRecord::Base
3 |
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test_app/config/initializers/countries.rb:
--------------------------------------------------------------------------------
1 | COUNTRIES = { 1 => "Spain", 2 => "Italy", 3 => "Germany", 4 => "France" }
2 |
--------------------------------------------------------------------------------
/test_app/app/assets/images/no.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/no.png
--------------------------------------------------------------------------------
/test_app/app/assets/images/yes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/yes.png
--------------------------------------------------------------------------------
/test_app/app/views/users/new.html.erb:
--------------------------------------------------------------------------------
1 |
39 | <%= f.label :country %>
40 | <%=
41 | f.select :country, COUNTRIES.map {|c|
42 | v0 = c[0]
43 | c[0] = c[1]
44 | c[1] = v0
45 | c
46 | }
47 | %>
48 |
49 | <%= f.submit %>
50 |
51 | <% end %>
52 |
--------------------------------------------------------------------------------
/best_in_place.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "best_in_place/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "best_in_place"
7 | s.version = BestInPlace::VERSION
8 | s.platform = Gem::Platform::RUBY
9 | s.authors = ["Bernat Farrero"]
10 | s.email = ["bernat@itnig.net"]
11 | s.homepage = "http://github.com/bernat/best_in_place"
12 | s.summary = %q{It makes any field in place editable by clicking on it, it works for inputs, textareas, select dropdowns and checkboxes}
13 | s.description = %q{BestInPlace is a jQuery script and a Rails 3 helper that provide the method best_in_place to display any object field easily editable for the user by just clicking on it. It supports input data, text data, boolean data and custom dropdown data. It works with RESTful controllers.}
14 |
15 | s.rubyforge_project = "best_in_place"
16 |
17 | s.files = `git ls-files`.split("\n")
18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20 | s.require_paths = ["lib"]
21 |
22 | s.add_dependency "rails", ">= 3.1"
23 | s.add_dependency "jquery-rails"
24 |
25 | s.add_development_dependency "rspec-rails", "~> 2.8.0"
26 | s.add_development_dependency "nokogiri"
27 | s.add_development_dependency "capybara", "~> 1.1.2"
28 | end
29 |
--------------------------------------------------------------------------------
/spec/integration/double_init_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require "spec_helper"
3 |
4 | describe "Double initialization bug", :js => true do
5 | before do
6 | @user = User.new :name => "Lucia",
7 | :last_name => "Napoli",
8 | :email => "lucianapoli@gmail.com",
9 | :height => "5' 5\"",
10 | :address => "Via Roma 99",
11 | :zip => "25123",
12 | :country => "2",
13 | :receive_email => false,
14 | :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem.",
15 | :money => 100
16 | end
17 |
18 | it "should be able to change a boolean value" do
19 | @user.save!
20 | visit double_init_user_path(@user)
21 |
22 | within("#receive_email") do
23 | page.should have_content("No thanks")
24 | end
25 |
26 | bip_bool @user, :receive_email
27 |
28 | visit double_init_user_path(@user)
29 | within("#receive_email") do
30 | page.should have_content("Yes of course")
31 | end
32 |
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/best_in_place/test_helpers.rb:
--------------------------------------------------------------------------------
1 | module BestInPlace
2 | module TestHelpers
3 |
4 | include ActionView::Helpers::JavaScriptHelper
5 |
6 | def bip_area(model, attr, new_value)
7 | id = BestInPlace::Utils.build_best_in_place_id model, attr
8 | page.execute_script <<-JS
9 | jQuery("##{id}").click();
10 | jQuery("##{id} form textarea").val('#{escape_javascript new_value.to_s}');
11 | jQuery("##{id} form textarea").blur();
12 | jQuery("##{id} form textarea").blur();
13 | JS
14 | end
15 |
16 | def bip_text(model, attr, new_value)
17 | id = BestInPlace::Utils.build_best_in_place_id model, attr
18 | page.execute_script <<-JS
19 | jQuery("##{id}").click();
20 | jQuery("##{id} input[name='#{attr}']").val('#{escape_javascript new_value.to_s}');
21 | jQuery("##{id} form").submit();
22 | JS
23 | end
24 |
25 | def bip_bool(model, attr)
26 | id = BestInPlace::Utils.build_best_in_place_id model, attr
27 | page.execute_script("jQuery('##{id}').click();")
28 | end
29 |
30 | def bip_select(model, attr, name)
31 | id = BestInPlace::Utils.build_best_in_place_id model, attr
32 | page.execute_script <<-JS
33 | (function() {
34 | jQuery("##{id}").click();
35 | var opt_value = jQuery("##{id} select option:contains('#{name}')").attr('value');
36 | jQuery("##{id} select option[value='" + opt_value + "']").attr('selected', true);
37 | jQuery("##{id} select").change();
38 | })();
39 | JS
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/test_app/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended to check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(:version => 20130213224102) do
15 |
16 | create_table "cars", :force => true do |t|
17 | t.string "model"
18 | end
19 |
20 | create_table "users", :force => true do |t|
21 | t.string "name"
22 | t.string "last_name"
23 | t.string "address"
24 | t.string "email", :null => false
25 | t.string "zip"
26 | t.string "country"
27 | t.datetime "created_at", :null => false
28 | t.datetime "updated_at", :null => false
29 | t.boolean "receive_email"
30 | t.text "description"
31 | t.string "favorite_color"
32 | t.text "favorite_books"
33 | t.datetime "birth_date"
34 | t.float "money"
35 | t.float "money_proc"
36 | t.string "height"
37 | t.string "favorite_movie"
38 | t.string "favorite_locale"
39 | end
40 |
41 | end
42 |
--------------------------------------------------------------------------------
/test_app/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | BipApp::Application.configure do
2 | # Settings specified here will take precedence over those in config/environment.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Log error messages when you accidentally call methods on nil.
11 | config.whiny_nils = true
12 |
13 | # Show full error reports and disable caching
14 | config.consider_all_requests_local = true
15 | config.action_controller.perform_caching = false
16 |
17 | # Raise exceptions instead of rendering exception templates
18 | config.action_dispatch.show_exceptions = false
19 |
20 | # Disable request forgery protection in test environment
21 | config.action_controller.allow_forgery_protection = false
22 |
23 | # Tell Action Mailer not to deliver emails to the real world.
24 | # The :test delivery method accumulates sent emails in the
25 | # ActionMailer::Base.deliveries array.
26 | config.action_mailer.delivery_method = :test
27 |
28 | # Use SQL instead of Active Record's schema dumper when creating the test database.
29 | # This is necessary if your schema can't be completely dumped by the schema dumper,
30 | # like if you have constraints or database-specific column types
31 | # config.active_record.schema_format = :sql
32 |
33 | # Print deprecation notices to the stderr
34 | config.active_support.deprecation = :stderr
35 | end
36 |
--------------------------------------------------------------------------------
/spec/integration/live_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require "spec_helper"
3 |
4 | describe "Monitor new fields", :js => true do
5 | before do
6 | @user = User.new :name => "Lucia",
7 | :last_name => "Napoli",
8 | :email => "lucianapoli@gmail.com",
9 | :height => "5' 5\"",
10 | :address => "Via Roma 99",
11 | :zip => "25123",
12 | :country => "2",
13 | :receive_email => false,
14 | :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem.",
15 | :money => 100
16 | end
17 |
18 | it "should work when new best_in_place spans are added to the page" do
19 | @user.save!
20 | visit show_ajax_user_path(@user)
21 |
22 | sleep(1) #give time to the ajax request to work
23 |
24 | within("#email") do
25 | page.should have_content("lucianapoli@gmail")
26 | end
27 |
28 | bip_text @user, :email, "new@email.com"
29 |
30 | within("#email") do
31 | page.should have_content("new@email.com")
32 | end
33 |
34 | bip_text @user, :email, "new_two@email.com"
35 |
36 | within("#email") do
37 | page.should have_content("new_two@email.com")
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/best_in_place/display_methods.rb:
--------------------------------------------------------------------------------
1 | module BestInPlace
2 | module DisplayMethods
3 | extend self
4 |
5 | class Renderer < Struct.new(:opts)
6 | def render_json(object)
7 | case opts[:type]
8 | when :model
9 | {:display_as => object.send(opts[:method])}.to_json
10 | when :helper
11 | value = if opts[:helper_options]
12 | BestInPlace::ViewHelpers.send(opts[:method], object.send(opts[:attr]), opts[:helper_options])
13 | else
14 | BestInPlace::ViewHelpers.send(opts[:method], object.send(opts[:attr]))
15 | end
16 | {:display_as => value}.to_json
17 | when :proc
18 | {:display_as => opts[:proc].call(object.send(opts[:attr]))}.to_json
19 | else
20 | {}.to_json
21 | end
22 | end
23 | end
24 |
25 | @@table = Hash.new { |h,k| h[k] = Hash.new(&h.default_proc) }
26 |
27 | def lookup(klass, attr)
28 | foo = @@table[klass.to_s][attr.to_s]
29 | foo == {} ? nil : foo
30 | end
31 |
32 | def add_model_method(klass, attr, display_as)
33 | @@table[klass.to_s][attr.to_s] = Renderer.new :method => display_as.to_sym, :type => :model
34 | end
35 |
36 | def add_helper_method(klass, attr, helper_method, helper_options = nil)
37 | @@table[klass.to_s][attr.to_s] = Renderer.new :method => helper_method.to_sym, :type => :helper, :attr => attr, :helper_options => helper_options
38 | end
39 |
40 | def add_helper_proc(klass, attr, helper_proc)
41 | @@table[klass.to_s][attr.to_s] = Renderer.new :type => :proc, :attr => attr, :proc => helper_proc
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test_app/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | BipApp::Application.configure do
2 | # Settings specified here will take precedence over those in config/environment.rb
3 |
4 | # The production environment is meant for finished, "live" apps.
5 | # Code is not reloaded between requests
6 | config.cache_classes = true
7 |
8 | # Full error reports are disabled and caching is turned on
9 | config.consider_all_requests_local = false
10 | config.action_controller.perform_caching = true
11 |
12 | # Specifies the header that your server uses for sending files
13 | config.action_dispatch.x_sendfile_header = "X-Sendfile"
14 |
15 | # For nginx:
16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
17 |
18 | # If you have no front-end server that supports something like X-Sendfile,
19 | # just comment this out and Rails will serve the files
20 |
21 | # See everything in the log (default is :info)
22 | # config.log_level = :debug
23 |
24 | # Use a different logger for distributed setups
25 | # config.logger = SyslogLogger.new
26 |
27 | # Use a different cache store in production
28 | # config.cache_store = :mem_cache_store
29 |
30 | # Disable Rails's static asset server
31 | # In production, Apache or nginx will already do this
32 | config.serve_static_assets = false
33 |
34 | # Enable serving of images, stylesheets, and javascripts from an asset server
35 | # config.action_controller.asset_host = "http://assets.example.com"
36 |
37 | # Disable delivery errors, bad email addresses will be ignored
38 | # config.action_mailer.raise_delivery_errors = false
39 |
40 | # Enable threaded mode
41 | # config.threadsafe!
42 |
43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
44 | # the I18n.default_locale when a translation can not be found)
45 | config.i18n.fallbacks = true
46 |
47 | # Send deprecation notices to registered listeners
48 | config.active_support.deprecation = :notify
49 | end
50 |
--------------------------------------------------------------------------------
/test_app/app/assets/stylesheets/style.css.erb:
--------------------------------------------------------------------------------
1 | /* ======================================================= */
2 | /* User Account */
3 | /* ======================================================= */
4 | table {
5 | margin-left: 5em;
6 | width: 50em;
7 | border: 1px solid #CCC;
8 | background-color: #f5f5f5;
9 | }
10 | table td:first-child {
11 | width: 12em;
12 | padding: 0.5em;
13 | }
14 | table th {
15 | text-align: left;
16 | }
17 | input {
18 | width: 80%;
19 | }
20 | input[type=submit], input[type=button] {
21 | width: 5em;
22 | }
23 | input[type=checkbox] {
24 | width: 1em;
25 | }
26 | .custom-submit {
27 | color: white;
28 | background-color: black;
29 | }
30 | .custom-cancel {
31 | border: 2px solid red;
32 | font-style: italic;
33 | }
34 | textarea {
35 | max-height:15em;
36 | min-width: 40em;
37 | }
38 | .best_in_place {
39 | padding: .1m;
40 | cursor: hand;
41 | cursor: pointer;
42 | -moz-border-radius: 5px;
43 | -webkit-border-radius: 5px;
44 | -o-border-radius: 5px;
45 | -ms-border-radius: 5px;
46 | -khtml-border-radius: 5px;
47 | border-radius: 5px;
48 | }
49 | .best_in_place:hover, #user_account .do_hover {
50 | padding-right: 1.5em;
51 | background: url(<%= asset_path "red_pen.png" %>) no-repeat right;
52 | background-color: #CCC;
53 | }
54 | .info_edit {
55 | float: right;
56 | cursor: hand;
57 | cursor: pointer;
58 | }
59 |
60 | /* Missatges Flotants */
61 |
62 | .purr {
63 | position: fixed;
64 | width: 324px;
65 | top: 20px;
66 | right: 15px;
67 | padding: 20px;
68 | background-color: #000000;
69 | color: #FFFFFF;
70 | border: 2px solid #AAA;
71 | -moz-border-radius: 10px;
72 | -webkit-border-radius: 10px;
73 | -o-border-radius: 10px;
74 | -ms-border-radius: 10px;
75 | -khtml-border-radius: 10px;
76 | border-radius: 10px;
77 | }
78 | .purr:hover .close {
79 | position: absolute;
80 | top: 5px;
81 | right: 3px;
82 | display: block;
83 | width: 25px;
84 | height: 25px;
85 | text-indent: -9999px;
86 | background: url("/images/close-button.gif") no-repeat;
87 | }
88 |
--------------------------------------------------------------------------------
/test_app/app/views/users/double_init.html.erb:
--------------------------------------------------------------------------------
1 |
This is a page that initializes best in place two times with testing purposes
2 |
3 |
User details
4 | <%= link_to "Go back to USERS", users_path %>
5 |
Click to edit
6 |
7 |
8 | | Name (edit me) |
9 |
10 | <%= best_in_place @user, :name, :type => :input, :activator => "#editme" %>
11 | |
12 |
13 |
14 | | Last Name |
15 |
16 | <%= best_in_place @user, :last_name, :nil => "Nothing to show", :path => test_respond_with_user_path(@user) %>
17 | |
18 |
19 |
20 | | Email |
21 |
22 | <%= best_in_place @user, :email %>
23 | |
24 |
25 |
26 | | Address |
27 |
28 | <%= best_in_place @user, :address %>
29 | |
30 |
31 |
32 | | ZIP |
33 |
34 | <%= best_in_place @user, :zip %>
35 | |
36 |
37 |
38 | | Country |
39 |
40 | <%= best_in_place @user, :country, :type => :select, :collection => @countries %>
41 | |
42 |
43 |
44 | | Receive newsletter? |
45 |
46 | <%= best_in_place @user, :receive_email, :type => :checkbox, :collection => ["No thanks", "Yes of course"] %>
47 | |
48 |
49 |
50 | | User description |
51 |
52 | <%= best_in_place @user, :description, :type => :textarea, :sanitize => false %>
53 | |
54 |
55 |
56 | | Alternative Money |
57 |
58 | <%= best_in_place @user, :money, :display_with => :number_to_currency, :helper_options => {:unit => "€"} %>
59 | |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | <%= javascript_tag do %>
68 | $(document).ready(function() {
69 | /* Activating Best In Place */
70 | jQuery(".best_in_place").best_in_place();
71 | });
72 | <% end %>
73 |
--------------------------------------------------------------------------------
/test_app/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | if defined?(Bundler)
6 | # If you precompile assets before deploying to production, use this line
7 | Bundler.require(*Rails.groups(:assets => %w(development test)))
8 | # If you want your assets lazily compiled in production, use this line
9 | # Bundler.require(:default, :assets, Rails.env)
10 | end
11 |
12 | module BipApp
13 | class Application < Rails::Application
14 | # Settings in config/environments/* take precedence over those specified here.
15 | # Application configuration should go into files in config/initializers
16 | # -- all .rb files in that directory are automatically loaded.
17 |
18 | # Custom directories with classes and modules you want to be autoloadable.
19 | # config.autoload_paths += %W(#{config.root}/extras)
20 |
21 | # Only load the plugins named here, in the order given (default is alphabetical).
22 | # :all can be used as a placeholder for all plugins not explicitly named.
23 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
24 |
25 | # Activate observers that should always be running.
26 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
27 |
28 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
29 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
30 | # config.time_zone = 'Central Time (US & Canada)'
31 |
32 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
33 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
34 | # config.i18n.default_locale = :de
35 |
36 | # JavaScript files you want as :defaults (application.js is always included).
37 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
38 |
39 | # Configure the default encoding used in templates for Ruby 1.9.
40 | config.encoding = "utf-8"
41 |
42 | # Configure sensitive parameters which will be filtered from the log file.
43 | config.filter_parameters += [:password]
44 |
45 | # Enable the asset pipeline
46 | config.assets.enabled = true
47 |
48 | # Version of your assets, change this if you want to expire all your assets
49 | config.assets.version = '1.0'
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/test_app/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | # GET /users
3 | # GET /users.xml
4 | def index
5 | @users = User.all
6 |
7 | respond_to do |format|
8 | format.html # index.html.erb
9 | format.xml { render :xml => @users }
10 | end
11 | end
12 |
13 | # GET /users/1
14 | # GET /users/1.xml
15 | def show
16 | @user = User.find(params[:id])
17 | @countries = COUNTRIES.to_a
18 |
19 | respond_to do |format|
20 | format.html # show.html.erb
21 | format.xml { render :xml => @user }
22 | end
23 | end
24 |
25 | def email_field
26 | @user = User.find(params[:id])
27 | render :action => :email_field, :layout => false
28 | end
29 |
30 | def show_ajax
31 | @user = User.find(params[:id])
32 | @countries = COUNTRIES.to_a
33 | end
34 |
35 | def double_init
36 | @user = User.find(params[:id])
37 | @countries = COUNTRIES.to_a
38 | end
39 |
40 | # GET /users/new
41 | # GET /users/new.xml
42 | def new
43 | @user = User.new
44 |
45 | respond_to do |format|
46 | format.html # new.html.erb
47 | format.xml { render :xml => @user }
48 | end
49 | end
50 |
51 | # GET /users/1/edit
52 | def edit
53 | @user = User.find(params[:id])
54 | end
55 |
56 | # POST /users
57 | # POST /users.xml
58 | def create
59 | @user = User.new(params[:user])
60 |
61 | respond_to do |format|
62 | if @user.save
63 | format.html { redirect_to(@user, :notice => 'User was successfully created.') }
64 | format.xml { render :xml => @user, :status => :created, :location => @user }
65 | else
66 | format.html { render :action => "new" }
67 | format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
68 | end
69 | end
70 | end
71 |
72 | # PUT /users/1
73 | # PUT /users/1.xml
74 | def update
75 | @user = User.find(params[:id])
76 |
77 | respond_to do |format|
78 | if @user.update_attributes(params[:user])
79 | format.html { redirect_to(@user, :notice => 'User was successfully updated.') }
80 | format.json { respond_with_bip(@user) }
81 | else
82 | format.html { render :action => "edit" }
83 | format.json { respond_with_bip(@user) }
84 | end
85 | end
86 | end
87 |
88 | # DELETE /users/1
89 | # DELETE /users/1.xml
90 | def destroy
91 | @user = User.find(params[:id])
92 | @user.destroy
93 |
94 | respond_to do |format|
95 | format.html { redirect_to(users_url) }
96 | format.xml { head :ok }
97 | end
98 | end
99 | end
100 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | #Changelog
2 |
3 | ##Master branch (and part of the Rails 3.0 branch)
4 | - v.0.1.0 Initial commit
5 | - v.0.1.2 Fixing errors in collections (taken value[0] instead of index) and fixing test_app controller responses
6 | - v.0.1.3 Bug in Rails Helper. Key wrongly considered an Integer.
7 | - v.0.1.4 Adding two new parameters for further customization urlObject and nilValue and making input update on blur.
8 | - v.0.1.5 **Attention: this release is not backwards compatible**. Changing params from list to option hash, helper's refactoring,
9 | fixing bug with objects inside namespaces, adding feature for passing an external activator handler as param. Adding feature
10 | of key ESCAPE for destroying changes before they are made permanent (in inputs and textarea).
11 | - v.0.1.6-0.1.7 Avoiding request when the input is not modified and allowing the user to not sanitize input data.
12 | - v.0.1.8 jslint compliant, sanitizing tags in the gem, getting right csrf params, controlling size of textarea (elastic script, for autogrowing textarea)
13 | - v.0.1.9 Adding elastic autogrowing textareas
14 | - v.1.0.0 Setting RSpec and Capybara up, and adding some utilities. Mantaining some HTML attributes. Fix a respond_with bug (thanks, @moabite). Triggering ajax:success when ajax call is complete (thanks, @indrekj). Setting up Travis CI. Updated for Rails 3.1.
15 | - v.1.0.1 Fixing a double initialization bug
16 | - v.1.0.2 New bip_area text helper to work with text areas.
17 | - v.1.0.3 replace apostrophes in collection with corresponding HTML entity,
18 | thanks @taavo. Implemented `:display_as` option and adding
19 | `respond_with_bip` to be used in the controller.
20 | - v.1.0.4 Depend on ActiveModel instead of ActiveRecord (thanks,
21 | @skinnyfit). Added date type (thanks @taavo). Added new feature:
22 | display_with.
23 | - v.1.0.5 Fix a bug involving quotes (thanks @ygoldshtrakh). Minor fixes
24 | by @bfalling. Add object name option (thanks @nicholassm). Check
25 | version of Rails before booting. Minor fixes.
26 | - v.1.0.6 Fix issue with display_with. Update test_app to 3.2.
27 | - v.1.1.0 Changed $ by jQuery for compatibility (thanks @tschmitz), new
28 | events for 'deactivate' (thanks @glebtv), added new 'data' attribute
29 | to BIP's span (thanks @straydogstudio), works with dynamically added
30 | elements to the page (thanks @enriclluelles), added object detection to
31 | the 'path' parameter and some more bugfixes.
32 |
33 | ##Rails 3.0 branch only
34 | - v.0.2.0 Added RSpec and Capybara setup, and some tests. Fix countries map syntax, Allowing href and some other HTML attributes. Adding Travis CI too. Added the best_in_place_if option. Added ajax:success trigger, thanks to @indrekj.
35 | - v.0.2.1 Fixing double initialization bug.
36 | - v.0.2.2 New bip_area text helper.
37 |
--------------------------------------------------------------------------------
/test_app/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | # This file should contain all the record creation needed to seed the database with its default values.
3 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
4 | #
5 | # Examples:
6 | #
7 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }])
8 | # Mayor.create(:name => 'Daley', :city => cities.first)
9 |
10 | User.delete_all
11 |
12 | User.create!(:name => "Lucia", :last_name => "Napoli", :email => "lucianapoli@gmail.com", :address => "Via Roma 99", :zip => "25123", :country => "1", :receive_email => false, :birth_date => Date.today - 21.years, :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem.")
13 | User.create!(:name => "Carmen", :last_name => "Luciago", :email => "carmen@luciago.com", :address => "c/Ambrosio 10", :zip => "21333", :country => "2", :receive_email => true, :birth_date => Date.today - 18.years, :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem.")
14 | User.create!(:name => "Angels", :last_name => "Domènech", :email => "angels@gmail.com", :address => "Avinguda Sant Andreu 1", :zip => "08033", :country => "3", :receive_email => false, :birth_date => Date.today - 65.years, :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem.")
15 | User.create!(:name => "Dominic", :last_name => "Lepoin", :email => "dominiclepoin@gmail.com", :address => "Rue Tour Eiffel 4993", :zip => "11192", :country => "4", :receive_email => true, :birth_date => Date.today - 40.years, :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem.")
16 |
17 |
18 | Cuca::Car.delete_all
19 | Cuca::Car.create! :model => "Ford"
20 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/jquery.purr.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jquery.purr.js
3 | * Copyright (c) 2008 Net Perspective (net-perspective.com)
4 | * Licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php)
5 | *
6 | * @author R.A. Ray
7 | * @projectDescription jQuery plugin for dynamically displaying unobtrusive messages in the browser. Mimics the behavior of the MacOS program "Growl."
8 | * @version 0.1.0
9 | *
10 | * @requires jquery.js (tested with 1.2.6)
11 | *
12 | * @param fadeInSpeed int - Duration of fade in animation in miliseconds
13 | * default: 500
14 | * @param fadeOutSpeed int - Duration of fade out animationin miliseconds
15 | default: 500
16 | * @param removeTimer int - Timeout, in miliseconds, before notice is removed once it is the top non-sticky notice in the list
17 | default: 4000
18 | * @param isSticky bool - Whether the notice should fade out on its own or wait to be manually closed
19 | default: false
20 | * @param usingTransparentPNG bool - Whether or not the notice is using transparent .png images in its styling
21 | default: false
22 | */
23 |
24 | (function(jQuery) {
25 |
26 | jQuery.purr = function(notice, options)
27 | {
28 | // Convert notice to a jQuery object
29 | notice = jQuery(notice);
30 |
31 | // Add a class to denote the notice as not sticky
32 | notice.addClass('purr');
33 |
34 | // Get the container element from the page
35 | var cont = document.getElementById('purr-container');
36 |
37 | // If the container doesn't yet exist, we need to create it
38 | if (!cont)
39 | {
40 | cont = '
';
41 | }
42 |
43 | // Convert cont to a jQuery object
44 | cont = jQuery(cont);
45 |
46 | // Add the container to the page
47 | jQuery('body').append(cont);
48 |
49 | notify();
50 |
51 | function notify ()
52 | {
53 | // Set up the close button
54 | var close = document.createElement('a');
55 | jQuery(close).attr({
56 | className: 'close',
57 | href: '#close'
58 | }).appendTo(notice).click(function() {
59 | removeNotice();
60 | return false;
61 | });
62 |
63 | // If ESC is pressed remove notice
64 | jQuery(document).keyup(function(e) {
65 | if (e.keyCode === 27) {
66 | removeNotice();
67 | }
68 | });
69 |
70 | // Add the notice to the page and keep it hidden initially
71 | notice.appendTo(cont).hide();
72 |
73 | //Fade in the notice we just added
74 | notice.fadeIn(options.fadeInSpeed);
75 |
76 | // Set up the removal interval for the added notice if that notice is not a sticky
77 | if (!options.isSticky)
78 | {
79 | var topSpotInt = setInterval(function() {
80 | // Check to see if our notice is the first non-sticky notice in the list
81 | if (notice.prevAll('.purr').length === 0)
82 | {
83 | // Stop checking once the condition is met
84 | clearInterval(topSpotInt);
85 |
86 | // Call the close action after the timeout set in options
87 | setTimeout(function() {
88 | removeNotice();
89 | }, options.removeTimer);
90 | }
91 | }, 200);
92 | }
93 | }
94 |
95 | function removeNotice()
96 | {
97 | // Fade the object out before reducing its height to produce the sliding effect
98 | notice.animate({ opacity: '0' },
99 | {
100 | duration: options.fadeOutSpeed,
101 | complete: function ()
102 | {
103 | notice.animate({ height: '0px' },
104 | {
105 | duration: options.fadeOutSpeed,
106 | complete: function()
107 | {
108 | notice.remove();
109 | }
110 | }
111 | );
112 | }
113 | }
114 | );
115 | };
116 | };
117 |
118 | jQuery.fn.purr = function(options)
119 | {
120 | options = options || {};
121 | options.fadeInSpeed = options.fadeInSpeed || 500;
122 | options.fadeOutSpeed = options.fadeOutSpeed || 500;
123 | options.removeTimer = options.removeTimer || 4000;
124 | options.isSticky = options.isSticky || false;
125 | options.usingTransparentPNG = options.usingTransparentPNG || false;
126 |
127 | this.each(function()
128 | {
129 | new jQuery.purr( this, options );
130 | }
131 | );
132 |
133 | return this;
134 | };
135 | })( jQuery );
136 |
--------------------------------------------------------------------------------
/test_app/app/views/users/show.html.erb:
--------------------------------------------------------------------------------
1 |
<%= notice %>
2 |
3 |
User details
4 | <%= link_to "Go back to USERS", users_path %>
5 |
Click to edit
6 |
7 |
8 | | Name (edit me) |
9 |
10 | <%= best_in_place @user, :name, :type => :input, :activator => "#editme" %>
11 | |
12 |
13 |
14 | | Last Name |
15 |
16 | <%= best_in_place @user, :last_name, :nil => "Nothing to show" %>
17 | |
18 |
19 |
20 | | Height |
21 |
22 | <%= best_in_place @user, :height, :type => :select, :collection => height_collection.zip(height_collection), :sanitize => false %>
23 | |
24 |
25 |
26 | | Email |
27 |
28 | <%= best_in_place @user, :email %>
29 | |
30 |
31 |
32 | | Birth date |
33 |
34 | <%= best_in_place @user, :birth_date, :type => :date %>
35 | |
36 |
37 |
38 | | Address |
39 |
40 | <%= best_in_place @user, :address, :display_as => :address_format %>
41 | |
42 |
43 |
44 | | ZIP |
45 |
46 | <%= best_in_place @user, :zip %>
47 | |
48 |
49 |
50 | | Country |
51 |
52 | <%= best_in_place @user, :country, :type => :select, :collection => @countries, :inner_class => :some_class %>
53 | |
54 |
55 |
56 | | Receive newsletter? |
57 |
58 | <%= best_in_place @user, :receive_email, :type => :checkbox, :collection => ["No thanks", "Yes of course"] %>
59 | |
60 |
61 |
62 | | Receive newsletter (image)? |
63 |
64 | <%= best_in_place @user, :receive_email, :type => :checkbox, :collection => [image_tag('no.png'), image_tag('yes.png')] %>
65 | |
66 |
67 |
68 | | Favorite color |
69 |
70 | <%- opts = { :ok_button => 'Do it!', :cancel_button => 'Nope', :ok_button_class => 'custom-submit other-custom-submit', :cancel_button_class => 'custom-cancel other-custom-cancel' } %>
71 | <%- opts.delete(:ok_button) if params[:suppress_ok_button] %>
72 | <%- opts[:nil] = "Click to add your favorite color" %>
73 | <%= best_in_place @user, :favorite_color, opts %>
74 | |
75 |
76 |
77 | | Favorite locale |
78 |
79 | <%= best_in_place @user, :favorite_locale, :nil => "N/A" %>
80 | |
81 |
82 |
83 | | Favorite books |
84 |
85 | <%- opts = { :type => :textarea, :ok_button => 'Save', :cancel_button => 'Cancel' } %>
86 | <%- opts.delete(:ok_button) if params[:suppress_ok_button] %>
87 | <%= best_in_place @user, :favorite_books, opts %>
88 | |
89 |
90 |
91 | | User description |
92 |
93 | <%= best_in_place @user, :description, :display_as => :markdown_desc, :type => :textarea, :sanitize => false %>
94 | |
95 |
96 |
97 | | Simple-formatted user description |
98 |
99 | <%= best_in_place @user, :description, :display_with => :simple_format, :type => :textarea %>
100 | |
101 |
102 |
103 | | Money |
104 |
105 | <%= best_in_place @user, :money, :display_with => :number_to_currency %>
106 | |
107 |
108 |
109 | | Money with proc |
110 |
111 | <%= best_in_place @user, :money_proc, :display_with => lambda{ |v| v.blank? ? "No money" : number_to_currency(v) } %>
112 | |
113 |
114 |
115 | | Money with custom helper |
116 |
117 | <%= best_in_place @user, :money_custom, :display_with => lambda { |x| bb(x) } %>
118 | |
119 |
120 |
121 | | Favorite Movie |
122 |
123 | <%- opts = { :ok_button => 'Do it!', :cancel_button => 'Nope', :use_confirm => false } %>
124 | <%- opts.delete(:ok_button) if params[:suppress_ok_button] %>
125 | <%= best_in_place @user, :favorite_movie, opts %>
126 | |
127 |
128 |
129 |
130 |
131 |
Try the features of Best In Place:
132 |
133 | - Try giving wrong email values or too short address, inputs to see server errors.
134 | - Click on newsletter to change a boolean value
135 | - Click on country to change the value in a collection of values
136 | - Use the external handler to change the value of the name
137 | - Try making changes inside inputs or textareas and then press the ESC key to recover the old value
138 |
139 |
More information on github or bernatfarrero.com.
140 |
141 |
142 |
--------------------------------------------------------------------------------
/lib/best_in_place/helper.rb:
--------------------------------------------------------------------------------
1 | module BestInPlace
2 | module BestInPlaceHelpers
3 |
4 | def best_in_place(object, field, opts = {})
5 | if opts[:display_as] && opts[:display_with]
6 | raise ArgumentError, "Can't use both 'display_as' and 'display_with' options at the same time"
7 | end
8 |
9 | if opts[:display_with] && !opts[:display_with].is_a?(Proc) && !ViewHelpers.respond_to?(opts[:display_with])
10 | raise ArgumentError, "Can't find helper #{opts[:display_with]}"
11 | end
12 |
13 | real_object = real_object_for object
14 | opts[:type] ||= :input
15 | opts[:collection] ||= []
16 | field = field.to_s
17 |
18 | display_value = build_value_for(real_object, field, opts)
19 |
20 | collection = nil
21 | value = nil
22 | if opts[:type] == :select && !opts[:collection].blank?
23 | value = real_object.send(field)
24 | display_value = Hash[opts[:collection]].stringify_keys[value.to_s]
25 | collection = opts[:collection].to_json
26 | end
27 | if opts[:type] == :checkbox
28 | value = !!real_object.send(field)
29 | if opts[:collection].blank? || opts[:collection].size != 2
30 | opts[:collection] = ["No", "Yes"]
31 | end
32 | display_value = value ? opts[:collection][1] : opts[:collection][0]
33 | collection = opts[:collection].to_json
34 | end
35 | classes = ["best_in_place"]
36 | unless opts[:classes].nil?
37 | # the next three lines enable this opt to handle both a stings and a arrays
38 | classes << opts[:classes]
39 | classes.flatten!
40 | end
41 |
42 | out = "
"
73 | out << display_value.to_s
74 | else
75 | out << ">#{h(display_value.to_s)}"
76 | end
77 | out << ""
78 | raw out
79 | end
80 |
81 | def best_in_place_if(condition, object, field, opts={})
82 | if condition
83 | best_in_place(object, field, opts)
84 | else
85 | build_value_for real_object_for(object), field, opts
86 | end
87 | end
88 |
89 | private
90 | def build_value_for(object, field, opts)
91 | return "" if object.send(field).blank?
92 |
93 | klass = if object.respond_to?(:id)
94 | "#{object.class}_#{object.id}"
95 | else
96 | object.class.to_s
97 | end
98 |
99 | if opts[:display_as]
100 | BestInPlace::DisplayMethods.add_model_method(klass, field, opts[:display_as])
101 | object.send(opts[:display_as]).to_s
102 |
103 | elsif opts[:display_with].try(:is_a?, Proc)
104 | BestInPlace::DisplayMethods.add_helper_proc(klass, field, opts[:display_with])
105 | opts[:display_with].call(object.send(field))
106 |
107 | elsif opts[:display_with]
108 | BestInPlace::DisplayMethods.add_helper_method(klass, field, opts[:display_with], opts[:helper_options])
109 | if opts[:helper_options]
110 | BestInPlace::ViewHelpers.send(opts[:display_with], object.send(field), opts[:helper_options])
111 | else
112 | BestInPlace::ViewHelpers.send(opts[:display_with], object.send(field))
113 | end
114 |
115 | else
116 | object.send(field).to_s
117 | end
118 | end
119 |
120 | def attribute_escape(data)
121 | return unless data
122 |
123 | data.to_s.
124 | gsub("&", "&").
125 | gsub("'", "'").
126 | gsub(/\r?\n/, "
")
127 | end
128 |
129 | def real_object_for(object)
130 | (object.is_a?(Array) && object.last.class.respond_to?(:model_name)) ? object.last : object
131 | end
132 | end
133 | end
134 |
135 |
--------------------------------------------------------------------------------
/test_app/README:
--------------------------------------------------------------------------------
1 | == Welcome to Rails
2 |
3 | Rails is a web-application framework that includes everything needed to create
4 | database-backed web applications according to the Model-View-Control pattern.
5 |
6 | This pattern splits the view (also called the presentation) into "dumb"
7 | templates that are primarily responsible for inserting pre-built data in between
8 | HTML tags. The model contains the "smart" domain objects (such as Account,
9 | Product, Person, Post) that holds all the business logic and knows how to
10 | persist themselves to a database. The controller handles the incoming requests
11 | (such as Save New Account, Update Product, Show Post) by manipulating the model
12 | and directing data to the view.
13 |
14 | In Rails, the model is handled by what's called an object-relational mapping
15 | layer entitled Active Record. This layer allows you to present the data from
16 | database rows as objects and embellish these data objects with business logic
17 | methods. You can read more about Active Record in
18 | link:files/vendor/rails/activerecord/README.html.
19 |
20 | The controller and view are handled by the Action Pack, which handles both
21 | layers by its two parts: Action View and Action Controller. These two layers
22 | are bundled in a single package due to their heavy interdependence. This is
23 | unlike the relationship between the Active Record and Action Pack that is much
24 | more separate. Each of these packages can be used independently outside of
25 | Rails. You can read more about Action Pack in
26 | link:files/vendor/rails/actionpack/README.html.
27 |
28 |
29 | == Getting Started
30 |
31 | 1. At the command prompt, create a new Rails application:
32 |
rails new myapp (where
myapp is the application name)
33 |
34 | 2. Change directory to
myapp and start the web server:
35 |
cd myapp; rails server (run with --help for options)
36 |
37 | 3. Go to http://localhost:3000/ and you'll see:
38 | "Welcome aboard: You're riding Ruby on Rails!"
39 |
40 | 4. Follow the guidelines to start developing your application. You can find
41 | the following resources handy:
42 |
43 | * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html
44 | * Ruby on Rails Tutorial Book: http://www.railstutorial.org/
45 |
46 |
47 | == Debugging Rails
48 |
49 | Sometimes your application goes wrong. Fortunately there are a lot of tools that
50 | will help you debug it and get it back on the rails.
51 |
52 | First area to check is the application log files. Have "tail -f" commands
53 | running on the server.log and development.log. Rails will automatically display
54 | debugging and runtime information to these files. Debugging info will also be
55 | shown in the browser on requests from 127.0.0.1.
56 |
57 | You can also log your own messages directly into the log file from your code
58 | using the Ruby logger class from inside your controllers. Example:
59 |
60 | class WeblogController < ActionController::Base
61 | def destroy
62 | @weblog = Weblog.find(params[:id])
63 | @weblog.destroy
64 | logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!")
65 | end
66 | end
67 |
68 | The result will be a message in your log file along the lines of:
69 |
70 | Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1!
71 |
72 | More information on how to use the logger is at http://www.ruby-doc.org/core/
73 |
74 | Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are
75 | several books available online as well:
76 |
77 | * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe)
78 | * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide)
79 |
80 | These two books will bring you up to speed on the Ruby language and also on
81 | programming in general.
82 |
83 |
84 | == Debugger
85 |
86 | Debugger support is available through the debugger command when you start your
87 | Mongrel or WEBrick server with --debugger. This means that you can break out of
88 | execution at any point in the code, investigate and change the model, and then,
89 | resume execution! You need to install ruby-debug to run the server in debugging
90 | mode. With gems, use
sudo gem install ruby-debug. Example:
91 |
92 | class WeblogController < ActionController::Base
93 | def index
94 | @posts = Post.find(:all)
95 | debugger
96 | end
97 | end
98 |
99 | So the controller will accept the action, run the first line, then present you
100 | with a IRB prompt in the server window. Here you can do things like:
101 |
102 | >> @posts.inspect
103 | => "[#
nil, "body"=>nil, "id"=>"1"}>,
105 | #"Rails", "body"=>"Only ten..", "id"=>"2"}>]"
107 | >> @posts.first.title = "hello from a debugger"
108 | => "hello from a debugger"
109 |
110 | ...and even better, you can examine how your runtime objects actually work:
111 |
112 | >> f = @posts.first
113 | => #nil, "body"=>nil, "id"=>"1"}>
114 | >> f.
115 | Display all 152 possibilities? (y or n)
116 |
117 | Finally, when you're ready to resume execution, you can enter "cont".
118 |
119 |
120 | == Console
121 |
122 | The console is a Ruby shell, which allows you to interact with your
123 | application's domain model. Here you'll have all parts of the application
124 | configured, just like it is when the application is running. You can inspect
125 | domain models, change values, and save to the database. Starting the script
126 | without arguments will launch it in the development environment.
127 |
128 | To start the console, run rails console from the application
129 | directory.
130 |
131 | Options:
132 |
133 | * Passing the -s, --sandbox argument will rollback any modifications
134 | made to the database.
135 | * Passing an environment name as an argument will load the corresponding
136 | environment. Example: rails console production.
137 |
138 | To reload your controllers and models after launching the console run
139 | reload!
140 |
141 | More information about irb can be found at:
142 | link:http://www.rubycentral.com/pickaxe/irb.html
143 |
144 |
145 | == dbconsole
146 |
147 | You can go to the command line of your database directly through rails
148 | dbconsole. You would be connected to the database with the credentials
149 | defined in database.yml. Starting the script without arguments will connect you
150 | to the development database. Passing an argument will connect you to a different
151 | database, like rails dbconsole production. Currently works for MySQL,
152 | PostgreSQL and SQLite 3.
153 |
154 | == Description of Contents
155 |
156 | The default directory structure of a generated Ruby on Rails application:
157 |
158 | |-- app
159 | | |-- controllers
160 | | |-- helpers
161 | | |-- models
162 | | `-- views
163 | | `-- layouts
164 | |-- config
165 | | |-- environments
166 | | |-- initializers
167 | | `-- locales
168 | |-- db
169 | |-- doc
170 | |-- lib
171 | | `-- tasks
172 | |-- log
173 | |-- public
174 | | |-- images
175 | | |-- javascripts
176 | | `-- stylesheets
177 | |-- script
178 | | `-- performance
179 | |-- test
180 | | |-- fixtures
181 | | |-- functional
182 | | |-- integration
183 | | |-- performance
184 | | `-- unit
185 | |-- tmp
186 | | |-- cache
187 | | |-- pids
188 | | |-- sessions
189 | | `-- sockets
190 | `-- vendor
191 | `-- plugins
192 |
193 | app
194 | Holds all the code that's specific to this particular application.
195 |
196 | app/controllers
197 | Holds controllers that should be named like weblogs_controller.rb for
198 | automated URL mapping. All controllers should descend from
199 | ApplicationController which itself descends from ActionController::Base.
200 |
201 | app/models
202 | Holds models that should be named like post.rb. Models descend from
203 | ActiveRecord::Base by default.
204 |
205 | app/views
206 | Holds the template files for the view that should be named like
207 | weblogs/index.html.erb for the WeblogsController#index action. All views use
208 | eRuby syntax by default.
209 |
210 | app/views/layouts
211 | Holds the template files for layouts to be used with views. This models the
212 | common header/footer method of wrapping views. In your views, define a layout
213 | using the layout :default and create a file named default.html.erb.
214 | Inside default.html.erb, call <% yield %> to render the view using this
215 | layout.
216 |
217 | app/helpers
218 | Holds view helpers that should be named like weblogs_helper.rb. These are
219 | generated for you automatically when using generators for controllers.
220 | Helpers can be used to wrap functionality for your views into methods.
221 |
222 | config
223 | Configuration files for the Rails environment, the routing map, the database,
224 | and other dependencies.
225 |
226 | db
227 | Contains the database schema in schema.rb. db/migrate contains all the
228 | sequence of Migrations for your schema.
229 |
230 | doc
231 | This directory is where your application documentation will be stored when
232 | generated using rake doc:app
233 |
234 | lib
235 | Application specific libraries. Basically, any kind of custom code that
236 | doesn't belong under controllers, models, or helpers. This directory is in
237 | the load path.
238 |
239 | public
240 | The directory available for the web server. Contains subdirectories for
241 | images, stylesheets, and javascripts. Also contains the dispatchers and the
242 | default HTML files. This should be set as the DOCUMENT_ROOT of your web
243 | server.
244 |
245 | script
246 | Helper scripts for automation and generation.
247 |
248 | test
249 | Unit and functional tests along with fixtures. When using the rails generate
250 | command, template test files will be generated for you and placed in this
251 | directory.
252 |
253 | vendor
254 | External libraries that the application depends on. Also includes the plugins
255 | subdirectory. If the app has frozen rails, those gems also go here, under
256 | vendor/rails/. This directory is in the load path.
257 |
--------------------------------------------------------------------------------
/spec/helpers/best_in_place_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require "spec_helper"
3 |
4 | describe BestInPlace::BestInPlaceHelpers do
5 | describe "#best_in_place" do
6 | before do
7 | @user = User.new :name => "Lucia",
8 | :last_name => "Napoli",
9 | :email => "lucianapoli@gmail.com",
10 | :height => "5' 5\"",
11 | :address => "Via Roma 99",
12 | :zip => "25123",
13 | :country => "2",
14 | :receive_email => false,
15 | :birth_date => Time.now.utc.to_date,
16 | :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem.",
17 | :money => 150
18 | end
19 |
20 | it "should generate a proper id for namespaced models" do
21 | @car = Cuca::Car.create :model => "Ford"
22 |
23 | nk = Nokogiri::HTML.parse(helper.best_in_place @car, :model, :path => helper.cuca_cars_path)
24 | span = nk.css("span")
25 | span.attribute("id").value.should == "best_in_place_cuca_car_#{@car.id}_model"
26 | end
27 |
28 | it "should generate a proper span" do
29 | nk = Nokogiri::HTML.parse(helper.best_in_place @user, :name)
30 | span = nk.css("span")
31 | span.should_not be_empty
32 | end
33 |
34 | it "should not allow both display_as and display_with option" do
35 | lambda { helper.best_in_place(@user, :money, :display_with => :number_to_currency, :display_as => :custom) }.should raise_error(ArgumentError)
36 | end
37 |
38 | describe "general properties" do
39 | before do
40 | nk = Nokogiri::HTML.parse(helper.best_in_place @user, :name)
41 | @span = nk.css("span")
42 | end
43 |
44 | context "when it's an ActiveRecord model" do
45 | it "should have a proper id" do
46 | @span.attribute("id").value.should == "best_in_place_user_#{@user.id}_name"
47 | end
48 | end
49 |
50 | context "when it's not an AR model" do
51 | it "shold generate an html id without any id" do
52 | nk = Nokogiri::HTML.parse(helper.best_in_place [1,2,3], :first, :path => @user)
53 | span = nk.css("span")
54 | span.attribute("id").value.should == "best_in_place_array_first"
55 | end
56 | end
57 |
58 | it "should have the best_in_place class" do
59 | @span.attribute("class").value.should == "best_in_place"
60 | end
61 |
62 | it "should have the correct data-attribute" do
63 | @span.attribute("data-attribute").value.should == "name"
64 | end
65 |
66 | it "should have the correct data-object" do
67 | @span.attribute("data-object").value.should == "user"
68 | end
69 |
70 | it "should have no activator by default" do
71 | @span.attribute("data-activator").should be_nil
72 | end
73 |
74 | it "should have no OK button text by default" do
75 | @span.attribute("data-ok-button").should be_nil
76 | end
77 |
78 | it "should have no OK button class by default" do
79 | @span.attribute("data-ok-button-class").should be_nil
80 | end
81 |
82 | it "should have no Cancel button text by default" do
83 | @span.attribute("data-cancel-button").should be_nil
84 | end
85 |
86 | it "should have no Cancel button class by default" do
87 | @span.attribute("data-cancel-button-class").should be_nil
88 | end
89 |
90 | it "should have no Use-Confirmation dialog option by default" do
91 | @span.attribute("data-use-confirm").should be_nil
92 | end
93 |
94 | it "should have no inner_class by default" do
95 | @span.attribute("data-inner-class").should be_nil
96 | end
97 |
98 | describe "url generation" do
99 | it "should have the correct default url" do
100 | @user.save!
101 | nk = Nokogiri::HTML.parse(helper.best_in_place @user, :name)
102 | span = nk.css("span")
103 | span.attribute("data-url").value.should == "/users/#{@user.id}"
104 | end
105 |
106 | it "should use the custom url specified in string format" do
107 | out = helper.best_in_place @user, :name, :path => "/custom/path"
108 | nk = Nokogiri::HTML.parse(out)
109 | span = nk.css("span")
110 | span.attribute("data-url").value.should == "/custom/path"
111 | end
112 |
113 | it "should use the path given in a named_path format" do
114 | out = helper.best_in_place @user, :name, :path => helper.users_path
115 | nk = Nokogiri::HTML.parse(out)
116 | span = nk.css("span")
117 | span.attribute("data-url").value.should == "/users"
118 | end
119 |
120 | it "should use the given path in a hash format" do
121 | out = helper.best_in_place @user, :name, :path => {:controller => :users, :action => :edit, :id => 23}
122 | nk = Nokogiri::HTML.parse(out)
123 | span = nk.css("span")
124 | span.attribute("data-url").value.should == "/users/23/edit"
125 | end
126 | end
127 |
128 | describe "nil option" do
129 | it "should have no nil data by default" do
130 | @span.attribute("data-nil").should be_nil
131 | end
132 |
133 | it "should show '' if the object responds with nil for the passed attribute" do
134 | @user.stub!(:name).and_return(nil)
135 | nk = Nokogiri::HTML.parse(helper.best_in_place @user, :name)
136 | span = nk.css("span")
137 | span.text.should == ""
138 | end
139 |
140 | it "should show '' if the object responds with an empty string for the passed attribute" do
141 | @user.stub!(:name).and_return("")
142 | nk = Nokogiri::HTML.parse(helper.best_in_place @user, :name)
143 | span = nk.css("span")
144 | span.text.should == ""
145 | end
146 | end
147 |
148 | it "should have the given inner_class" do
149 | out = helper.best_in_place @user, :name, :inner_class => "awesome"
150 | nk = Nokogiri::HTML.parse(out)
151 | span = nk.css("span")
152 | span.attribute("data-inner-class").value.should == "awesome"
153 | end
154 |
155 | it "should have the given activator" do
156 | out = helper.best_in_place @user, :name, :activator => "awesome"
157 | nk = Nokogiri::HTML.parse(out)
158 | span = nk.css("span")
159 | span.attribute("data-activator").value.should == "awesome"
160 | end
161 |
162 | it "should have the given OK button text" do
163 | out = helper.best_in_place @user, :name, :ok_button => "okay"
164 | nk = Nokogiri::HTML.parse(out)
165 | span = nk.css("span")
166 | span.attribute("data-ok-button").value.should == "okay"
167 | end
168 |
169 | it "should have the given OK button class" do
170 | out = helper.best_in_place @user, :name, :ok_button => "okay", :ok_button_class => "okay-class"
171 | nk = Nokogiri::HTML.parse(out)
172 | span = nk.css("span")
173 | span.attribute("data-ok-button-class").value.should == "okay-class"
174 | end
175 |
176 | it "should have the given Cancel button text" do
177 | out = helper.best_in_place @user, :name, :cancel_button => "nasty"
178 | nk = Nokogiri::HTML.parse(out)
179 | span = nk.css("span")
180 | span.attribute("data-cancel-button").value.should == "nasty"
181 | end
182 |
183 | it "should have the given Cancel button class" do
184 | out = helper.best_in_place @user, :name, :cancel_button => "nasty", :cancel_button_class => "nasty-class"
185 | nk = Nokogiri::HTML.parse(out)
186 | span = nk.css("span")
187 | span.attribute("data-cancel-button-class").value.should == "nasty-class"
188 | end
189 |
190 | it "should have the given Use-Confirmation dialog option" do
191 | out = helper.best_in_place @user, :name, :use_confirm => "false"
192 | nk = Nokogiri::HTML.parse(out)
193 | span = nk.css("span")
194 | span.attribute("data-use-confirm").value.should == "false"
195 | end
196 |
197 | describe "object_name" do
198 | it "should change the data-object value" do
199 | out = helper.best_in_place @user, :name, :object_name => "my_user"
200 | nk = Nokogiri::HTML.parse(out)
201 | span = nk.css("span")
202 | span.attribute("data-object").value.should == "my_user"
203 | end
204 | end
205 |
206 | it "should have html5 data attributes" do
207 | out = helper.best_in_place @user, :name, :data => { :foo => "awesome", :bar => "nasty" }
208 | nk = Nokogiri::HTML.parse(out)
209 | span = nk.css("span")
210 | span.attribute("data-foo").value.should == "awesome"
211 | span.attribute("data-bar").value.should == "nasty"
212 | end
213 |
214 | describe "display_as" do
215 | it "should render the address with a custom renderer" do
216 | @user.should_receive(:address_format).and_return("the result")
217 | out = helper.best_in_place @user, :address, :display_as => :address_format
218 | nk = Nokogiri::HTML.parse(out)
219 | span = nk.css("span")
220 | span.text.should == "the result"
221 | end
222 | end
223 |
224 | describe "display_with" do
225 | it "should render the money with the given view helper" do
226 | out = helper.best_in_place @user, :money, :display_with => :number_to_currency
227 | nk = Nokogiri::HTML.parse(out)
228 | span = nk.css("span")
229 | span.text.should == "$150.00"
230 | end
231 |
232 | it "accepts a proc" do
233 | out = helper.best_in_place @user, :name, :display_with => Proc.new { |v| v.upcase }
234 | nk = Nokogiri::HTML.parse(out)
235 | span = nk.css("span")
236 | span.text.should == "LUCIA"
237 | end
238 |
239 | it "should raise an error if the given helper can't be found" do
240 | lambda { helper.best_in_place @user, :money, :display_with => :fk_number_to_currency }.should raise_error(ArgumentError)
241 | end
242 |
243 | it "should call the helper method with the given arguments" do
244 | out = helper.best_in_place @user, :money, :display_with => :number_to_currency, :helper_options => {:unit => "º"}
245 | nk = Nokogiri::HTML.parse(out)
246 | span = nk.css("span")
247 | span.text.should == "º150.00"
248 | end
249 | end
250 |
251 | describe "array-like objects" do
252 | it "should work with array-like objects in order to provide support to namespaces" do
253 | nk = Nokogiri::HTML.parse(helper.best_in_place [:admin, @user], :name)
254 | span = nk.css("span")
255 | span.text.should == "Lucia"
256 | end
257 | end
258 | end
259 |
260 | context "with a text field attribute" do
261 | before do
262 | nk = Nokogiri::HTML.parse(helper.best_in_place @user, :name)
263 | @span = nk.css("span")
264 | end
265 |
266 | it "should render the name as text" do
267 | @span.text.should == "Lucia"
268 | end
269 |
270 | it "should have an input data-type" do
271 | @span.attribute("data-type").value.should == "input"
272 | end
273 |
274 | it "should have no data-collection" do
275 | @span.attribute("data-collection").should be_nil
276 | end
277 | end
278 |
279 | context "with a date attribute" do
280 | before do
281 | nk = Nokogiri::HTML.parse(helper.best_in_place @user, :birth_date, :type => :date)
282 | @span = nk.css("span")
283 | end
284 |
285 | it "should render the date as text" do
286 | @span.text.should == @user.birth_date.to_date.to_s
287 | end
288 |
289 | it "should have a date data-type" do
290 | @span.attribute("data-type").value.should == "date"
291 | end
292 |
293 | it "should have no data-collection" do
294 | @span.attribute("data-collection").should be_nil
295 | end
296 | end
297 |
298 | context "with a boolean attribute" do
299 | before do
300 | nk = Nokogiri::HTML.parse(helper.best_in_place @user, :receive_email, :type => :checkbox)
301 | @span = nk.css("span")
302 | end
303 |
304 | it "should have a checkbox data-type" do
305 | @span.attribute("data-type").value.should == "checkbox"
306 | end
307 |
308 | it "should have the default data-collection" do
309 | data = ["No", "Yes"]
310 | @span.attribute("data-collection").value.should == data.to_json
311 | end
312 |
313 | it "should render the current option as No" do
314 | @span.text.should == "No"
315 | end
316 |
317 | describe "custom collection" do
318 | before do
319 | nk = Nokogiri::HTML.parse(helper.best_in_place @user, :receive_email, :type => :checkbox, :collection => ["Nain", "Da"])
320 | @span = nk.css("span")
321 | end
322 |
323 | it "should show the message with the custom values" do
324 | @span.text.should == "Nain"
325 | end
326 |
327 | it "should render the proper data-collection" do
328 | @span.attribute("data-collection").value.should == ["Nain", "Da"].to_json
329 | end
330 | end
331 |
332 | end
333 |
334 | context "with a select attribute" do
335 | before do
336 | @countries = COUNTRIES.to_a
337 | nk = Nokogiri::HTML.parse(helper.best_in_place @user, :country, :type => :select, :collection => @countries)
338 | @span = nk.css("span")
339 | end
340 |
341 | it "should have a select data-type" do
342 | @span.attribute("data-type").value.should == "select"
343 | end
344 |
345 | it "should have a proper data collection" do
346 | @span.attribute("data-collection").value.should == @countries.to_json
347 | end
348 |
349 | it "should show the current country" do
350 | @span.text.should == "Italy"
351 | end
352 |
353 | it "should include the proper data-value" do
354 | @span.attribute("data-value").value.should == "2"
355 | end
356 |
357 | context "with an apostrophe in it" do
358 | before do
359 | @apostrophe_countries = [[1, "Joe's Country"], [2, "Bob's Country"]]
360 | nk = Nokogiri::HTML.parse(helper.best_in_place @user, :country, :type => :select, :collection => @apostrophe_countries)
361 | @span = nk.css("span")
362 | end
363 |
364 | it "should have a proper data collection" do
365 | @span.attribute("data-collection").value.should == @apostrophe_countries.to_json
366 | end
367 | end
368 | end
369 | end
370 |
371 | describe "#best_in_place_if" do
372 | context "when the parameters are valid" do
373 | before do
374 | @user = User.new :name => "Lucia",
375 | :last_name => "Napoli",
376 | :email => "lucianapoli@gmail.com",
377 | :height => "5' 5\"",
378 | :address => "Via Roma 99",
379 | :zip => "25123",
380 | :country => "2",
381 | :receive_email => false,
382 | :birth_date => Time.now.utc.to_date,
383 | :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem.",
384 | :money => 150
385 | @options = {}
386 | end
387 |
388 | context "when the condition is true" do
389 | before {@condition = true}
390 |
391 | it "should work with array-like objects in order to provide support to namespaces" do
392 | nk = Nokogiri::HTML.parse(helper.best_in_place_if @condition, [:admin, @user], :name)
393 | span = nk.css("span")
394 | span.text.should == "Lucia"
395 | end
396 |
397 | context "when the options parameter is left off" do
398 | it "should call best_in_place with the rest of the parameters and empty options" do
399 | helper.should_receive(:best_in_place).with(@user, :name, {})
400 | helper.best_in_place_if @condition, @user, :name
401 | end
402 | end
403 |
404 | context "when the options parameter is included" do
405 | it "should call best_in_place with the rest of the parameters" do
406 | helper.should_receive(:best_in_place).with(@user, :name, @options)
407 | helper.best_in_place_if @condition, @user, :name, @options
408 | end
409 | end
410 | end
411 |
412 | context "when the condition is false" do
413 | before {@condition = false}
414 |
415 | it "should work with array-like objects in order to provide support to namespaces" do
416 | helper.best_in_place_if(@condition, [:admin, @user], :name).should eq "Lucia"
417 | end
418 |
419 | it "should return the value of the field when the options value is left off" do
420 | helper.best_in_place_if(@condition, @user, :name).should eq "Lucia"
421 | end
422 |
423 | it "should return the value of the field when the options value is included" do
424 | helper.best_in_place_if(@condition, @user, :name, @options).should eq "Lucia"
425 | end
426 | end
427 | end
428 | end
429 | end
430 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Best In Place
2 | [](http://travis-ci.org/bernat/best_in_place)
3 | **The Unobtrusive in Place editing solution**
4 |
5 |
6 | ##Description
7 |
8 | **Best in Place** is a jQuery based AJAX Inplace-Editor that takes profit of RESTful server-side controllers to allow users to edit stuff with
9 | no need of forms. If the server have standard defined REST methods, particularly those to UPDATE your objects (HTTP PUT), then by adding the
10 | Javascript file to the application it is making all the fields with the proper defined classes to become user in-place editable.
11 |
12 | The editor works by PUTting the updated value to the server and GETting the updated record afterwards to display the updated value.
13 |
14 | [**SEE DEMO**](http://bipapp.heroku.com/)
15 |
16 | ---
17 |
18 | ##Features
19 |
20 | - Compatible with text **inputs**
21 | - Compatible with **textarea**
22 | - Compatible with **select** dropdown with custom collections
23 | - Compatible with custom boolean values (same usage of **checkboxes**)
24 | - Compatible with **jQuery UI Datepickers**
25 | - Sanitize HTML and trim spaces of user's input on user's choice
26 | - Displays server-side **validation** errors
27 | - Allows external activator
28 | - Allows optional, configurable OK and Cancel buttons for inputs and textareas
29 | - ESC key destroys changes (requires user confirmation)
30 | - Autogrowing textarea
31 | - Helper for generating the best_in_place field only if a condition is satisfied
32 | - Provided test helpers to be used in your integration specs
33 | - Custom display methods using a method from your model or an existing rails
34 | view helper
35 |
36 | ##Usage of Rails 3 Gem
37 |
38 | ###best_in_place
39 | **best_in_place object, field, OPTIONS**
40 |
41 | Params:
42 |
43 | - **object** (Mandatory): The Object parameter represents the object itself you are about to modify
44 | - **field** (Mandatory): The field (passed as symbol) is the attribute of the Object you are going to display/edit.
45 |
46 | Options:
47 |
48 | - **:type** It can be only [:input, :textarea, :select, :checkbox, :date (>= 1.0.4)] or if undefined it defaults to :input.
49 | - **:collection**: In case you are using the :select type then you must specify the collection of values it takes. In case you are
50 | using the :checkbox type you can specify the two values it can take, or otherwise they will default to Yes and No.
51 | - **:path**: URL to which the updating action will be sent. If not defined it defaults to the :object path.
52 | - **:nil**: The nil param defines the content displayed in case no value is defined for that field. It can be something like "click me to edit".
53 | If not defined it will show *"-"*.
54 | - **:activator**: Is the DOM object that can activate the field. If not defined the user will making editable by clicking on it.
55 | - **:ok_button**: (Inputs and textareas only) If set to a string, then an OK button will be shown with the string as its label, replacing save on blur.
56 | - **:ok_button_class**: (Inputs and textareas only) Specifies any extra classes to set on the OK button.
57 | - **:cancel_button**: (Inputs and textareas only) If set to a string, then a Cancel button will be shown with the string as its label.
58 | - **:cancel_button_class**: (Inputs and textareas only) Specifies any extra classes to set on the Cancel button.
59 | - **:sanitize**: True by default. If set to false the input/textarea will accept html tags.
60 | - **:html_attrs**: Hash of html arguments, such as maxlength, default-value etc.
61 | - **:inner_class**: Class that is set to the rendered input.
62 | - **:display_as**: A model method which will be called in order to display
63 | this field.
64 | - **:object_name**: Used for overriding the default params key used for the object (the data-object attribute). Useful for e.g. STI scenarios where best_in_place should post to a common controller for different models.
65 | - **:data**: Hash of custom data attributes to be added to span. Can be used to provide data to the ajax:success callback.
66 | - **:classes**: Additional classes to apply to the best_in_place span. Accepts either a string or Array of strings
67 |
68 | ###best_in_place_if
69 | **best_in_place_if condition, object, field, OPTIONS**
70 |
71 | It allows us to use best_in_place only if the first new parameter, a
72 | condition, is satisfied. Specifically:
73 |
74 | * Will show a normal best_in_place if the condition is satisfied
75 | * Will only show the attribute from the instance if the condition is not satisfied
76 |
77 | Say we have something like
78 |
79 | <%= best_in_place_if condition, @user, :name, :type => :input %>
80 |
81 | In case *condition* is satisfied, the outcome will be just the same as:
82 |
83 | <%= best_in_place @user, :name, :type => :input %>
84 |
85 | Otherwise, we will have the same outcome as:
86 |
87 | <%= @user.name %>
88 |
89 | It is a very useful feature to use with, for example, [Ryan Bates](https://github.com/ryanb)' [CanCan](https://github.com/ryanb/cancan), so we only allow BIP edition if the current user has permission to do it.
90 |
91 | ---
92 |
93 | ##TestApp and examples
94 | A [test_app](https://github.com/bernat/best_in_place/tree/master/test_app) was created, and can be seen in action in a [running demo on heroku](http://bipapp.heroku.com).
95 |
96 | Examples (code in the views):
97 |
98 | ### Input
99 |
100 | <%= best_in_place @user, :name, :type => :input %>
101 |
102 | <%= best_in_place @user, :name, :type => :input, :nil => "Click me to add content!" %>
103 |
104 | ### Textarea
105 |
106 | <%= best_in_place @user, :description, :type => :textarea %>
107 |
108 | <%= best_in_place @user, :favorite_books, :type => :textarea, :ok_button => 'Save', :cancel_button => 'Cancel' %>
109 |
110 | ### Select
111 |
112 | <%= best_in_place @user, :country, :type => :select, :collection => [[1, "Spain"], [2, "Italy"], [3, "Germany"], [4, "France"]] %>
113 |
114 | Of course it can take an instance or global variable for the collection, just remember the structure `[[key, value], [key, value],...]`.
115 | The key can be a string or an integer.
116 |
117 | ### Checkbox
118 |
119 | <%= best_in_place @user, :receive_emails, :type => :checkbox, :collection => ["No, thanks", "Yes, of course!"] %>
120 |
121 | The first value is always the negative boolean value and the second the positive. Structure: `["false value", "true value"]`.
122 | If not defined, it will default to *Yes* and *No* options.
123 |
124 | ### Date
125 |
126 | <%= best_in_place @user, :birth_date, :type => :date %>
127 |
128 | With the :date type the input field will be initialized as a datepicker input.
129 | In order to provide custom options to the datepicker initialization you must
130 | prepare a `$.datepicker.setDefaults` call with the preferences of your choice.
131 |
132 | More information about datepicker and setting defaults can be found
133 | [here](http://docs.jquery.com/UI/Datepicker/$.datepicker.setDefaults)
134 |
135 | ## Controller response with respond_with_bip
136 |
137 | Best in place provides a utility method you should use in your controller in
138 | order to provide the response that is expected by the javascript side, using
139 | the :json format. This is a simple example showing an update action using it:
140 |
141 | def update
142 | @user = User.find params[:id]
143 |
144 | respond_to do |format|
145 | if @user.update_attributes(params[:user])
146 | format.html { redirect_to(@user, :notice => 'User was successfully updated.') }
147 | format.json { respond_with_bip(@user) }
148 | else
149 | format.html { render :action => "edit" }
150 | format.json { respond_with_bip(@user) }
151 | end
152 | end
153 | end
154 |
155 |
156 | ## Custom display methods
157 |
158 | ### Using `display_as`
159 |
160 | As of best in place 1.0.3 you can use custom methods in your model in order to
161 | decide how a certain field has to be displayed. You can write something like:
162 |
163 | = best_in_place @user, :description, :type => :textarea, :display_as => :mk_description
164 |
165 | Then instead of using `@user.description` to show the actual value, best in
166 | place will call `@user.mk_description`. This can be used for any kind of
167 | custom formatting, text with markdown, etc...
168 |
169 | ### Using `display_with`
170 |
171 | In practice the most common situation is when you want to use an existing
172 | helper to render the attribute, like `number_to_currency` or `simple_format`.
173 | As of version 1.0.4 best in place provides this feature using the
174 | `display_with` option. You can use it like this:
175 |
176 | = best_in_place @user, :money, :display_with => :number_to_currency
177 |
178 | If you want to pass further arguments to the helper you can do it providing an
179 | additional `helper_options` hash:
180 |
181 | = best_in_place @user, :money, :display_with => :number_to_currency, :helper_options => {:unit => "€"}
182 |
183 | You can also pass in a proc or lambda like this:
184 |
185 | = best_in_place @post, :body, :display_with => lambda { |v| textilize(v).html_safe }
186 |
187 | ## Ajax success callback
188 |
189 | ### Binding to ajax:success
190 |
191 | The 'ajax:success' event is triggered upon success. Use bind:
192 |
193 | $('.best_in_place').bind("ajax:success", function () {$(this).closest('tr').effect('highlight'); });
194 |
195 | To bind a callback that is specific to a particular field, use the 'classes' option in the helper method and
196 | then bind to that class.
197 |
198 | <%= best_in_place @user, :name, :classes => 'highlight_on_success' %>
199 | <%= best_in_place @user, :mail, :classes => 'bounce_on_success' %>
200 |
201 | $('.highlight_on_success').bind("ajax:success", function(){$(this).closest('tr').effect('highlight'));});
202 | $('.bounce_on_success').bind("ajax:success", function(){$(this).closest('tr').effect('bounce'));});
203 |
204 | ### Providing data to the callback
205 |
206 | Use the :data option to add HTML5 data attributes to the best_in_place span. For example, in your view:
207 |
208 | <%= best_in_place @user, :name, :data => {:user_name => @user.name} %>
209 |
210 | And in your javascript:
211 |
212 | $('.best_in_place').bind("ajax:success", function(){ alert('Name updated for '+$(this).data('userName')); });
213 |
214 | ##Non Active Record environments
215 | We are not planning to support other ORMs apart from Active Record, at least for now. So, you can perfectly consider the following workaround as *the right way* until a specific implementation is done for your ORM.
216 |
217 | Best In Place automatically assumes that Active Record is the ORM you are using. However, this might not be your case, as you might use another ORM (or not ORM at all for that case!). Good news for you: even in such situation Best In Place can be used!
218 |
219 | Let's setup an example so we can illustrate how to use Best In Place too in a non-ORM case. Imagine you have an awesome ice cream shop, and you have a model representing a single type of ice cream. The IceCream model has a name, a description, a... nevermind. The thing is that it also has a stock, which is a combination of flavour and size. A big chocolate ice cream (yummy!), a small paella ice cream (...really?), and so on. Shall we see some code?
220 |
221 | class IceCream < ActiveRecord::Base
222 | serialize :stock, Hash
223 |
224 | # consider the get_stock and set_stock methods are already defined
225 | end
226 |
227 | Imagine we want to have a grid showing all the combinations of flavour and size and, for each combination, an editable stock. Since the stock for a flavour and a size is not a single and complete model attribute, we cannot use Best In Place *directly*. But we can set it up with an easy workaround.
228 |
229 | In the view, we'd do:
230 |
231 | // @ice_cream is already available
232 | - flavours = ... // get them somewhere
233 | - sizes = ... // get them somewhere
234 | %table
235 | %tr
236 | - ([""] + flavours).each do |flavour|
237 | %th= flavour
238 | - sizes.each do |size|
239 | %tr
240 | %th= size
241 | - flavours.each do |flavour|
242 | - v = @ice_cream.get_stock(:flavour => flavour, :size => size)
243 | %td= best_in_place v, :to_i, :type => :input, :path => set_stock_ice_cream_path(:flavour => flavour, :size => size)
244 |
245 | Now we need a route to which send the stock updates:
246 |
247 | TheAwesomeIceCreamShop::Application.routes.draw do
248 | ...
249 |
250 | resources :ice_creams, :only => :none do
251 | member do
252 | put :set_stock
253 | end
254 | end
255 |
256 | ...
257 | end
258 |
259 | And finally we need a controller:
260 |
261 |
262 | class IceCreamsController < ApplicationController::Base
263 | respond_to :html, :json
264 |
265 | ...
266 |
267 | def set_stock
268 | flavour = params[:flavour]
269 | size = params[:size]
270 | new_stock = (params["fixnum"] || {})["to_i"]
271 |
272 | @ice_cream.set_stock(new_stock, { :flavour => flavour, :size => size })
273 | if @ice_cream.save
274 | head :ok
275 | else
276 | render :json => @ice_cream.errors.full_messages, :status => :unprocessable_entity
277 | end
278 | end
279 |
280 | ...
281 |
282 | end
283 |
284 | And this is how it is done!
285 |
286 | ---
287 |
288 | ##Test Helpers
289 | Best In Place has also some helpers that may be very useful for integration testing. Since it might very common to test some views using Best In Place, some helpers are provided to ease it.
290 |
291 | As of now, a total of four helpers are available. There is one for each of the following BIP types: a plain text input, a textarea, a boolean input and a selector. Its function is to simulate the user's action of filling such fields.
292 |
293 | These four helpers are listed below:
294 |
295 | * **bip_area(model, attr, new_value)**
296 | * **bip_text(model, attr, new_value)**
297 | * **bip_bool(model, attr)**
298 | * **bip_select(model, attr, name)**
299 |
300 | The parameters are defined here (some are method-specific):
301 |
302 | * **model**: the model to which this action applies.
303 | * **attr**: the attribute of the model to which this action applies.
304 | * **new_value** (only **bip_area** and **bip_text**): the new value with which to fill the BIP field.
305 | * **name** (only **bip_select**): the name to select from the dropdown selector.
306 |
307 | ---
308 |
309 | ##Installation
310 |
311 | ###Rails 3.1 and higher
312 |
313 | Installing *best_in_place* is very easy and straight-forward, even more
314 | thanks to Rails 3.1. Just begin including the gem in your Gemfile:
315 |
316 | gem "best_in_place"
317 |
318 | After that, specify the use of the jquery and best in place
319 | javascripts in your application.js, and optionally specify jquery-ui if
320 | you want to use jQuery UI datepickers:
321 |
322 | //= require jquery
323 | //= require jquery-ui
324 | //= require best_in_place
325 |
326 | If you want to use jQuery UI datepickers, you should also install and
327 | load your preferred jquery-ui CSS file and associated assets.
328 |
329 | Then, just add a binding to prepare all best in place fields when the document is ready:
330 |
331 | $(document).ready(function() {
332 | /* Activating Best In Place */
333 | jQuery(".best_in_place").best_in_place();
334 | });
335 |
336 | You are done!
337 |
338 | ###Rails 3.0 and lower
339 |
340 | Installing *best_in_place* for Rails 3.0 or below is a little bit
341 | different, since the master branch is specifically updated for Rails
342 | 3.1. But don't be scared, you'll be fine!
343 |
344 | Rails 3.0 support will be held in the 0.2.X versions, but we have planned not to continue developing for this version of Rails. Nevertheless, you can by implementing what you want and sending us a pull request.
345 |
346 | First, add the gem's 0.2 version in the Gemfile:
347 |
348 | gem "best_in_place", "~> 0.2.0"
349 |
350 | After that, install and load all the javascripts from the folder
351 | **/public/javascripts** in your layouts. They have to be in the order:
352 |
353 | * jquery
354 | * **best_in_place**
355 |
356 | You can automatize this installation by doing
357 |
358 | rails g best_in_place:setup
359 |
360 | If you want to use jQuery UI datepickers, you should also install and
361 | load jquery-ui.js as well as your preferred jquery-ui CSS file and
362 | associated assets.
363 |
364 | Finally, as for Rails 3.1, just add a binding to prepare all best in place fields when the document is ready:
365 |
366 | $(document).ready(function() {
367 | /* Activating Best In Place */
368 | jQuery(".best_in_place").best_in_place();
369 | });
370 |
371 | ---
372 |
373 | ## Notification
374 |
375 | Sometimes your in-place updates will fail due to validation or for some other reason. In such case, you'll want to notify the user somehow. **Best in Place** supports doing so through the best_in_place:error event, and has built-in support for notification via jquery.purr, right out of the box.
376 |
377 | To opt into the jquery.purr error notification, just add best_in_place.purr to your javascripts, as described below. If you'd like to develop your own custom form of error notification, you can use best_in_place.purr as an example to guide you.
378 |
379 | ###Rails 3.1 and higher
380 |
381 | It's as simple as adding:
382 |
383 | //= require best_in_place.purr
384 |
385 | ###Rails 3.0 and lower
386 |
387 | You'll have to load the following additional javascripts, in this order, after loading jquery and **best_in_place**:
388 |
389 | * jquery.purr
390 | * **best_in_place.purr**
391 |
392 | ---
393 |
394 | ## Security
395 |
396 | If the script is used with the Rails Gem no html tags will be allowed unless the sanitize option is set to true, in that case only the tags [*b i u s a strong em p h1 h2 h3 h4 h5 ul li ol hr pre span img*] will be allowed. If the script is used without the gem and with frameworks other than Rails, then you should make sure you are providing the csrf authenticity params as meta tags and you should always escape undesired html tags such as script, object and so forth.
397 |
398 |
399 |
400 |
401 | ---
402 |
403 | ##TODO
404 |
405 | - Client Side Validation definitions
406 | - Accepting more than one handler to activate best_in_place fields
407 |
408 | ---
409 |
410 | ## Development
411 |
412 | Fork the project on [github](https://github.com/bernat/best_in_place 'bernat / best_in_place on Github')
413 |
414 | $ git clone <
415 | $ cd best_in_place
416 | $ bundle
417 |
418 | ### Prepare the test app
419 |
420 | $ cd test_app
421 | $ bundle
422 | $ bundle exec rake db:test:prepare
423 | $ cd ..
424 |
425 | ### Run the specs
426 |
427 | $ bundle exec rspec spec/
428 |
429 | ### Bundler / gem troubleshooting
430 |
431 | - make sure you've run the bundle command for both the app and test_app!
432 | - run bundle update < (in the right place) for any gems that are causing issues
433 |
434 | ---
435 |
436 | ##Authors, License and Stuff
437 |
438 | Code by [Bernat Farrero](http://bernatfarrero.com) from [Itnig Web Services](http://itnig.net) (it was based on the [original project](http://github.com/janv/rest_in_place/) of Jan Varwig) and released under [MIT license](http://www.opensource.org/licenses/mit-license.php).
439 |
440 | Many thanks to the contributors: [Roger Campos](http://github.com/rogercampos), [Jack Senechal](https://github.com/jacksenechal) and [Albert Bellonch](https://github.com/albertbellonch).
441 |
--------------------------------------------------------------------------------
/test_app/app/assets/stylesheets/jquery-ui-1.8.16.custom.css.erb:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery UI CSS Framework 1.8.16
3 | *
4 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
5 | * Dual licensed under the MIT or GPL Version 2 licenses.
6 | * http://jquery.org/license
7 | *
8 | * http://docs.jquery.com/UI/Theming/API
9 | */
10 |
11 | /* Layout helpers
12 | ----------------------------------*/
13 | .ui-helper-hidden { display: none; }
14 | .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); }
15 | .ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
16 | .ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
17 | .ui-helper-clearfix { display: inline-block; }
18 | /* required comment for clearfix to work in Opera \*/
19 | * html .ui-helper-clearfix { height:1%; }
20 | .ui-helper-clearfix { display:block; }
21 | /* end clearfix */
22 | .ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
23 |
24 |
25 | /* Interaction Cues
26 | ----------------------------------*/
27 | .ui-state-disabled { cursor: default !important; }
28 |
29 |
30 | /* Icons
31 | ----------------------------------*/
32 |
33 | /* states and images */
34 | .ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
35 |
36 |
37 | /* Misc visuals
38 | ----------------------------------*/
39 |
40 | /* Overlays */
41 | .ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
42 |
43 |
44 | /*
45 | * jQuery UI CSS Framework 1.8.16
46 | *
47 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
48 | * Dual licensed under the MIT or GPL Version 2 licenses.
49 | * http://jquery.org/license
50 | *
51 | * http://docs.jquery.com/UI/Theming/API
52 | *
53 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS,%20Tahoma,%20Verdana,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=12_gloss_wave.png&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=03_highlight_soft.png&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=08_diagonals_thick.png&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=08_diagonals_thick.png&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px
54 | */
55 |
56 |
57 | /* Component containers
58 | ----------------------------------*/
59 | .ui-widget { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; font-size: 1.1em; }
60 | .ui-widget .ui-widget { font-size: 1em; }
61 | .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif; font-size: 1em; }
62 | .ui-widget-content { border: 1px solid #dddddd; background: #eeeeee url(<%= asset_path "ui-bg_highlight-soft_100_eeeeee_1x100.png" %>) 50% top repeat-x; color: #333333; }
63 | .ui-widget-content a { color: #333333; }
64 | .ui-widget-header { border: 1px solid #e78f08; background: #f6a828 url(<%= asset_path "ui-bg_gloss-wave_35_f6a828_500x100.png" %>) 50% 50% repeat-x; color: #ffffff; font-weight: bold; }
65 | .ui-widget-header a { color: #ffffff; }
66 |
67 | /* Interaction states
68 | ----------------------------------*/
69 | .ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #cccccc; background: #f6f6f6 url(<%= asset_path "ui-bg_glass_100_f6f6f6_1x400.png" %>) 50% 50% repeat-x; font-weight: bold; color: #1c94c4; }
70 | .ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #1c94c4; text-decoration: none; }
71 | .ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #fbcb09; background: #fdf5ce url(<%= asset_path "ui-bg_glass_100_fdf5ce_1x400.png" %>) 50% 50% repeat-x; font-weight: bold; color: #c77405; }
72 | .ui-state-hover a, .ui-state-hover a:hover { color: #c77405; text-decoration: none; }
73 | .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #fbd850; background: #ffffff url(<%= asset_path "ui-bg_glass_65_ffffff_1x400.png" %>) 50% 50% repeat-x; font-weight: bold; color: #eb8f00; }
74 | .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #eb8f00; text-decoration: none; }
75 | .ui-widget :active { outline: none; }
76 |
77 | /* Interaction Cues
78 | ----------------------------------*/
79 | .ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fed22f; background: #ffe45c url(<%= asset_path "ui-bg_highlight-soft_75_ffe45c_1x100.png" %>) 50% top repeat-x; color: #363636; }
80 | .ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; }
81 | .ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #b81900 url(<%= asset_path "ui-bg_diagonals-thick_18_b81900_40x40.png" %>) 50% 50% repeat; color: #ffffff; }
82 | .ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; }
83 | .ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; }
84 | .ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
85 | .ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
86 | .ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
87 |
88 | /* Icons
89 | ----------------------------------*/
90 |
91 | /* states and images */
92 | .ui-icon { width: 16px; height: 16px; background-image: url(<%= asset_path "ui-icons_222222_256x240.png" %>); }
93 | .ui-widget-content .ui-icon {background-image: url(<%= asset_path "ui-icons_222222_256x240.png" %>); }
94 | .ui-widget-header .ui-icon {background-image: url(<%= asset_path "ui-icons_ffffff_256x240.png" %>); }
95 | .ui-state-default .ui-icon { background-image: url(<%= asset_path "ui-icons_ef8c08_256x240.png" %>); }
96 | .ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(<%= asset_path "ui-icons_ef8c08_256x240.png" %>); }
97 | .ui-state-active .ui-icon {background-image: url(<%= asset_path "ui-icons_ef8c08_256x240.png" %>); }
98 | .ui-state-highlight .ui-icon {background-image: url(<%= asset_path "ui-icons_228ef1_256x240.png" %>); }
99 | .ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(<%= asset_path "ui-icons_ffd27a_256x240.png" %>); }
100 |
101 | /* positioning */
102 | .ui-icon-carat-1-n { background-position: 0 0; }
103 | .ui-icon-carat-1-ne { background-position: -16px 0; }
104 | .ui-icon-carat-1-e { background-position: -32px 0; }
105 | .ui-icon-carat-1-se { background-position: -48px 0; }
106 | .ui-icon-carat-1-s { background-position: -64px 0; }
107 | .ui-icon-carat-1-sw { background-position: -80px 0; }
108 | .ui-icon-carat-1-w { background-position: -96px 0; }
109 | .ui-icon-carat-1-nw { background-position: -112px 0; }
110 | .ui-icon-carat-2-n-s { background-position: -128px 0; }
111 | .ui-icon-carat-2-e-w { background-position: -144px 0; }
112 | .ui-icon-triangle-1-n { background-position: 0 -16px; }
113 | .ui-icon-triangle-1-ne { background-position: -16px -16px; }
114 | .ui-icon-triangle-1-e { background-position: -32px -16px; }
115 | .ui-icon-triangle-1-se { background-position: -48px -16px; }
116 | .ui-icon-triangle-1-s { background-position: -64px -16px; }
117 | .ui-icon-triangle-1-sw { background-position: -80px -16px; }
118 | .ui-icon-triangle-1-w { background-position: -96px -16px; }
119 | .ui-icon-triangle-1-nw { background-position: -112px -16px; }
120 | .ui-icon-triangle-2-n-s { background-position: -128px -16px; }
121 | .ui-icon-triangle-2-e-w { background-position: -144px -16px; }
122 | .ui-icon-arrow-1-n { background-position: 0 -32px; }
123 | .ui-icon-arrow-1-ne { background-position: -16px -32px; }
124 | .ui-icon-arrow-1-e { background-position: -32px -32px; }
125 | .ui-icon-arrow-1-se { background-position: -48px -32px; }
126 | .ui-icon-arrow-1-s { background-position: -64px -32px; }
127 | .ui-icon-arrow-1-sw { background-position: -80px -32px; }
128 | .ui-icon-arrow-1-w { background-position: -96px -32px; }
129 | .ui-icon-arrow-1-nw { background-position: -112px -32px; }
130 | .ui-icon-arrow-2-n-s { background-position: -128px -32px; }
131 | .ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
132 | .ui-icon-arrow-2-e-w { background-position: -160px -32px; }
133 | .ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
134 | .ui-icon-arrowstop-1-n { background-position: -192px -32px; }
135 | .ui-icon-arrowstop-1-e { background-position: -208px -32px; }
136 | .ui-icon-arrowstop-1-s { background-position: -224px -32px; }
137 | .ui-icon-arrowstop-1-w { background-position: -240px -32px; }
138 | .ui-icon-arrowthick-1-n { background-position: 0 -48px; }
139 | .ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
140 | .ui-icon-arrowthick-1-e { background-position: -32px -48px; }
141 | .ui-icon-arrowthick-1-se { background-position: -48px -48px; }
142 | .ui-icon-arrowthick-1-s { background-position: -64px -48px; }
143 | .ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
144 | .ui-icon-arrowthick-1-w { background-position: -96px -48px; }
145 | .ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
146 | .ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
147 | .ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
148 | .ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
149 | .ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
150 | .ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
151 | .ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
152 | .ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
153 | .ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
154 | .ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
155 | .ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
156 | .ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
157 | .ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
158 | .ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
159 | .ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
160 | .ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
161 | .ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
162 | .ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
163 | .ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
164 | .ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
165 | .ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
166 | .ui-icon-arrow-4 { background-position: 0 -80px; }
167 | .ui-icon-arrow-4-diag { background-position: -16px -80px; }
168 | .ui-icon-extlink { background-position: -32px -80px; }
169 | .ui-icon-newwin { background-position: -48px -80px; }
170 | .ui-icon-refresh { background-position: -64px -80px; }
171 | .ui-icon-shuffle { background-position: -80px -80px; }
172 | .ui-icon-transfer-e-w { background-position: -96px -80px; }
173 | .ui-icon-transferthick-e-w { background-position: -112px -80px; }
174 | .ui-icon-folder-collapsed { background-position: 0 -96px; }
175 | .ui-icon-folder-open { background-position: -16px -96px; }
176 | .ui-icon-document { background-position: -32px -96px; }
177 | .ui-icon-document-b { background-position: -48px -96px; }
178 | .ui-icon-note { background-position: -64px -96px; }
179 | .ui-icon-mail-closed { background-position: -80px -96px; }
180 | .ui-icon-mail-open { background-position: -96px -96px; }
181 | .ui-icon-suitcase { background-position: -112px -96px; }
182 | .ui-icon-comment { background-position: -128px -96px; }
183 | .ui-icon-person { background-position: -144px -96px; }
184 | .ui-icon-print { background-position: -160px -96px; }
185 | .ui-icon-trash { background-position: -176px -96px; }
186 | .ui-icon-locked { background-position: -192px -96px; }
187 | .ui-icon-unlocked { background-position: -208px -96px; }
188 | .ui-icon-bookmark { background-position: -224px -96px; }
189 | .ui-icon-tag { background-position: -240px -96px; }
190 | .ui-icon-home { background-position: 0 -112px; }
191 | .ui-icon-flag { background-position: -16px -112px; }
192 | .ui-icon-calendar { background-position: -32px -112px; }
193 | .ui-icon-cart { background-position: -48px -112px; }
194 | .ui-icon-pencil { background-position: -64px -112px; }
195 | .ui-icon-clock { background-position: -80px -112px; }
196 | .ui-icon-disk { background-position: -96px -112px; }
197 | .ui-icon-calculator { background-position: -112px -112px; }
198 | .ui-icon-zoomin { background-position: -128px -112px; }
199 | .ui-icon-zoomout { background-position: -144px -112px; }
200 | .ui-icon-search { background-position: -160px -112px; }
201 | .ui-icon-wrench { background-position: -176px -112px; }
202 | .ui-icon-gear { background-position: -192px -112px; }
203 | .ui-icon-heart { background-position: -208px -112px; }
204 | .ui-icon-star { background-position: -224px -112px; }
205 | .ui-icon-link { background-position: -240px -112px; }
206 | .ui-icon-cancel { background-position: 0 -128px; }
207 | .ui-icon-plus { background-position: -16px -128px; }
208 | .ui-icon-plusthick { background-position: -32px -128px; }
209 | .ui-icon-minus { background-position: -48px -128px; }
210 | .ui-icon-minusthick { background-position: -64px -128px; }
211 | .ui-icon-close { background-position: -80px -128px; }
212 | .ui-icon-closethick { background-position: -96px -128px; }
213 | .ui-icon-key { background-position: -112px -128px; }
214 | .ui-icon-lightbulb { background-position: -128px -128px; }
215 | .ui-icon-scissors { background-position: -144px -128px; }
216 | .ui-icon-clipboard { background-position: -160px -128px; }
217 | .ui-icon-copy { background-position: -176px -128px; }
218 | .ui-icon-contact { background-position: -192px -128px; }
219 | .ui-icon-image { background-position: -208px -128px; }
220 | .ui-icon-video { background-position: -224px -128px; }
221 | .ui-icon-script { background-position: -240px -128px; }
222 | .ui-icon-alert { background-position: 0 -144px; }
223 | .ui-icon-info { background-position: -16px -144px; }
224 | .ui-icon-notice { background-position: -32px -144px; }
225 | .ui-icon-help { background-position: -48px -144px; }
226 | .ui-icon-check { background-position: -64px -144px; }
227 | .ui-icon-bullet { background-position: -80px -144px; }
228 | .ui-icon-radio-off { background-position: -96px -144px; }
229 | .ui-icon-radio-on { background-position: -112px -144px; }
230 | .ui-icon-pin-w { background-position: -128px -144px; }
231 | .ui-icon-pin-s { background-position: -144px -144px; }
232 | .ui-icon-play { background-position: 0 -160px; }
233 | .ui-icon-pause { background-position: -16px -160px; }
234 | .ui-icon-seek-next { background-position: -32px -160px; }
235 | .ui-icon-seek-prev { background-position: -48px -160px; }
236 | .ui-icon-seek-end { background-position: -64px -160px; }
237 | .ui-icon-seek-start { background-position: -80px -160px; }
238 | /* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
239 | .ui-icon-seek-first { background-position: -80px -160px; }
240 | .ui-icon-stop { background-position: -96px -160px; }
241 | .ui-icon-eject { background-position: -112px -160px; }
242 | .ui-icon-volume-off { background-position: -128px -160px; }
243 | .ui-icon-volume-on { background-position: -144px -160px; }
244 | .ui-icon-power { background-position: 0 -176px; }
245 | .ui-icon-signal-diag { background-position: -16px -176px; }
246 | .ui-icon-signal { background-position: -32px -176px; }
247 | .ui-icon-battery-0 { background-position: -48px -176px; }
248 | .ui-icon-battery-1 { background-position: -64px -176px; }
249 | .ui-icon-battery-2 { background-position: -80px -176px; }
250 | .ui-icon-battery-3 { background-position: -96px -176px; }
251 | .ui-icon-circle-plus { background-position: 0 -192px; }
252 | .ui-icon-circle-minus { background-position: -16px -192px; }
253 | .ui-icon-circle-close { background-position: -32px -192px; }
254 | .ui-icon-circle-triangle-e { background-position: -48px -192px; }
255 | .ui-icon-circle-triangle-s { background-position: -64px -192px; }
256 | .ui-icon-circle-triangle-w { background-position: -80px -192px; }
257 | .ui-icon-circle-triangle-n { background-position: -96px -192px; }
258 | .ui-icon-circle-arrow-e { background-position: -112px -192px; }
259 | .ui-icon-circle-arrow-s { background-position: -128px -192px; }
260 | .ui-icon-circle-arrow-w { background-position: -144px -192px; }
261 | .ui-icon-circle-arrow-n { background-position: -160px -192px; }
262 | .ui-icon-circle-zoomin { background-position: -176px -192px; }
263 | .ui-icon-circle-zoomout { background-position: -192px -192px; }
264 | .ui-icon-circle-check { background-position: -208px -192px; }
265 | .ui-icon-circlesmall-plus { background-position: 0 -208px; }
266 | .ui-icon-circlesmall-minus { background-position: -16px -208px; }
267 | .ui-icon-circlesmall-close { background-position: -32px -208px; }
268 | .ui-icon-squaresmall-plus { background-position: -48px -208px; }
269 | .ui-icon-squaresmall-minus { background-position: -64px -208px; }
270 | .ui-icon-squaresmall-close { background-position: -80px -208px; }
271 | .ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
272 | .ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
273 | .ui-icon-grip-solid-vertical { background-position: -32px -224px; }
274 | .ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
275 | .ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
276 | .ui-icon-grip-diagonal-se { background-position: -80px -224px; }
277 |
278 |
279 | /* Misc visuals
280 | ----------------------------------*/
281 |
282 | /* Corner radius */
283 | .ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -khtml-border-top-left-radius: 4px; border-top-left-radius: 4px; }
284 | .ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -khtml-border-top-right-radius: 4px; border-top-right-radius: 4px; }
285 | .ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -khtml-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
286 | .ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; -khtml-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
287 |
288 | /* Overlays */
289 | .ui-widget-overlay { background: #666666 url(<%= asset_path "ui-bg_diagonals-thick_20_666666_40x40.png" %>) 50% 50% repeat; opacity: .50;filter:Alpha(Opacity=50); }
290 | .ui-widget-shadow { margin: -5px 0 0 -5px; padding: 5px; background: #000000 url(<%= asset_path "ui-bg_flat_10_000000_40x100.png" %>) 50% 50% repeat-x; opacity: .20;filter:Alpha(Opacity=20); -moz-border-radius: 5px; -khtml-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; }/*
291 | * jQuery UI Datepicker 1.8.16
292 | *
293 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
294 | * Dual licensed under the MIT or GPL Version 2 licenses.
295 | * http://jquery.org/license
296 | *
297 | * http://docs.jquery.com/UI/Datepicker#theming
298 | */
299 | .ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
300 | .ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
301 | .ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
302 | .ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
303 | .ui-datepicker .ui-datepicker-prev { left:2px; }
304 | .ui-datepicker .ui-datepicker-next { right:2px; }
305 | .ui-datepicker .ui-datepicker-prev-hover { left:1px; }
306 | .ui-datepicker .ui-datepicker-next-hover { right:1px; }
307 | .ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; }
308 | .ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
309 | .ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
310 | .ui-datepicker select.ui-datepicker-month-year {width: 100%;}
311 | .ui-datepicker select.ui-datepicker-month,
312 | .ui-datepicker select.ui-datepicker-year { width: 49%;}
313 | .ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
314 | .ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; }
315 | .ui-datepicker td { border: 0; padding: 1px; }
316 | .ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
317 | .ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
318 | .ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
319 | .ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
320 |
321 | /* with multiple calendars */
322 | .ui-datepicker.ui-datepicker-multi { width:auto; }
323 | .ui-datepicker-multi .ui-datepicker-group { float:left; }
324 | .ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
325 | .ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
326 | .ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
327 | .ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
328 | .ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
329 | .ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
330 | .ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
331 | .ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
332 |
333 | /* RTL support */
334 | .ui-datepicker-rtl { direction: rtl; }
335 | .ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
336 | .ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
337 | .ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
338 | .ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
339 | .ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
340 | .ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
341 | .ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
342 | .ui-datepicker-rtl .ui-datepicker-group { float:right; }
343 | .ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
344 | .ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
345 |
346 | /* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
347 | .ui-datepicker-cover {
348 | display: none; /*sorry for IE5*/
349 | display/**/: block; /*sorry for IE5*/
350 | position: absolute; /*must have*/
351 | z-index: -1; /*must have*/
352 | filter: mask(); /*must have*/
353 | top: -4px; /*must have*/
354 | left: -4px; /*must have*/
355 | width: 200px; /*must have*/
356 | height: 200px; /*must have*/
357 | }
358 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/best_in_place.js:
--------------------------------------------------------------------------------
1 | /*
2 | BestInPlace (for jQuery)
3 | version: 0.1.0 (01/01/2011)
4 | @requires jQuery >= v1.4
5 | @requires jQuery.purr to display pop-up windows
6 |
7 | By Bernat Farrero based on the work of Jan Varwig.
8 | Examples at http://bernatfarrero.com
9 |
10 | Licensed under the MIT:
11 | http://www.opensource.org/licenses/mit-license.php
12 |
13 | Usage:
14 |
15 | Attention.
16 | The format of the JSON object given to the select inputs is the following:
17 | [["key", "value"],["key", "value"]]
18 | The format of the JSON object given to the checkbox inputs is the following:
19 | ["falseValue", "trueValue"]
20 | */
21 |
22 | function BestInPlaceEditor(e) {
23 | this.element = e;
24 | this.initOptions();
25 | this.bindForm();
26 | this.initNil();
27 | if(this.dblClick) {
28 | jQuery(this.activator).bind('dblclick', {editor: this}, this.clickHandler);
29 | } else {
30 | jQuery(this.activator).bind('click', {editor: this}, this.clickHandler);
31 | }
32 | }
33 |
34 | BestInPlaceEditor.prototype = {
35 | // Public Interface Functions //////////////////////////////////////////////
36 |
37 | activate : function() {
38 | var to_display = "";
39 | if (this.isNil()) {
40 | to_display = "";
41 | }
42 | else if (this.original_content) {
43 | to_display = this.original_content;
44 | }
45 | else {
46 | if (this.sanitize) {
47 | to_display = this.element.text();
48 | } else {
49 | to_display = this.element.html().replace('&', '&');
50 | }
51 | }
52 |
53 | this.oldValue = this.isNil() ? "" : this.element.html();
54 | this.display_value = to_display;
55 | if(this.dblClick) {
56 | jQuery(this.activator).unbind("dblclick", this.clickHandler);
57 | } else {
58 | jQuery(this.activator).unbind("click", this.clickHandler);
59 | }
60 | this.activateForm();
61 | this.element.trigger(jQuery.Event("best_in_place:activate"));
62 | },
63 |
64 | abort : function() {
65 | this.activateText(this.oldValue);
66 | if(this.dblClick) {
67 | jQuery(this.activator).bind('dblclick', {editor: this}, this.clickHandler);
68 | } else {
69 | jQuery(this.activator).bind('click', {editor: this}, this.clickHandler);
70 | }
71 | this.element.trigger(jQuery.Event("best_in_place:abort"));
72 | this.element.trigger(jQuery.Event("best_in_place:deactivate"));
73 | },
74 |
75 | abortIfConfirm : function () {
76 | if (!this.useConfirm) {
77 | this.abort();
78 | return;
79 | }
80 |
81 | if (confirm("Are you sure you want to discard your changes?")) {
82 | this.abort();
83 | }
84 | },
85 |
86 | update : function() {
87 | var editor = this;
88 | if (this.formType in {"input":1, "textarea":1} && this.getValue() == this.oldValue)
89 | { // Avoid request if no change is made
90 | this.abort();
91 | return true;
92 | }
93 | editor.ajax({
94 | "type" : "post",
95 | "dataType" : "text",
96 | "data" : editor.requestData(),
97 | "success" : function(data){ editor.loadSuccessCallback(data); },
98 | "error" : function(request, error){ editor.loadErrorCallback(request, error); }
99 | });
100 | if (this.formType == "select") {
101 | var value = this.getValue();
102 | this.previousCollectionValue = value;
103 |
104 | jQuery.each(this.values, function(i, v) {
105 | if (value == v[0]) {
106 | editor.element.html(v[1]);
107 | }
108 | }
109 | );
110 | } else if (this.formType == "checkbox") {
111 | editor.element.html(this.getValue() ? this.values[1] : this.values[0]);
112 | } else {
113 | if (this.getValue() !== "") {
114 | editor.element.text(this.getValue());
115 | } else {
116 | editor.element.html(this.nil);
117 | }
118 | }
119 | editor.element.trigger(jQuery.Event("best_in_place:update"));
120 | },
121 |
122 | activateForm : function() {
123 | alert("The form was not properly initialized. activateForm is unbound");
124 | },
125 |
126 | activateText : function(value){
127 | this.element.html(value);
128 | if(this.isNil()) this.element.html(this.nil);
129 | },
130 |
131 | // Helper Functions ////////////////////////////////////////////////////////
132 |
133 | initOptions : function() {
134 | // Try parent supplied info
135 | var self = this;
136 | self.element.parents().each(function(){
137 | $parent = jQuery(this);
138 | self.url = self.url || $parent.attr("data-url");
139 | self.collection = self.collection || $parent.attr("data-collection");
140 | self.formType = self.formType || $parent.attr("data-type");
141 | self.objectName = self.objectName || $parent.attr("data-object");
142 | self.attributeName = self.attributeName || $parent.attr("data-attribute");
143 | self.activator = self.activator || $parent.attr("data-activator");
144 | self.okButton = self.okButton || $parent.attr("data-ok-button");
145 | self.dblClick = self.dblClick || $parent.attr("data-dbl-click");
146 | self.okButtonClass = self.okButtonClass || $parent.attr("data-ok-button-class");
147 | self.cancelButton = self.cancelButton || $parent.attr("data-cancel-button");
148 | self.cancelButtonClass = self.cancelButtonClass || $parent.attr("data-cancel-button-class");
149 | self.nil = self.nil || $parent.attr("data-nil");
150 | self.inner_id = self.inner_id || $parent.attr("data-inner-id");
151 | self.inner_class = self.inner_class || $parent.attr("data-inner-class");
152 | self.html_attrs = self.html_attrs || $parent.attr("data-html-attrs");
153 | self.original_content = self.original_content || $parent.attr("data-original-content");
154 | self.collectionValue = self.collectionValue || $parent.attr("data-value");
155 | });
156 |
157 | // Try Rails-id based if parents did not explicitly supply something
158 | self.element.parents().each(function(){
159 | var res = this.id.match(/^(\w+)_(\d+)$/i);
160 | if (res) {
161 | self.objectName = self.objectName || res[1];
162 | }
163 | });
164 |
165 | // Load own attributes (overrides all others)
166 | self.url = self.element.attr("data-url") || self.url || document.location.pathname;
167 | self.collection = self.element.attr("data-collection") || self.collection;
168 | self.formType = self.element.attr("data-type") || self.formtype || "input";
169 | self.objectName = self.element.attr("data-object") || self.objectName;
170 | self.attributeName = self.element.attr("data-attribute") || self.attributeName;
171 | self.activator = self.element.attr("data-activator") || self.element;
172 | self.okButton = self.element.attr("data-ok-button") || self.okButton;
173 | self.dblClick = self.element.attr("data-dbl-click") || self.dblClick;
174 | self.okButtonClass = self.element.attr("data-ok-button-class") || self.okButtonClass || "";
175 | self.cancelButton = self.element.attr("data-cancel-button") || self.cancelButton;
176 | self.cancelButtonClass = self.element.attr("data-cancel-button-class") || self.cancelButtonClass || "";
177 | self.nil = self.element.attr("data-nil") || self.nil || "—";
178 | self.inner_class = self.element.attr("data-inner-class") || self.inner_class || null;
179 | self.inner_id = self.element.attr("data-inner-id") || self.inner_id || null;
180 | self.html_attrs = self.element.attr("data-html-attrs") || self.html_attrs;
181 | self.original_content = self.element.attr("data-original-content") || self.original_content;
182 | self.collectionValue = self.element.attr("data-value") || self.collectionValue;
183 |
184 | if (!self.element.attr("data-sanitize")) {
185 | self.sanitize = true;
186 | }
187 | else {
188 | self.sanitize = (self.element.attr("data-sanitize") == "true");
189 | }
190 |
191 | if (!self.element.attr("data-use-confirm")) {
192 | self.useConfirm = true;
193 | } else {
194 | self.useConfirm = (self.element.attr("data-use-confirm") != "false");
195 | }
196 |
197 | if ((self.formType == "select" || self.formType == "checkbox") && self.collection !== null)
198 | {
199 | self.values = jQuery.parseJSON(self.collection);
200 | }
201 |
202 | },
203 |
204 | bindForm : function() {
205 | this.activateForm = BestInPlaceEditor.forms[this.formType].activateForm;
206 | this.getValue = BestInPlaceEditor.forms[this.formType].getValue;
207 | },
208 |
209 | initNil: function() {
210 | if (this.element.html() === "")
211 | {
212 | this.element.html(this.nil);
213 | }
214 | },
215 |
216 | isNil: function() {
217 | // TODO: It only work when form is deactivated.
218 | // Condition will fail when form is activated
219 | return this.element.html() === "" || this.element.html() === this.nil;
220 | },
221 |
222 | getValue : function() {
223 | alert("The form was not properly initialized. getValue is unbound");
224 | },
225 |
226 | // Trim and Strips HTML from text
227 | sanitizeValue : function(s) {
228 | return jQuery.trim(s);
229 | },
230 |
231 | /* Generate the data sent in the POST request */
232 | requestData : function() {
233 | // To prevent xss attacks, a csrf token must be defined as a meta attribute
234 | csrf_token = jQuery('meta[name=csrf-token]').attr('content');
235 | csrf_param = jQuery('meta[name=csrf-param]').attr('content');
236 |
237 | var data = "_method=put";
238 | data += "&" + this.objectName + '[' + this.attributeName + ']=' + encodeURIComponent(this.getValue());
239 |
240 | if (csrf_param !== undefined && csrf_token !== undefined) {
241 | data += "&" + csrf_param + "=" + encodeURIComponent(csrf_token);
242 | }
243 | return data;
244 | },
245 |
246 | ajax : function(options) {
247 | options.url = this.url;
248 | options.beforeSend = function(xhr){ xhr.setRequestHeader("Accept", "application/json"); };
249 | return jQuery.ajax(options);
250 | },
251 |
252 | // Handlers ////////////////////////////////////////////////////////////////
253 |
254 | loadSuccessCallback : function(data) {
255 | data = jQuery.trim(data);
256 |
257 | if(data && data!=""){
258 | var response = jQuery.parseJSON(jQuery.trim(data));
259 | if (response !== null && response.hasOwnProperty("display_as")) {
260 | this.element.attr("data-original-content", this.element.text());
261 | this.original_content = this.element.text();
262 | this.element.html(response["display_as"]);
263 | }
264 |
265 | this.element.trigger(jQuery.Event("best_in_place:success"), data);
266 | this.element.trigger(jQuery.Event("ajax:success"), data);
267 | } else {
268 | this.element.trigger(jQuery.Event("best_in_place:success"));
269 | this.element.trigger(jQuery.Event("ajax:success"));
270 | }
271 |
272 | // Binding back after being clicked
273 | if(this.dblClick) {
274 | jQuery(this.activator).bind('dblclick', {editor: this}, this.clickHandler);
275 | } else {
276 | jQuery(this.activator).bind('click', {editor: this}, this.clickHandler);
277 | }
278 | this.element.trigger(jQuery.Event("best_in_place:deactivate"));
279 |
280 | if (this.collectionValue !== null && this.formType == "select") {
281 | this.collectionValue = this.previousCollectionValue;
282 | this.previousCollectionValue = null;
283 | }
284 | },
285 |
286 | loadErrorCallback : function(request, error) {
287 | this.activateText(this.oldValue);
288 |
289 | this.element.trigger(jQuery.Event("best_in_place:error"), [request, error]);
290 | this.element.trigger(jQuery.Event("ajax:error"), request, error);
291 |
292 | // Binding back after being clicked
293 | if(this.dblClick) {
294 | jQuery(this.activator).bind('dblclick', {editor: this}, this.clickHandler);
295 | } else {
296 | jQuery(this.activator).bind('click', {editor: this}, this.clickHandler);
297 | }
298 | this.element.trigger(jQuery.Event("best_in_place:deactivate"));
299 | },
300 |
301 | clickHandler : function(event) {
302 | event.preventDefault();
303 | event.data.editor.activate();
304 | },
305 |
306 | setHtmlAttributes : function() {
307 | var formField = this.element.find(this.formType);
308 |
309 | if(this.html_attrs){
310 | var attrs = jQuery.parseJSON(this.html_attrs);
311 | for(var key in attrs){
312 | formField.attr(key, attrs[key]);
313 | }
314 | }
315 | }
316 | };
317 |
318 |
319 | // Button cases:
320 | // If no buttons, then blur saves, ESC cancels
321 | // If just Cancel button, then blur saves, ESC or clicking Cancel cancels (careful of blur event!)
322 | // If just OK button, then clicking OK saves (careful of blur event!), ESC or blur cancels
323 | // If both buttons, then clicking OK saves, ESC or clicking Cancel or blur cancels
324 | BestInPlaceEditor.forms = {
325 | "input" : {
326 | activateForm : function() {
327 | var output = jQuery(document.createElement('form'))
328 | .addClass('form_in_place')
329 | .attr('action', 'javascript:void(0);')
330 | .attr('style', 'display:inline');
331 | var input_elt = jQuery(document.createElement('input'))
332 | .attr('type', 'text')
333 | .attr('name', this.attributeName)
334 | .val(this.display_value);
335 | if(this.inner_class !== null) {
336 | input_elt.addClass(this.inner_class);
337 | }
338 | if(this.inner_id !== null) {
339 | input_elt.attr('id', this.inner_id);
340 | }
341 | output.append(input_elt);
342 | if(this.okButton) {
343 | output.append(
344 | jQuery(document.createElement('input'))
345 | .attr('type', 'submit')
346 | .attr('class', this.okButtonClass)
347 | .attr('value', this.okButton)
348 | )
349 | }
350 | if(this.cancelButton) {
351 | output.append(
352 | jQuery(document.createElement('input'))
353 | .attr('type', 'button')
354 | .attr('class', this.cancelButtonClass)
355 | .attr('value', this.cancelButton)
356 | )
357 | }
358 |
359 | this.element.html(output);
360 | this.setHtmlAttributes();
361 | this.element.find("input[type='text']")[0].select();
362 | this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler);
363 | if (this.cancelButton) {
364 | this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.input.cancelButtonHandler);
365 | }
366 | this.element.find("input[type='text']").bind('blur', {editor: this}, BestInPlaceEditor.forms.input.inputBlurHandler);
367 | this.element.find("input[type='text']").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler);
368 | this.blurTimer = null;
369 | this.userClicked = false;
370 | },
371 |
372 | getValue : function() {
373 | return this.sanitizeValue(this.element.find("input").val());
374 | },
375 |
376 | // When buttons are present, use a timer on the blur event to give precedence to clicks
377 | inputBlurHandler : function(event) {
378 | if (event.data.editor.okButton) {
379 | event.data.editor.blurTimer = setTimeout(function () {
380 | if (!event.data.editor.userClicked) {
381 | event.data.editor.abort();
382 | }
383 | }, 500);
384 | } else {
385 | if (event.data.editor.cancelButton) {
386 | event.data.editor.blurTimer = setTimeout(function () {
387 | if (!event.data.editor.userClicked) {
388 | event.data.editor.update();
389 | }
390 | }, 500);
391 | } else {
392 | event.data.editor.update();
393 | }
394 | }
395 | },
396 |
397 | submitHandler : function(event) {
398 | event.data.editor.userClicked = true;
399 | clearTimeout(event.data.editor.blurTimer);
400 | event.data.editor.update();
401 | },
402 |
403 | cancelButtonHandler : function(event) {
404 | event.data.editor.userClicked = true;
405 | clearTimeout(event.data.editor.blurTimer);
406 | event.data.editor.abort();
407 | event.stopPropagation(); // Without this, click isn't handled
408 | },
409 |
410 | keyupHandler : function(event) {
411 | if (event.keyCode == 27) {
412 | event.data.editor.abort();
413 | }
414 | }
415 | },
416 |
417 | "date" : {
418 | activateForm : function() {
419 | var that = this,
420 | output = jQuery(document.createElement('form'))
421 | .addClass('form_in_place')
422 | .attr('action', 'javascript:void(0);')
423 | .attr('style', 'display:inline'),
424 | input_elt = jQuery(document.createElement('input'))
425 | .attr('type', 'text')
426 | .attr('name', this.attributeName)
427 | .attr('value', this.sanitizeValue(this.display_value));
428 | if(this.inner_class !== null) {
429 | input_elt.addClass(this.inner_class);
430 | }
431 | output.append(input_elt)
432 |
433 | this.element.html(output);
434 | this.setHtmlAttributes();
435 | this.element.find('input')[0].select();
436 | this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler);
437 | this.element.find("input").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler);
438 |
439 | this.element.find('input')
440 | .datepicker({
441 | onClose: function() {
442 | that.update();
443 | }
444 | })
445 | .datepicker('show');
446 | },
447 |
448 | getValue : function() {
449 | return this.sanitizeValue(this.element.find("input").val());
450 | },
451 |
452 | submitHandler : function(event) {
453 | event.data.editor.update();
454 | },
455 |
456 | keyupHandler : function(event) {
457 | if (event.keyCode == 27) {
458 | event.data.editor.abort();
459 | }
460 | }
461 | },
462 |
463 | "select" : {
464 | activateForm : function() {
465 | var output = jQuery(document.createElement('form'))
466 | .attr('action', 'javascript:void(0)')
467 | .attr('style', 'display:inline');
468 | selected = '',
469 | oldValue = this.oldValue,
470 | select_elt = jQuery(document.createElement('select'))
471 | .attr('class', this.inned_class !== null ? this.inner_class : '' ),
472 | currentCollectionValue = this.collectionValue;
473 |
474 | jQuery.each(this.values, function (index, value) {
475 | var option_elt = jQuery(document.createElement('option'))
476 | // .attr('value', value[0])
477 | .val(value[0])
478 | .html(value[1]);
479 | if(value[0] == currentCollectionValue) {
480 | option_elt.attr('selected', 'selected');
481 | }
482 | select_elt.append(option_elt);
483 | });
484 | output.append(select_elt);
485 |
486 | this.element.html(output);
487 | this.setHtmlAttributes();
488 | this.element.find("select").bind('change', {editor: this}, BestInPlaceEditor.forms.select.blurHandler);
489 | this.element.find("select").bind('blur', {editor: this}, BestInPlaceEditor.forms.select.blurHandler);
490 | this.element.find("select").bind('keyup', {editor: this}, BestInPlaceEditor.forms.select.keyupHandler);
491 | this.element.find("select")[0].focus();
492 | },
493 |
494 | getValue : function() {
495 | return this.sanitizeValue(this.element.find("select").val());
496 | // return this.element.find("select").val();
497 | },
498 |
499 | blurHandler : function(event) {
500 | event.data.editor.update();
501 | },
502 |
503 | keyupHandler : function(event) {
504 | if (event.keyCode == 27) event.data.editor.abort();
505 | }
506 | },
507 |
508 | "checkbox" : {
509 | activateForm : function() {
510 | this.collectionValue = !this.getValue();
511 | this.setHtmlAttributes();
512 | this.update();
513 | },
514 |
515 | getValue : function() {
516 | return this.collectionValue;
517 | }
518 | },
519 |
520 | "textarea" : {
521 | activateForm : function() {
522 | // grab width and height of text
523 | width = this.element.css('width');
524 | height = this.element.css('height');
525 |
526 | // construct form
527 | var output = jQuery(document.createElement('form'))
528 | .attr('action', 'javascript:void(0)')
529 | .attr('style', 'display:inline')
530 | .append(jQuery(document.createElement('textarea'))
531 | .val(this.sanitizeValue(this.display_value)));
532 | if(this.okButton) {
533 | output.append(
534 | jQuery(document.createElement('input'))
535 | .attr('type', 'submit')
536 | .attr('value', this.okButton)
537 | );
538 | }
539 | if(this.cancelButton) {
540 | output.append(
541 | jQuery(document.createElement('input'))
542 | .attr('type', 'button')
543 | .attr('value', this.cancelButton)
544 | )
545 | }
546 |
547 | this.element.html(output);
548 | this.setHtmlAttributes();
549 |
550 | // set width and height of textarea
551 | jQuery(this.element.find("textarea")[0]).css({ 'min-width': width, 'min-height': height });
552 | //jQuery(this.element.find("textarea")[0]).elastic();
553 |
554 | this.element.find("textarea")[0].focus();
555 | this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.textarea.submitHandler);
556 | if (this.cancelButton) {
557 | this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.textarea.cancelButtonHandler);
558 | }
559 | //this.element.find("textarea").bind('blur', {editor: this}, BestInPlaceEditor.forms.textarea.blurHandler);
560 | this.element.find("textarea").bind('keydown', {editor: this}, BestInPlaceEditor.forms.textarea.keydownHandler);
561 | this.blurTimer = null;
562 | this.userClicked = false;
563 | },
564 |
565 | getValue : function() {
566 | return this.sanitizeValue(this.element.find("textarea").val());
567 | },
568 |
569 | // When buttons are present, use a timer on the blur event to give precedence to clicks
570 | blurHandler : function(event) {
571 | if (event.data.editor.okButton) {
572 | event.data.editor.blurTimer = setTimeout(function () {
573 | if (!event.data.editor.userClicked) {
574 | event.data.editor.abortIfConfirm();
575 | }
576 | }, 500);
577 | } else {
578 | if (event.data.editor.cancelButton) {
579 | event.data.editor.blurTimer = setTimeout(function () {
580 | if (!event.data.editor.userClicked) {
581 | event.data.editor.update();
582 | }
583 | }, 500);
584 | } else {
585 | event.data.editor.update();
586 | }
587 | }
588 | },
589 |
590 | submitHandler : function(event) {
591 | event.data.editor.userClicked = true;
592 | clearTimeout(event.data.editor.blurTimer);
593 | event.data.editor.update();
594 | },
595 |
596 | cancelButtonHandler : function(event) {
597 | event.data.editor.userClicked = true;
598 | clearTimeout(event.data.editor.blurTimer);
599 | event.data.editor.abortIfConfirm();
600 | event.stopPropagation(); // Without this, click isn't handled
601 | },
602 |
603 | keydownHandler : function(event) {
604 | if (event.keyCode == 27) {
605 | event.data.editor.abortIfConfirm();
606 | }
607 | if (event.keyCode == 13) {
608 | event.data.editor.userClicked = true;
609 | clearTimeout(event.data.editor.blurTimer);
610 | event.data.editor.update();
611 | }
612 | }
613 | }
614 | };
615 |
616 | jQuery.fn.best_in_place = function() {
617 |
618 | function setBestInPlace(element) {
619 | if (!element.data('bestInPlaceEditor')) {
620 | element.data('bestInPlaceEditor', new BestInPlaceEditor(element));
621 | return true;
622 | }
623 | }
624 |
625 | jQuery(this.context).delegate(this.selector, 'click', function () {
626 | var el = jQuery(this);
627 | if (setBestInPlace(el))
628 | el.click();
629 | });
630 |
631 | this.each(function () {
632 | setBestInPlace(jQuery(this));
633 | });
634 |
635 | return this;
636 | };
637 |
638 |
639 |
640 | /**
641 | * @name Elastic
642 | * @descripton Elastic is Jquery plugin that grow and shrink your textareas automaticliy
643 | * @version 1.6.5
644 | * @requires Jquery 1.2.6+
645 | *
646 | * @author Jan Jarfalk
647 | * @author-email jan.jarfalk@unwrongest.com
648 | * @author-website http://www.unwrongest.com
649 | *
650 | * @licens MIT License - http://www.opensource.org/licenses/mit-license.php
651 | */
652 |
653 | (function(jQuery){
654 | if (typeof jQuery.fn.elastic !== 'undefined') return;
655 |
656 | jQuery.fn.extend({
657 | elastic: function() {
658 | // We will create a div clone of the textarea
659 | // by copying these attributes from the textarea to the div.
660 | var mimics = [
661 | 'paddingTop',
662 | 'paddingRight',
663 | 'paddingBottom',
664 | 'paddingLeft',
665 | 'fontSize',
666 | 'lineHeight',
667 | 'fontFamily',
668 | 'width',
669 | 'fontWeight'];
670 |
671 | return this.each( function() {
672 |
673 | // Elastic only works on textareas
674 | if ( this.type != 'textarea' ) {
675 | return false;
676 | }
677 |
678 | var $textarea = jQuery(this),
679 | $twin = jQuery('').css({'position': 'absolute','display':'none','word-wrap':'break-word'}),
680 | lineHeight = parseInt($textarea.css('line-height'),10) || parseInt($textarea.css('font-size'),'10'),
681 | minheight = parseInt($textarea.css('height'),10) || lineHeight*3,
682 | maxheight = parseInt($textarea.css('max-height'),10) || Number.MAX_VALUE,
683 | goalheight = 0,
684 | i = 0;
685 |
686 | // Opera returns max-height of -1 if not set
687 | if (maxheight < 0) { maxheight = Number.MAX_VALUE; }
688 |
689 | // Append the twin to the DOM
690 | // We are going to meassure the height of this, not the textarea.
691 | $twin.appendTo($textarea.parent());
692 |
693 | // Copy the essential styles (mimics) from the textarea to the twin
694 | i = mimics.length;
695 | while(i--){
696 | $twin.css(mimics[i].toString(),$textarea.css(mimics[i].toString()));
697 | }
698 |
699 |
700 | // Sets a given height and overflow state on the textarea
701 | function setHeightAndOverflow(height, overflow){
702 | curratedHeight = Math.floor(parseInt(height,10));
703 | if($textarea.height() != curratedHeight){
704 | $textarea.css({'height': curratedHeight + 'px','overflow':overflow});
705 |
706 | }
707 | }
708 |
709 |
710 | // This function will update the height of the textarea if necessary
711 | function update() {
712 |
713 | // Get curated content from the textarea.
714 | var textareaContent = $textarea.val().replace(/&/g,'&').replace(/ /g, ' ').replace(/<|>/g, '>').replace(/\n/g, '
');
715 |
716 | // Compare curated content with curated twin.
717 | var twinContent = $twin.html().replace(/
/ig,'
');
718 |
719 | if(textareaContent+' ' != twinContent){
720 |
721 | // Add an extra white space so new rows are added when you are at the end of a row.
722 | $twin.html(textareaContent+' ');
723 |
724 | // Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height
725 | if(Math.abs($twin.height() + lineHeight - $textarea.height()) > 3){
726 |
727 | var goalheight = $twin.height()+lineHeight;
728 | if(goalheight >= maxheight) {
729 | setHeightAndOverflow(maxheight,'auto');
730 | } else if(goalheight <= minheight) {
731 | setHeightAndOverflow(minheight,'hidden');
732 | } else {
733 | setHeightAndOverflow(goalheight,'hidden');
734 | }
735 |
736 | }
737 |
738 | }
739 |
740 | }
741 |
742 | // Hide scrollbars
743 | $textarea.css({'overflow':'hidden'});
744 |
745 | // Update textarea size on keyup, change, cut and paste
746 | $textarea.bind('keyup change cut paste', function(){
747 | update();
748 | });
749 |
750 | // Compact textarea on blur
751 | // Lets animate this....
752 | $textarea.bind('blur',function(){
753 | if($twin.height() < maxheight){
754 | if($twin.height() > minheight) {
755 | $textarea.height($twin.height());
756 | } else {
757 | $textarea.height(minheight);
758 | }
759 | }
760 | });
761 |
762 | // And this line is to catch the browser paste event
763 | $textarea.on("input paste", function(e){ setTimeout( update, 250); });
764 |
765 | // Run update once when elastic is initialized
766 | update();
767 |
768 | });
769 |
770 | }
771 | });
772 | })(jQuery);
773 |
--------------------------------------------------------------------------------
/spec/integration/js_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require "spec_helper"
3 |
4 | describe "JS behaviour", :js => true do
5 | before do
6 | @user = User.new :name => "Lucia",
7 | :last_name => "Napoli",
8 | :email => "lucianapoli@gmail.com",
9 | :height => "5' 5\"",
10 | :address => "Via Roma 99",
11 | :zip => "25123",
12 | :country => "2",
13 | :receive_email => false,
14 | :birth_date => Time.now.utc,
15 | :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem.",
16 | :money => 100,
17 | :money_proc => 100,
18 | :favorite_color => 'Red',
19 | :favorite_books => "The City of Gold and Lead",
20 | :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem.",
21 | :favorite_movie => "The Hitchhiker's Guide to the Galaxy"
22 | end
23 |
24 | describe "namespaced controllers" do
25 | it "should be able to use array-notation to describe both object and path" do
26 | @user.save!
27 | visit admin_user_path(@user)
28 |
29 | within("#last_name") { page.should have_content("Napoli") }
30 | bip_text @user, :last_name, "Other thing"
31 |
32 | within("#last_name") { page.should have_content("Other thing") }
33 | end
34 | end
35 |
36 | describe "nil option" do
37 | it "should render an em-dash when the field is empty" do
38 | @user.name = ""
39 | @user.save :validate => false
40 | visit user_path(@user)
41 |
42 | within("#name") do
43 | page.should have_content("\u2014")
44 | end
45 | end
46 |
47 | it "should render the default em-dash string when there is an error and if the intial string is em-dash" do
48 | @user.money = nil
49 | @user.save!
50 | visit user_path(@user)
51 |
52 | bip_text @user, :money, "abcd"
53 |
54 | within("#money") do
55 | page.should have_content("\u2014")
56 | end
57 | end
58 |
59 | it "should render the passed nil value if the field is empty" do
60 | @user.last_name = ""
61 | @user.save :validate => false
62 | visit user_path(@user)
63 |
64 | within("#last_name") do
65 | page.should have_content("Nothing to show")
66 | end
67 | end
68 |
69 | it "should render html content for nil option" do
70 | @user.favorite_color = ""
71 | @user.save!
72 | visit user_path(@user)
73 | within("#favorite_color") do
74 | page.should have_xpath("//span[@class='nil']")
75 | end
76 | end
77 |
78 | it "should render html content for nil option after edit" do
79 | @user.favorite_color = "Blue"
80 | @user.save!
81 | visit user_path(@user)
82 |
83 | bip_text @user, :favorite_color, ""
84 |
85 | within("#favorite_color") do
86 | page.should have_xpath("//span[@class='nil']")
87 | end
88 | end
89 |
90 | it "should display an empty input field the second time I open it" do
91 | @user.favorite_locale = nil
92 | @user.save!
93 | visit user_path(@user)
94 |
95 | within("#favorite_locale") do
96 | page.should have_content("N/A")
97 | end
98 |
99 | id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_locale
100 | page.execute_script <<-JS
101 | $("##{id}").click();
102 | JS
103 |
104 | text = page.find("##{id} input").value
105 | text.should == ""
106 |
107 | page.execute_script <<-JS
108 | $("##{id} input[name='favorite_locale']").blur();
109 | $("##{id} input[name='favorite_locale']").blur();
110 | JS
111 | sleep 1
112 |
113 | page.execute_script <<-JS
114 | $("##{id}").click();
115 | JS
116 |
117 | text = page.find("##{id} input").value
118 | text.should == ""
119 | end
120 | end
121 |
122 | it "should be able to update last but one item in list" do
123 | @user.save!
124 | @user2 = User.create :name => "Test",
125 | :last_name => "User",
126 | :email => "test@example.com",
127 | :height => "5' 5\"",
128 | :address => "Via Roma 99",
129 | :zip => "25123",
130 | :country => "2",
131 | :receive_email => false,
132 | :birth_date => Time.now.utc,
133 | :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem.",
134 | :money => 100,
135 | :money_proc => 100,
136 | :favorite_color => 'Red',
137 | :favorite_books => "The City of Gold and Lead",
138 | :description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem."
139 |
140 | visit users_path
141 |
142 | within("tr#user_#{@user.id} > .name > span") do
143 | page.should have_content("Lucia")
144 | page.should have_xpath("//a[contains(@href,'#{user_path(@user)}')]")
145 | end
146 |
147 | id = BestInPlace::Utils.build_best_in_place_id @user, :name
148 | page.execute_script <<-JS
149 | $("#edit_#{@user.id}").click();
150 | $("##{id} input[name='name']").val('Lisa');
151 | $("##{id} form").submit();
152 | JS
153 |
154 | within("tr#user_#{@user.id} > .name > span") do
155 | page.should have_content('Lisa')
156 | end
157 | end
158 |
159 | it "should be able to use bip_text to update a text field" do
160 | @user.save!
161 | visit user_path(@user)
162 | within("#email") do
163 | page.should have_content("lucianapoli@gmail.com")
164 | end
165 |
166 | bip_text @user, :email, "new@email.com"
167 |
168 | visit user_path(@user)
169 | within("#email") do
170 | page.should have_content("new@email.com")
171 | end
172 | end
173 |
174 | it "should be able to update a field two consecutive times" do
175 | @user.save!
176 | visit user_path(@user)
177 |
178 | bip_text @user, :email, "new@email.com"
179 |
180 | within("#email") do
181 | page.should have_content("new@email.com")
182 | end
183 |
184 | bip_text @user, :email, "new_two@email.com"
185 |
186 | within("#email") do
187 | page.should have_content("new_two@email.com")
188 | end
189 |
190 | visit user_path(@user)
191 | within("#email") do
192 | page.should have_content("new_two@email.com")
193 | end
194 | end
195 |
196 | it "should be able to update a field after an error" do
197 | @user.save!
198 | visit user_path(@user)
199 |
200 | bip_text @user, :email, "wrong format"
201 | page.should have_content("Email has wrong email format")
202 |
203 | bip_text @user, :email, "another@email.com"
204 | within("#email") do
205 | page.should have_content("another@email.com")
206 | end
207 |
208 | visit user_path(@user)
209 | within("#email") do
210 | page.should have_content("another@email.com")
211 | end
212 | end
213 |
214 | it "should be able to use bip_select to change a select field" do
215 | @user.save!
216 | visit user_path(@user)
217 | within("#country") do
218 | page.should have_content("Italy")
219 | end
220 |
221 | bip_select @user, :country, "France"
222 |
223 | visit user_path(@user)
224 | within("#country") do
225 | page.should have_content("France")
226 | end
227 | end
228 |
229 | it "should apply the inner_class option to a select field" do
230 | @user.save!
231 | visit user_path(@user)
232 |
233 | find('#country span').click
234 | find('#country').should have_css('select.some_class')
235 | end
236 |
237 | it "should be able to use bip_text to change a date field" do
238 | @user.save!
239 | today = Time.now.utc.to_date
240 | visit user_path(@user)
241 | within("#birth_date") do
242 | page.should have_content(today)
243 | end
244 |
245 | bip_text @user, :birth_date, (today - 1.days)
246 |
247 | visit user_path(@user)
248 | within("#birth_date") do
249 | page.should have_content(today - 1.days)
250 | end
251 | end
252 |
253 | it "should be able to use datepicker to change a date field" do
254 | @user.save!
255 | today = Time.now.utc.to_date
256 | visit user_path(@user)
257 | within("#birth_date") do
258 | page.should have_content(today)
259 | end
260 |
261 | id = BestInPlace::Utils.build_best_in_place_id @user, :birth_date
262 | page.execute_script <<-JS
263 | $("##{id}").click()
264 | $(".ui-datepicker-calendar tbody td").not(".ui-datepicker-other-month").first().click()
265 | JS
266 |
267 | visit user_path(@user)
268 | within("#birth_date") do
269 | page.should have_content(today.beginning_of_month)
270 | end
271 | end
272 |
273 | it "should be able to modify the datepicker options, displaying the date with another format" do
274 | @user.save!
275 | today = Time.now.utc.to_date
276 | visit user_path(@user)
277 | within("#birth_date") do
278 | page.should have_content(today)
279 | end
280 |
281 | id = BestInPlace::Utils.build_best_in_place_id @user, :birth_date
282 | page.execute_script <<-JS
283 | $("##{id}").click()
284 | $(".ui-datepicker-calendar tbody td").not(".ui-datepicker-other-month").first().click()
285 | JS
286 |
287 | within("#birth_date") do
288 | page.should have_content(today.beginning_of_month.strftime("%d-%m-%Y"))
289 | end
290 | end
291 |
292 | it "should be able to use bip_bool to change a boolean value" do
293 | @user.save!
294 | visit user_path(@user)
295 |
296 | within("#receive_email") do
297 | page.should have_content("No thanks")
298 | end
299 |
300 | bip_bool @user, :receive_email
301 |
302 | visit user_path(@user)
303 | within("#receive_email") do
304 | page.should have_content("Yes of course")
305 | end
306 | end
307 |
308 | it "should be able to use bip_bool to change a boolean value using an image" do
309 | @user.save!
310 | visit user_path(@user)
311 |
312 | within("#receive_email_image") do
313 | page.should have_xpath("//img[contains(@src,'no.png')]")
314 | end
315 |
316 | bip_bool @user, :receive_email
317 |
318 | visit user_path(@user)
319 | within("#receive_email_image") do
320 | page.should have_xpath("//img[contains(@src,'yes.png')]")
321 | end
322 | end
323 |
324 | it "should correctly use an OK submit button when so configured for an input" do
325 | @user.save!
326 | visit user_path(@user)
327 |
328 | within("#favorite_color") do
329 | page.should have_content('Red')
330 | end
331 |
332 | id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_color
333 | page.execute_script <<-JS
334 | $("##{id}").click();
335 | $("##{id} input[name='favorite_color']").val('Blue');
336 | JS
337 |
338 | page.find("##{id} input[type='submit']").value.should == 'Do it!'
339 | page.should have_css("##{id} input[type='submit'].custom-submit.other-custom-submit")
340 |
341 | page.execute_script <<-JS
342 | $("##{id} input[type='submit']").click();
343 | JS
344 |
345 | visit user_path(@user)
346 | within("#favorite_color") do
347 | page.should have_content('Blue')
348 | end
349 | end
350 |
351 | it "should correctly use a Cancel button when so configured for an input" do
352 | @user.save!
353 | visit user_path(@user)
354 |
355 | within("#favorite_color") do
356 | page.should have_content('Red')
357 | end
358 |
359 | id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_color
360 | page.execute_script <<-JS
361 | $("##{id}").click();
362 | $("##{id} input[name='favorite_color']").val('Blue');
363 | JS
364 |
365 | page.find("##{id} input[type='button']").value.should == 'Nope'
366 | page.should have_css("##{id} input[type='button'].custom-cancel.other-custom-cancel")
367 |
368 | page.execute_script <<-JS
369 | $("##{id} input[type='button']").click();
370 | JS
371 |
372 | visit user_path(@user)
373 | within("#favorite_color") do
374 | page.should have_content('Red')
375 | end
376 | end
377 |
378 | it "should not ask for confirmation on cancel if it is switched off" do
379 | @user.save!
380 | visit user_path(@user)
381 |
382 | id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_movie
383 | page.execute_script <<-JS
384 | $("##{id}").click();
385 | $("##{id} input[name='favorite_movie']").val('No good movie');
386 | $("##{id} input[type='button']").click();
387 | JS
388 |
389 | lambda { page.driver.browser.switch_to.alert }.should raise_exception(Selenium::WebDriver::Error::NoAlertPresentError)
390 | within("#favorite_movie") do
391 | page.should have_content("The Hitchhiker's Guide to the Galaxy")
392 | end
393 | end
394 |
395 | it "should not submit input on blur if there's an OK button present" do
396 | @user.save!
397 | visit user_path(@user)
398 |
399 | within("#favorite_color") do
400 | page.should have_content('Red')
401 | end
402 |
403 | id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_color
404 | page.execute_script <<-JS
405 | $("##{id}").click();
406 | $("##{id} input[name='favorite_color']").val('Blue');
407 | $("##{id} input[name='favorite_color']").blur();
408 | JS
409 | sleep 1 # Increase if browser is slow
410 |
411 | visit user_path(@user)
412 | within("#favorite_color") do
413 | page.should have_content('Red')
414 | end
415 | end
416 |
417 | it "should still submit input on blur if there's only a Cancel button present" do
418 | @user.save!
419 | visit user_path(@user, :suppress_ok_button => 1)
420 |
421 | within("#favorite_color") do
422 | page.should have_content('Red')
423 | end
424 |
425 | id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_color
426 | page.execute_script %{$("##{id}").click();}
427 | page.should have_no_css("##{id} input[type='submit']")
428 | page.execute_script <<-JS
429 | $("##{id} input[name='favorite_color']").val('Blue');
430 | $("##{id} input[name='favorite_color']").blur();
431 | $("##{id} input[name='favorite_color']").blur();
432 | JS
433 | sleep 1 # Increase if browser is slow
434 |
435 | visit user_path(@user)
436 | within("#favorite_color") do
437 | page.should have_content('Blue')
438 | end
439 | end
440 |
441 | it "should correctly use an OK submit button when so configured for a text area" do
442 | @user.save!
443 | visit user_path(@user)
444 |
445 | within("#favorite_books") do
446 | page.should have_content('The City of Gold and Lead')
447 | end
448 |
449 | id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_books
450 | page.execute_script <<-JS
451 | $("##{id}").click();
452 | $("##{id} textarea").val('1Q84');
453 | $("##{id} input[type='submit']").click();
454 | JS
455 |
456 | visit user_path(@user)
457 | within("#favorite_books") do
458 | page.should have_content('1Q84')
459 | end
460 | end
461 |
462 | it "should correctly use a Cancel button when so configured for a text area" do
463 | @user.save!
464 | visit user_path(@user)
465 |
466 | within("#favorite_books") do
467 | page.should have_content('The City of Gold and Lead')
468 | end
469 |
470 | id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_books
471 | page.execute_script <<-JS
472 | $("##{id}").click();
473 | $("##{id} textarea").val('1Q84');
474 | $("##{id} input[type='button']").click();
475 | JS
476 | page.driver.browser.switch_to.alert.accept
477 |
478 | visit user_path(@user)
479 | within("#favorite_books") do
480 | page.should have_content('The City of Gold and Lead')
481 | end
482 | end
483 |
484 | it "should not submit text area on blur if there's an OK button present" do
485 | @user.save!
486 | visit user_path(@user)
487 |
488 | within("#favorite_books") do
489 | page.should have_content('The City of Gold and Lead')
490 | end
491 |
492 | id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_books
493 | page.execute_script <<-JS
494 | $("##{id}").click();
495 | $("##{id} textarea").val('1Q84');
496 | $("##{id} textarea").blur();
497 | $("##{id} textarea").blur();
498 | JS
499 | sleep 1 # Increase if browser is slow
500 | page.driver.browser.switch_to.alert.accept
501 |
502 | visit user_path(@user)
503 | within("#favorite_books") do
504 | page.should have_content('The City of Gold and Lead')
505 | end
506 | end
507 |
508 | it "should still submit text area on blur if there's only a Cancel button present" do
509 | @user.save!
510 | visit user_path(@user, :suppress_ok_button => 1)
511 |
512 | within("#favorite_books") do
513 | page.should have_content('The City of Gold and Lead')
514 | end
515 |
516 | id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_books
517 | page.execute_script %{$("##{id}").click();}
518 | page.should have_no_css("##{id} input[type='submit']")
519 | page.execute_script <<-JS
520 | $("##{id} textarea").val('1Q84');
521 | $("##{id} textarea").blur();
522 | $("##{id} textarea").blur();
523 | JS
524 | sleep 1 # Increase if browser is slow
525 |
526 | visit user_path(@user)
527 | within("#favorite_books") do
528 | page.should have_content('1Q84')
529 | end
530 | end
531 |
532 | it "should show validation errors" do
533 | @user.save!
534 | visit user_path(@user)
535 |
536 | bip_text @user, :address, ""
537 | page.should have_content("Address can't be blank")
538 | within("#address") do
539 | page.should have_content("Via Roma 99")
540 | end
541 | end
542 |
543 | it "should fire off a callback when updating a field" do
544 | @user.save!
545 | visit user_path(@user)
546 |
547 | id = BestInPlace::Utils.build_best_in_place_id @user, :last_name
548 | page.execute_script <<-JS
549 | $("##{id}").bind('best_in_place:update', function() { $('body').append('Last name was updated!') });
550 | JS
551 |
552 | page.should have_no_content('Last name was updated!')
553 | bip_text @user, :last_name, 'Another'
554 | page.should have_content('Last name was updated!')
555 | end
556 |
557 | it "should fire off a callback when retrieve success with empty data" do
558 | @user.save!
559 | visit user_path(@user)
560 |
561 | id = BestInPlace::Utils.build_best_in_place_id @user, :last_name
562 | page.execute_script <<-JS
563 | $("##{id}").bind('best_in_place:success', function() { $('body').append('Updated successfully!') });
564 | JS
565 |
566 | page.should have_no_content('Updated successfully!')
567 | bip_text @user, :last_name, 'Empty'
568 | page.should have_content('Updated successfully!')
569 | end
570 |
571 | describe "display_as" do
572 | it "should render the address with a custom format" do
573 | @user.save!
574 | visit user_path(@user)
575 |
576 | within("#address") do
577 | page.should have_content("addr => [Via Roma 99]")
578 | end
579 | end
580 |
581 | it "should still show the custom format after an error" do
582 | @user.save!
583 | visit user_path(@user)
584 |
585 | bip_text @user, :address, "inva"
586 |
587 | within("#address") do
588 | page.should have_content("addr => [Via Roma 99]")
589 | end
590 | end
591 |
592 | it "should show the new result with the custom format after an update" do
593 | @user.save!
594 | visit user_path(@user)
595 |
596 | bip_text @user, :address, "New address"
597 |
598 | within("#address") do
599 | page.should have_content("addr => [New address]")
600 | end
601 | end
602 |
603 | it "should display the original content when editing the form" do
604 | @user.save!
605 | retry_on_timeout do
606 | visit user_path(@user)
607 |
608 | id = BestInPlace::Utils.build_best_in_place_id @user, :address
609 | page.execute_script <<-JS
610 | $("##{id}").click();
611 | JS
612 |
613 | text = page.find("##{id} input").value
614 | text.should == "Via Roma 99"
615 | end
616 | end
617 |
618 | it "should display the updated content after editing the field two consecutive times" do
619 | @user.save!
620 | retry_on_timeout do
621 | visit user_path(@user)
622 |
623 | bip_text @user, :address, "New address"
624 |
625 | sleep 1
626 |
627 | id = BestInPlace::Utils.build_best_in_place_id @user, :address
628 | page.execute_script <<-JS
629 | $("##{id}").click();
630 | JS
631 |
632 | sleep 1
633 |
634 | text = page.find("##{id} input").value
635 | text.should == "New address"
636 | end
637 | end
638 |
639 | it "should quote properly the data-original-content attribute" do
640 | @user.address = "A's & B's"
641 | @user.save!
642 | retry_on_timeout do
643 | visit user_path(@user)
644 |
645 | id = BestInPlace::Utils.build_best_in_place_id @user, :address
646 |
647 | text = page.find("##{id}")["data-original-content"]
648 | text.should == "A's & B's"
649 | end
650 | end
651 | end
652 |
653 | describe "display_with" do
654 | it "should show nil text when original value is nil" do
655 | @user.description = ""
656 | @user.save!
657 |
658 | visit user_path(@user)
659 |
660 | within("#dw_description") { page.should have_content("\u2014") }
661 | end
662 |
663 | it "should render the money using number_to_currency" do
664 | @user.save!
665 | visit user_path(@user)
666 |
667 | within("#money") do
668 | page.should have_content("$100.00")
669 | end
670 | end
671 |
672 | it "should let me use custom helpers with a lambda" do
673 | @user.save!
674 | visit user_path(@user)
675 |
676 | page.should have_content("100.0 €")
677 | bip_text @user, :money_custom, "250"
678 |
679 | within("#money_custom") do
680 | page.should have_content("250.0 €")
681 | end
682 | end
683 |
684 | it "should still show the custom format after an error" do
685 | @user.save!
686 | visit user_path(@user)
687 |
688 | bip_text @user, :money, "string"
689 |
690 | page.should have_content("Money is not a number")
691 |
692 | within("#money") do
693 | page.should have_content("$100.00")
694 | end
695 | end
696 |
697 | it "should show the new value using the helper after a successful update" do
698 | @user.save!
699 | visit user_path(@user)
700 |
701 | bip_text @user, :money, "240"
702 |
703 | within("#money") do
704 | page.should have_content("$240.00")
705 | end
706 | end
707 |
708 | it "should display the original content when editing the form" do
709 | @user.save!
710 | retry_on_timeout do
711 | visit user_path(@user)
712 |
713 | id = BestInPlace::Utils.build_best_in_place_id @user, :money
714 | page.execute_script <<-JS
715 | $("##{id}").click();
716 | JS
717 |
718 | text = page.find("##{id} input").value
719 | text.should == "100.0"
720 | end
721 | end
722 |
723 | it "should display the updated content after editing the field two consecutive times" do
724 | @user.save!
725 |
726 | retry_on_timeout do
727 | visit user_path(@user)
728 |
729 | bip_text @user, :money, "40"
730 |
731 | sleep 1
732 |
733 | id = BestInPlace::Utils.build_best_in_place_id @user, :money
734 | page.execute_script <<-JS
735 | $("##{id}").click();
736 | JS
737 |
738 | sleep 1
739 |
740 | text = page.find("##{id} input").value
741 | text.should == "40"
742 | end
743 | end
744 |
745 | it "should show the money in euros" do
746 | @user.save!
747 | visit double_init_user_path(@user)
748 |
749 | within("#alt_money") { page.should have_content("€100.00") }
750 |
751 | bip_text @user, :money, 58
752 |
753 | within("#alt_money") { page.should have_content("€58.00") }
754 | end
755 |
756 | it "should keep link after edit with display_with :link_to" do
757 | @user.save!
758 | visit users_path
759 | within("tr#user_#{@user.id} > .name > span") do
760 | page.should have_content("Lucia")
761 | page.should have_xpath("//a[contains(@href,'#{user_path(@user)}')]")
762 | end
763 | id = BestInPlace::Utils.build_best_in_place_id @user, :name
764 | page.execute_script <<-JS
765 | jQuery("#edit_#{@user.id}").click();
766 | jQuery("##{id} input[name='name']").val('Maria Lucia');
767 | jQuery("##{id} form").submit();
768 | JS
769 | within("tr#user_#{@user.id} > .name > span") do
770 | page.should have_content("Maria Lucia")
771 | page.should have_xpath("//a[contains(@href,'#{user_path(@user)}')]")
772 | end
773 | end
774 |
775 | it "should keep link after aborting edit with display_with :link_to" do
776 | @user.save!
777 | visit users_path
778 | within("tr#user_#{@user.id} > .name > span") do
779 | page.should have_content("Lucia")
780 | page.should have_xpath("//a[contains(@href,'#{user_path(@user)}')]")
781 | end
782 | id = BestInPlace::Utils.build_best_in_place_id @user, :name
783 | page.execute_script <<-JS
784 | jQuery("#edit_#{@user.id}").click();
785 | jQuery("##{id} input[name='name']").blur();
786 | jQuery("##{id} input[name='name']").blur();
787 | JS
788 | within("tr#user_#{@user.id} > .name > span") do
789 | page.should have_content("Lucia")
790 | page.should have_xpath("//a[contains(@href,'#{user_path(@user)}')]")
791 | end
792 | end
793 |
794 | describe "display_with using a lambda" do
795 | it "should render the money" do
796 | @user.save!
797 | visit user_path(@user)
798 |
799 | within("#money_proc") do
800 | page.should have_content("$100.00")
801 | end
802 | end
803 |
804 | it "should show the new value using the helper after a successful update" do
805 | @user.save!
806 | visit user_path(@user)
807 |
808 | bip_text @user, :money_proc, "240"
809 |
810 | within("#money_proc") do
811 | page.should have_content("$240.00")
812 | end
813 | end
814 |
815 | it "should display the original content when editing the form" do
816 | @user.save!
817 | retry_on_timeout do
818 | visit user_path(@user)
819 |
820 | id = BestInPlace::Utils.build_best_in_place_id @user, :money_proc
821 | page.execute_script <<-JS
822 | $("##{id}").click();
823 | JS
824 |
825 | text = page.find("##{id} input").value
826 | text.should == "100.0"
827 | end
828 | end
829 |
830 | it "should display the updated content after editing the field two consecutive times" do
831 | @user.save!
832 |
833 | retry_on_timeout do
834 | visit user_path(@user)
835 |
836 | bip_text @user, :money_proc, "40"
837 |
838 | sleep 1
839 |
840 | id = BestInPlace::Utils.build_best_in_place_id @user, :money_proc
841 | page.execute_script <<-JS
842 | $("##{id}").click();
843 | JS
844 |
845 | sleep 1
846 |
847 | text = page.find("##{id} input").value
848 | text.should == "40"
849 | end
850 | end
851 |
852 | end
853 |
854 | end
855 |
856 | it "should display strings with quotes correctly in fields" do
857 | @user.last_name = "A last name \"with double quotes\""
858 | @user.save!
859 |
860 | retry_on_timeout do
861 | visit user_path(@user)
862 |
863 | id = BestInPlace::Utils.build_best_in_place_id @user, :last_name
864 | page.execute_script <<-JS
865 | $("##{id}").click();
866 | JS
867 |
868 | text = page.find("##{id} input").value
869 | text.should == "A last name \"with double quotes\""
870 | end
871 | end
872 |
873 | it "should allow me to set texts with quotes with sanitize => false" do
874 | @user.save!
875 |
876 | retry_on_timeout do
877 | visit double_init_user_path(@user)
878 |
879 | bip_area @user, :description, "A link in this text not sanitized."
880 | visit double_init_user_path(@user)
881 |
882 | page.should have_link("link in this text", :href => "http://google.es")
883 | end
884 | end
885 |
886 | it "should show the input with not-scaped ampersands with sanitize => false" do
887 | @user.description = "A text with an & and a Raw html"
888 | @user.save!
889 |
890 | retry_on_timeout do
891 | visit double_init_user_path(@user)
892 |
893 | id = BestInPlace::Utils.build_best_in_place_id @user, :description
894 | page.execute_script <<-JS
895 | $("##{id}").click();
896 | JS
897 |
898 | text = page.find("##{id} textarea").value
899 | text.should == "A text with an & and a Raw html"
900 | end
901 | end
902 |
903 | it "should keep the same value after multipe edits" do
904 | @user.save!
905 |
906 | retry_on_timeout do
907 | visit double_init_user_path(@user)
908 |
909 | bip_area @user, :description, "A link in this text not sanitized."
910 | visit double_init_user_path(@user)
911 |
912 | page.should have_link("link in this text", :href => "http://google.es")
913 |
914 | id = BestInPlace::Utils.build_best_in_place_id @user, :description
915 | page.execute_script <<-JS
916 | $("##{id}").click();
917 | JS
918 |
919 | page.find("##{id} textarea").value.should eq("A link in this text not sanitized.")
920 | end
921 | end
922 |
923 | it "should display single- and double-quotes in values appropriately" do
924 | @user.height = %{5' 6"}
925 | @user.save!
926 |
927 | retry_on_timeout do
928 | visit user_path(@user)
929 |
930 | id = BestInPlace::Utils.build_best_in_place_id @user, :height
931 | page.execute_script <<-JS
932 | $("##{id}").click();
933 | JS
934 |
935 | page.find("##{id} select").value.should eq(%{5' 6"})
936 | end
937 | end
938 |
939 | it "should save single- and double-quotes in values appropriately" do
940 | @user.height = %{5' 10"}
941 | @user.save!
942 |
943 | retry_on_timeout do
944 | visit user_path(@user)
945 |
946 | id = BestInPlace::Utils.build_best_in_place_id @user, :height
947 | page.execute_script <<-JS
948 | $("##{id}").click();
949 | $("##{id} select").val("5' 7\\\"");
950 | $("##{id} select").blur();
951 | $("##{id} select").blur();
952 | JS
953 |
954 | sleep 1
955 |
956 | @user.reload
957 | @user.height.should eq(%{5' 7"})
958 | end
959 | end
960 |
961 | it "should escape javascript in test helpers" do
962 | @user.save!
963 |
964 | retry_on_timeout do
965 | visit user_path(@user)
966 |
967 | bip_text @user, :last_name, "Other '); alert('hi');"
968 | sleep 1
969 |
970 | @user.reload
971 | @user.last_name.should eq("Other '); alert('hi');")
972 | end
973 | end
974 |
975 | it "should save text in database without encoding" do
976 | @user.save!
977 |
978 | retry_on_timeout do
979 | visit user_path(@user)
980 |
981 | bip_text @user, :last_name, "Other \"thing\""
982 | sleep 1
983 |
984 | @user.reload
985 | @user.last_name.should eq("Other \"thing\"")
986 | end
987 | end
988 |
989 | it "should not strip html tags" do
990 | @user.save!
991 |
992 | retry_on_timeout do
993 | visit user_path(@user)
994 |
995 | bip_text @user, :last_name, ""
996 | within("#last_name") { page.should have_content("") }
997 |
998 | visit user_path(@user)
999 |
1000 | id = BestInPlace::Utils.build_best_in_place_id @user, :last_name
1001 | page.execute_script <<-JS
1002 | $("##{id}").click();
1003 | JS
1004 |
1005 | page.find("##{id} input").value.should eq("")
1006 | end
1007 | end
1008 |
1009 | it "should generate the select html with the proper current option selected" do
1010 | @user.save!
1011 | visit user_path(@user)
1012 | within("#country") do
1013 | page.should have_content("Italy")
1014 | end
1015 |
1016 | id = BestInPlace::Utils.build_best_in_place_id @user, :country
1017 | page.execute_script <<-JS
1018 | $("##{id}").click();
1019 | JS
1020 |
1021 | page.should have_css("##{id} select option[value='2'][selected='selected']")
1022 | end
1023 |
1024 | it "should generate the select with the proper current option without reloading the page" do
1025 | @user.save!
1026 | visit user_path(@user)
1027 | within("#country") do
1028 | page.should have_content("Italy")
1029 | end
1030 |
1031 | bip_select @user, :country, "France"
1032 |
1033 | sleep 1 # Increase if browser is slow
1034 | id = BestInPlace::Utils.build_best_in_place_id @user, :country
1035 | page.execute_script <<-JS
1036 | $("##{id}").click();
1037 | JS
1038 |
1039 | page.should have_css("##{id} select option[value='4'][selected='selected']")
1040 | end
1041 | end
1042 |
--------------------------------------------------------------------------------