├── .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 |

New user

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Back', users_path %> 6 | -------------------------------------------------------------------------------- /test_app/app/assets/images/red_pen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/red_pen.png -------------------------------------------------------------------------------- /test_app/app/views/users/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit user

2 | 3 | <%= render 'form' %> 4 | 5 | <%= link_to 'Back', users_path %> 6 | -------------------------------------------------------------------------------- /test_app/test/unit/helpers/users_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /test_app/config/initializers/default_date_format.rb: -------------------------------------------------------------------------------- 1 | Date::DATE_FORMATS[:default] = '%Y-%m-%d' 2 | Time::DATE_FORMATS[:default] = '%Y-%m-%d' 3 | -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-icons_228ef1_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-icons_228ef1_256x240.png -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-icons_ef8c08_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-icons_ef8c08_256x240.png -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-icons_ffd27a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-icons_ffd27a_256x240.png -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-bg_flat_10_000000_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-bg_flat_10_000000_40x100.png -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-bg_glass_100_f6f6f6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-bg_glass_100_f6f6f6_1x400.png -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-bg_glass_100_fdf5ce_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-bg_glass_100_fdf5ce_1x400.png -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /test_app/db/migrate/20111224181356_add_money_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddMoneyToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :money, :float 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test_app/db/migrate/20120620165212_add_height_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddHeightToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :height, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-bg_gloss-wave_35_f6a828_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-bg_gloss-wave_35_f6a828_500x100.png -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-bg_highlight-soft_75_ffe45c_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-bg_highlight-soft_75_ffe45c_1x100.png -------------------------------------------------------------------------------- /test_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run BipApp::Application 5 | -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-bg_diagonals-thick_18_b81900_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-bg_diagonals-thick_18_b81900_40x40.png -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-bg_diagonals-thick_20_666666_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-bg_diagonals-thick_20_666666_40x40.png -------------------------------------------------------------------------------- /test_app/app/assets/images/ui-bg_highlight-soft_100_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasks2/best_in_place/master/test_app/app/assets/images/ui-bg_highlight-soft_100_eeeeee_1x100.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .bundle 3 | pkg/* 4 | 5 | Gemfile.lock 6 | test_app/.bundle 7 | test_app/db/*.sqlite3 8 | test_app/log/*.log 9 | test_app/tmp/**/* 10 | 11 | .rvmrc 12 | coverage 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core' 5 | require 'rspec/core/rake_task' 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task :default => :spec 9 | -------------------------------------------------------------------------------- /test_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | BipApp::Application.initialize! 6 | -------------------------------------------------------------------------------- /test_app/db/migrate/20111217215935_add_birth_date_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddBirthDateToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :birth_date, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test_app/db/migrate/20120616170454_add_money_proc_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddMoneyProcToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :money_proc, :float 4 | 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/best_in_place/railtie.rb: -------------------------------------------------------------------------------- 1 | module BestInPlace 2 | class Railtie < Rails::Railtie 3 | config.after_initialize do 4 | BestInPlace::ViewHelpers = ActionView::Base.new 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test_app/db/migrate/20111210084202_add_favorite_color_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddFavoriteColorToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :favorite_color, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test_app/db/migrate/20111210084251_add_favorite_books_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddFavoriteBooksToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :favorite_books, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test_app/db/migrate/20120607172609_add_favorite_movie_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddFavoriteMovieToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :favorite_movie, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test_app/db/migrate/20130213224102_add_favorite_locale_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddFavoriteLocaleToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :favorite_locale, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test_app/test/unit/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # Replace this with your real tests. 5 | test "the truth" do 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in best_in_place.gemspec 4 | gemspec 5 | 6 | gem 'sqlite3' 7 | gem 'jquery-rails' 8 | gem 'rdiscount' 9 | gem 'coveralls', require: false 10 | -------------------------------------------------------------------------------- /test_app/doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. 3 | -------------------------------------------------------------------------------- /test_app/lib/tasks/cron.rake: -------------------------------------------------------------------------------- 1 | desc "This task is called by the Heroku cron add-on" 2 | task :cron => :environment do 3 | if Time.now.hour == 0 # run at midnight 4 | Rake::Task["db:setup"].execute 5 | puts "db:setup performed" 6 | end 7 | end -------------------------------------------------------------------------------- /test_app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /test_app/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /test_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /test_app/db/migrate/20120513003308_create_cars.rb: -------------------------------------------------------------------------------- 1 | class CreateCars < ActiveRecord::Migration 2 | def up 3 | create_table :cars do |t| 4 | t.string :model 5 | end 6 | end 7 | 8 | def down 9 | drop_table :cars 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test_app/db/migrate/20101212170114_add_receive_email_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddReceiveEmailToUser < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :receive_email, :boolean 4 | end 5 | 6 | def self.down 7 | remove_column :users, :receive_email 8 | end 9 | end -------------------------------------------------------------------------------- /test_app/db/migrate/20110115204441_add_description_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionToUser < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :description, :text 4 | end 5 | 6 | def self.down 7 | remove_column :users, :description 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | 4 | env: "RAILS_ENV=test DISPLAY=:99.0" 5 | 6 | before_script: 7 | - "sh -c 'cd test_app && bundle && bundle exec rake db:drop db:migrate'" 8 | - "sh -e /etc/init.d/xvfb start" 9 | 10 | branches: 11 | only: 12 | - master 13 | - rails-3.0 14 | -------------------------------------------------------------------------------- /test_app/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | require 'rake' 6 | 7 | BipApp::Application.load_tasks 8 | -------------------------------------------------------------------------------- /lib/best_in_place/engine.rb: -------------------------------------------------------------------------------- 1 | module BestInPlace 2 | class Engine < Rails::Engine 3 | initializer "setup for rails" do 4 | ActionView::Base.send(:include, BestInPlace::BestInPlaceHelpers) 5 | ActionController::Base.send(:include, BestInPlace::ControllerExtensions) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test_app/test/performance/browsing_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rails/performance_test_help' 3 | 4 | # Profiling results for each test method are written to tmp/performance. 5 | class BrowsingTest < ActionDispatch::PerformanceTest 6 | def test_homepage 7 | get '/' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/retry_on_timeout.rb: -------------------------------------------------------------------------------- 1 | def retry_on_timeout(n = 3, &block) 2 | block.call 3 | rescue Capybara::TimeoutError, Capybara::ElementNotFound => e 4 | if n > 0 5 | puts "Catched error: #{e.message}. #{n-1} more attempts." 6 | retry_on_timeout(n - 1, &block) 7 | else 8 | raise 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/best_in_place/check_version.rb: -------------------------------------------------------------------------------- 1 | module BestInPlace 2 | module CheckVersion 3 | if Rails::VERSION::STRING < "3.1" 4 | raise "This version of Best in Place is intended to be used for Rails >= 3.1. If you want to use it with Rails 3.0 or lower, please use the rails-3.0 branch." 5 | return 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test_app/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /test_app/Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'rails', '3.2' 4 | gem 'sqlite3' 5 | 6 | gem 'best_in_place', :path => ".." 7 | 8 | gem 'jquery-rails' 9 | 10 | gem 'rdiscount' 11 | 12 | group :assets do 13 | gem 'sass-rails', '~> 3.2.3' 14 | gem 'coffee-rails', '~> 3.2.1' 15 | gem 'uglifier', '>= 1.0.3' 16 | end 17 | -------------------------------------------------------------------------------- /test_app/app/views/cuca/cars/show.html.erb: -------------------------------------------------------------------------------- 1 |

<%= notice %>

2 |
3 |

Car details

4 |

Click to edit

5 | 6 | 7 | 8 | 11 | 12 |
Model 9 | <%= best_in_place @car, :model %> 10 |
13 |
14 | -------------------------------------------------------------------------------- /test_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Best In Place TEST APP 5 | <%= stylesheet_link_tag "scaffold", "style", "jquery-ui-1.8.16.custom" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tag %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test_app/app/controllers/admin/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::UsersController < ApplicationController 2 | def show 3 | @user = User.find params[:id] 4 | end 5 | 6 | def update 7 | @user = User.find(params[:id]) 8 | 9 | respond_to do |format| 10 | @user.update_attributes(params[:user]) 11 | format.json { respond_with_bip(@user) } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | gemfile = File.expand_path('../../Gemfile', __FILE__) 5 | begin 6 | ENV['BUNDLE_GEMFILE'] = gemfile 7 | require 'bundler' 8 | Bundler.setup 9 | rescue Bundler::GemNotFound => e 10 | STDERR.puts e.message 11 | STDERR.puts "Try running `bundle install`." 12 | exit! 13 | end if File.exist?(gemfile) 14 | -------------------------------------------------------------------------------- /lib/best_in_place.rb: -------------------------------------------------------------------------------- 1 | require "best_in_place/check_version" 2 | require "best_in_place/utils" 3 | require "best_in_place/helper" 4 | require "best_in_place/engine" 5 | require "best_in_place/railtie" 6 | require "best_in_place/controller_extensions" 7 | require "best_in_place/display_methods" 8 | require "action_view" 9 | 10 | module BestInPlace 11 | autoload :TestHelpers, "best_in_place/test_helpers" 12 | end 13 | -------------------------------------------------------------------------------- /test_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | BipApp::Application.routes.draw do 2 | resources :users do 3 | member do 4 | put :test_respond_with 5 | get :double_init 6 | get :show_ajax 7 | get :email_field 8 | end 9 | end 10 | 11 | namespace :cuca do 12 | resources :cars 13 | end 14 | 15 | namespace :admin do 16 | resources :users 17 | end 18 | 19 | root :to => "users#index" 20 | end 21 | -------------------------------------------------------------------------------- /test_app/test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one: 4 | name: testy 5 | last_name: tester 6 | address: 123 main st 7 | email: testytester@example.com 8 | zip: 12345 9 | country: 1 10 | 11 | two: 12 | name: mystery 13 | last_name: man 14 | address: PO Box 0, Nowhere 15 | email: misterioso@example.com 16 | zip: 99999 17 | country: 18 | -------------------------------------------------------------------------------- /test_app/app/controllers/cuca/cars_controller.rb: -------------------------------------------------------------------------------- 1 | module Cuca 2 | class CarsController < ApplicationController 3 | def show 4 | @car = Car.find params[:id] 5 | end 6 | 7 | def update 8 | @car = Car.find params[:id] 9 | 10 | respond_to do |format| 11 | @car.update_attributes params[:cuca_car] 12 | format.json { respond_with_bip(@car) } 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test_app/app/views/users/show_ajax.html.erb: -------------------------------------------------------------------------------- 1 | <%= render :template => 'users/show' %> 2 | 13 | -------------------------------------------------------------------------------- /test_app/db/migrate/20101206205922_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def self.up 3 | create_table :users do |t| 4 | t.string :name 5 | t.string :last_name 6 | t.string :address 7 | t.string :email, :null => false 8 | t.string :zip 9 | t.string :country 10 | 11 | t.timestamps 12 | end 13 | end 14 | 15 | def self.down 16 | drop_table :users 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /test_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test_app/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | BipApp::Application.config.session_store :cookie_store, :key => '_bip_app_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rake db:sessions:create") 8 | # BipApp::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /lib/assets/javascripts/best_in_place.purr.js: -------------------------------------------------------------------------------- 1 | //= require jquery.purr 2 | 3 | jQuery(document).on('best_in_place:error', function(event, request, error) { 4 | // Display all error messages from server side validation 5 | jQuery.each(jQuery.parseJSON(request.responseText), function(index, value) { 6 | if( typeof(value) == "object") {value = index + " " + value.toString(); } 7 | var container = jQuery("").html(value); 8 | container.purr(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test_app/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. 7 | # 8 | # Note: You'll currently still have to declare fixtures explicitly in integration tests 9 | # -- they do not yet inherit this setting 10 | fixtures :all 11 | 12 | # Add more helper methods to be used by all tests here... 13 | end 14 | -------------------------------------------------------------------------------- /test_app/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | BipApp::Application.config.secret_token = '757d6525986af9c0cb75690db38ba36fd6c1b6f5badb7f586db2da390e87805099de9f365e4e9ebcbba7e2f5cada3f011d620af4a3610b0b96bb01c10175eb33' 8 | -------------------------------------------------------------------------------- /test_app/app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | #encoding: utf-8 2 | module UsersHelper 3 | def height_collection 4 | [ 5 | %{5' 1"} , 6 | %{5' 2"} , 7 | %{5' 3"} , 8 | %{5' 4"} , 9 | %{5' 5"} , 10 | %{5' 6"} , 11 | %{5' 7"} , 12 | %{5' 8"} , 13 | %{5' 9"} , 14 | %{5' 10"}, 15 | %{5' 11"}, 16 | %{6' 0"} , 17 | %{6' 1"} , 18 | %{6' 2"} , 19 | %{6' 3"} , 20 | %{6' 4"} , 21 | %{6' 5"} , 22 | %{6' 6"} 23 | ] 24 | end 25 | 26 | def bb(value) 27 | "#{value} €" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test_app/app/views/admin/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 | 11 | 12 | 13 | 14 | 17 | 18 |
9 | <%= best_in_place [:admin, @user], :name %> 10 |
Last Name 15 | <%= best_in_place [:admin, @user], :last_name, :nil => "Nothing to show" %> 16 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /test_app/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3-ruby (not necessary on OS X Leopard) 3 | development: 4 | adapter: sqlite3 5 | database: db/development.sqlite3 6 | pool: 5 7 | timeout: 5000 8 | 9 | # Warning: The database defined as "test" will be erased and 10 | # re-generated from your development database when you run "rake". 11 | # Do not set this db to the same as development or production. 12 | test: 13 | adapter: sqlite3 14 | database: db/test.sqlite3 15 | pool: 5 16 | timeout: 5000 17 | 18 | production: 19 | adapter: sqlite3 20 | database: db/production.sqlite3 21 | pool: 5 22 | timeout: 5000 23 | -------------------------------------------------------------------------------- /lib/best_in_place/utils.rb: -------------------------------------------------------------------------------- 1 | module BestInPlace 2 | module Utils 3 | extend self 4 | 5 | def build_best_in_place_id(object, field) 6 | if object.is_a?(Symbol) || object.is_a?(String) 7 | return "best_in_place_#{object}_#{field}" 8 | end 9 | 10 | id = "best_in_place_#{object_to_key(object)}" 11 | id << "_#{object.id}" if object.class.ancestors.include?(ActiveModel::Serializers::JSON) 12 | id << "_#{field}" 13 | id 14 | end 15 | 16 | def object_to_key(object) 17 | return object.class.to_s.underscore unless object.class.respond_to?(:model_name) 18 | ActiveModel::Naming.param_key(object.class) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Envinronment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require File.expand_path('../../test_app/config/environment', __FILE__) 5 | require 'coveralls' 6 | require "rspec/rails" 7 | require "nokogiri" 8 | 9 | Coveralls.wear! 10 | 11 | # Load support files 12 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each{|f| require f} 13 | 14 | RSpec.configure do |config| 15 | # Remove this line if you don't want RSpec's should and should_not 16 | # methods or matchers 17 | require 'rspec/expectations' 18 | 19 | config.include RSpec::Matchers 20 | config.include BestInPlace::TestHelpers 21 | 22 | # == Mock Framework 23 | config.mock_with :rspec 24 | end 25 | 26 | Capybara.default_wait_time = 5 27 | -------------------------------------------------------------------------------- /test_app/app/views/users/index.html.erb: -------------------------------------------------------------------------------- 1 |

Listing users

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <% @users.each do |user| %> 11 | 12 | 18 | 19 | 20 | 21 | 22 | <% end %> 23 |
NameLast nameCountry
13 | <%= best_in_place user, :name, 14 | activator: "#edit_#{user.id}", 15 | display_with: :link_to, 16 | helper_options: user_path(user) %> 17 | edit<%= user.last_name %><%= COUNTRIES[user.country.to_i] %>
24 | 25 |
26 | -------------------------------------------------------------------------------- /test_app/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test_app/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test_app/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |

We've been notified about this issue and we'll take a look at it shortly.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/best_in_place/controller_extensions.rb: -------------------------------------------------------------------------------- 1 | module BestInPlace 2 | module ControllerExtensions 3 | def respond_with_bip(obj) 4 | obj.changed? ? respond_bip_error(obj) : respond_bip_ok(obj) 5 | end 6 | 7 | private 8 | def respond_bip_ok(obj) 9 | if obj.respond_to?(:id) 10 | klass = "#{obj.class}_#{obj.id}" 11 | else 12 | klass = obj.class.to_s 13 | end 14 | param_key = BestInPlace::Utils.object_to_key(obj) 15 | updating_attr = params[param_key].keys.first 16 | 17 | if renderer = BestInPlace::DisplayMethods.lookup(klass, updating_attr) 18 | render :json => renderer.render_json(obj) 19 | else 20 | head :no_content 21 | end 22 | end 23 | 24 | def respond_bip_error(obj) 25 | render :json => obj.errors.full_messages, :status => :unprocessable_entity 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | BipApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the webserver when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | end 25 | 26 | -------------------------------------------------------------------------------- /test_app/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | validates :name, 3 | :length => { :minimum => 2, :maximum => 24, :message => "has invalid length"}, 4 | :presence => {:message => "can't be blank"} 5 | validates :last_name, 6 | :length => { :minimum => 2, :maximum => 50, :message => "has invalid length"}, 7 | :presence => {:message => "can't be blank"} 8 | validates :address, 9 | :length => { :minimum => 5, :message => "too short length"}, 10 | :presence => {:message => "can't be blank"} 11 | validates :email, 12 | :presence => {:message => "can't be blank"}, 13 | :format => {:with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :message => "has wrong email format"} 14 | validates :zip, :numericality => true, :length => { :minimum => 5 } 15 | validates_numericality_of :money, :allow_blank => true 16 | validates_numericality_of :money_proc, :allow_blank => true 17 | 18 | alias_attribute :money_custom, :money 19 | 20 | def address_format 21 | "addr => [#{address}]".html_safe 22 | end 23 | 24 | def markdown_desc 25 | RDiscount.new(description).to_html.html_safe 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/integration/text_area_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 | :description => "User description" 15 | end 16 | 17 | it "should be able to use bip_text to update a text area" do 18 | @user.save! 19 | visit user_path(@user) 20 | within("#description") do 21 | page.should have_content("User description") 22 | end 23 | 24 | bip_area @user, :description, "A new description" 25 | 26 | visit user_path(@user) 27 | within("#description") do 28 | page.should have_content("A new description") 29 | end 30 | end 31 | 32 | it "should be able to use a bip_text with :display_with option" do 33 | @user.description = "I'm so awesome" 34 | @user.save! 35 | visit user_path(@user) 36 | within("#dw_description") do 37 | page.should have_content("I'm so awesome") 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test_app/app/assets/stylesheets/scaffold.css: -------------------------------------------------------------------------------- 1 | body { background-color: #fff; color: #333; } 2 | 3 | body, p, ol, ul, td { 4 | font-family: verdana, arial, helvetica, sans-serif; 5 | font-size: 13px; 6 | line-height: 18px; 7 | } 8 | 9 | pre { 10 | background-color: #eee; 11 | padding: 10px; 12 | font-size: 11px; 13 | } 14 | 15 | a { color: #000; } 16 | a:visited { color: #666; } 17 | a:hover { color: #fff; background-color:#000; } 18 | 19 | div.field, div.actions { 20 | margin-bottom: 10px; 21 | } 22 | 23 | #notice { 24 | color: green; 25 | } 26 | 27 | .field_with_errors { 28 | padding: 2px; 29 | background-color: red; 30 | display: table; 31 | } 32 | 33 | #error_explanation { 34 | width: 450px; 35 | border: 2px solid red; 36 | padding: 7px; 37 | padding-bottom: 0; 38 | margin-bottom: 20px; 39 | background-color: #f0f0f0; 40 | } 41 | 42 | #error_explanation h2 { 43 | text-align: left; 44 | font-weight: bold; 45 | padding: 5px 5px 5px 15px; 46 | font-size: 12px; 47 | margin: -7px; 48 | margin-bottom: 0px; 49 | background-color: #c00; 50 | color: #fff; 51 | } 52 | 53 | #error_explanation ul li { 54 | font-size: 12px; 55 | list-style: square; 56 | } 57 | 58 | span.nil { 59 | color: #999; 60 | } -------------------------------------------------------------------------------- /test_app/test/functional/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersControllerTest < ActionController::TestCase 4 | setup do 5 | @user = users(:one) 6 | end 7 | 8 | test "should get index" do 9 | get :index 10 | assert_response :success 11 | assert_not_nil assigns(:users) 12 | end 13 | 14 | test "should get new" do 15 | get :new 16 | assert_response :success 17 | end 18 | 19 | test "should create user" do 20 | assert_difference('User.count') do 21 | post :create, :user => @user.attributes 22 | end 23 | 24 | assert_redirected_to user_path(assigns(:user)) 25 | end 26 | 27 | test "should show user" do 28 | get :show, :id => @user.to_param 29 | assert_response :success 30 | end 31 | 32 | test "should get edit" do 33 | get :edit, :id => @user.to_param 34 | assert_response :success 35 | end 36 | 37 | test "should update user" do 38 | put :update, :id => @user.to_param, :user => @user.attributes 39 | assert_redirected_to user_path(assigns(:user)) 40 | end 41 | 42 | test "should destroy user" do 43 | assert_difference('User.count', -1) do 44 | delete :destroy, :id => @user.to_param 45 | end 46 | 47 | assert_redirected_to users_path 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test_app/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery-ui 3 | //= require best_in_place 4 | //= require best_in_place.purr 5 | //= require_self 6 | 7 | $(document).ready(function() { 8 | /* Activating Best In Place */ 9 | jQuery(".best_in_place").best_in_place(); 10 | }); 11 | 12 | /* Inicialització en català per a l'extenció 'calendar' per jQuery. */ 13 | /* Writers: (joan.leon@gmail.com). */ 14 | jQuery(function($){ 15 | $.datepicker.regional['ca'] = { 16 | closeText: 'Tancar', 17 | prevText: '<Ant', 18 | nextText: 'Seg>', 19 | currentText: 'Avui', 20 | monthNames: ['Gener','Febrer','Març','Abril','Maig','Juny', 21 | 'Juliol','Agost','Setembre','Octubre','Novembre','Desembre'], 22 | monthNamesShort: ['Gen','Feb','Mar','Abr','Mai','Jun', 23 | 'Jul','Ago','Set','Oct','Nov','Des'], 24 | dayNames: ['Diumenge','Dilluns','Dimarts','Dimecres','Dijous','Divendres','Dissabte'], 25 | dayNamesShort: ['Dug','Dln','Dmt','Dmc','Djs','Dvn','Dsb'], 26 | dayNamesMin: ['Dg','Dl','Dt','Dc','Dj','Dv','Ds'], 27 | weekHeader: 'Sm', 28 | dateFormat: 'dd-mm-yy', 29 | firstDay: 1, 30 | isRTL: false, 31 | showMonthAfterYear: false, 32 | yearSuffix: ''}; 33 | $.datepicker.setDefaults($.datepicker.regional['ca']); 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /test_app/app/views/users/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(@user) do |f| %> 2 | <% if @user.errors.any? %> 3 |
4 |

<%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:

5 | 6 | 11 |
12 | <% end %> 13 | 14 |
15 | <%= f.label :name %>
16 | <%= f.text_field :name %> 17 |
18 |
19 | <%= f.label :last_name %>
20 | <%= f.text_field :last_name %> 21 |
22 |
23 | <%= f.label :height %>
24 | <%= f.select :height, height_collection %> 25 |
26 |
27 | <%= f.label :address %>
28 | <%= f.text_field :address %> 29 |
30 |
31 | <%= f.label :email %>
32 | <%= f.text_field :email %> 33 |
34 |
35 | <%= f.label :zip %>
36 | <%= f.text_field :zip %> 37 |
38 |
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 | 9 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 60 | 61 |
Name (edit me) 10 | <%= best_in_place @user, :name, :type => :input, :activator => "#editme" %> 11 |
Last Name 16 | <%= best_in_place @user, :last_name, :nil => "Nothing to show", :path => test_respond_with_user_path(@user) %> 17 |
Email 22 | <%= best_in_place @user, :email %> 23 |
Address 28 | <%= best_in_place @user, :address %> 29 |
ZIP 34 | <%= best_in_place @user, :zip %> 35 |
Country 40 | <%= best_in_place @user, :country, :type => :select, :collection => @countries %> 41 |
Receive newsletter? 46 | <%= best_in_place @user, :receive_email, :type => :checkbox, :collection => ["No thanks", "Yes of course"] %> 47 |
User description 52 | <%= best_in_place @user, :description, :type => :textarea, :sanitize => false %> 53 |
Alternative Money 58 | <%= best_in_place @user, :money, :display_with => :number_to_currency, :helper_options => {:unit => "€"} %> 59 |
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 | 9 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | 92 | 95 | 96 | 97 | 98 | 101 | 102 | 103 | 104 | 107 | 108 | 109 | 110 | 113 | 114 | 115 | 116 | 119 | 120 | 121 | 122 | 127 | 128 |
Name (edit me) 10 | <%= best_in_place @user, :name, :type => :input, :activator => "#editme" %> 11 |
Last Name 16 | <%= best_in_place @user, :last_name, :nil => "Nothing to show" %> 17 |
Height 22 | <%= best_in_place @user, :height, :type => :select, :collection => height_collection.zip(height_collection), :sanitize => false %> 23 |
Email 28 | <%= best_in_place @user, :email %> 29 |
Birth date 34 | <%= best_in_place @user, :birth_date, :type => :date %> 35 |
Address 40 | <%= best_in_place @user, :address, :display_as => :address_format %> 41 |
ZIP 46 | <%= best_in_place @user, :zip %> 47 |
Country 52 | <%= best_in_place @user, :country, :type => :select, :collection => @countries, :inner_class => :some_class %> 53 |
Receive newsletter? 58 | <%= best_in_place @user, :receive_email, :type => :checkbox, :collection => ["No thanks", "Yes of course"] %> 59 |
Receive newsletter (image)? 64 | <%= best_in_place @user, :receive_email, :type => :checkbox, :collection => [image_tag('no.png'), image_tag('yes.png')] %> 65 |
Favorite color 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 |
Favorite locale 79 | <%= best_in_place @user, :favorite_locale, :nil => "N/A" %> 80 |
Favorite books 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 |
User description 93 | <%= best_in_place @user, :description, :display_as => :markdown_desc, :type => :textarea, :sanitize => false %> 94 |
Simple-formatted user description 99 | <%= best_in_place @user, :description, :display_with => :simple_format, :type => :textarea %> 100 |
Money 105 | <%= best_in_place @user, :money, :display_with => :number_to_currency %> 106 |
Money with proc 111 | <%= best_in_place @user, :money_proc, :display_with => lambda{ |v| v.blank? ? "No money" : number_to_currency(v) } %> 112 |
Money with custom helper 117 | <%= best_in_place @user, :money_custom, :display_with => lambda { |x| bb(x) } %> 118 |
Favorite Movie 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 |
129 |
130 |
131 |

Try the features of Best In Place:

132 | 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 | [![Build Status](https://secure.travis-ci.org/bernat/best_in_place.png)](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 | --------------------------------------------------------------------------------