├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README ├── README.textile ├── Rakefile ├── TUTORIAL.textile ├── app ├── controllers │ ├── application_controller.rb │ ├── home_controller.rb │ ├── sites_controller.rb │ ├── subdomains_controller.rb │ └── users_controller.rb ├── helpers │ ├── application_helper.rb │ ├── home_helper.rb │ ├── sites_helper.rb │ ├── subdomains_helper.rb │ ├── url_helper.rb │ └── users_helper.rb ├── models │ ├── site.rb │ ├── subdomain.rb │ └── user.rb └── views │ ├── devise │ ├── confirmations │ │ └── new.html.erb │ ├── mailer │ │ ├── confirmation_instructions.html.erb │ │ ├── reset_password_instructions.html.erb │ │ └── unlock_instructions.html.erb │ ├── menu │ │ ├── _login_items.html.erb │ │ └── _registration_items.html.erb │ ├── passwords │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── registrations │ │ ├── edit.html.erb │ │ └── new.html.erb │ ├── sessions │ │ └── new.html.erb │ ├── shared │ │ └── _links.erb │ └── unlocks │ │ └── new.html.erb │ ├── home │ └── index.html.erb │ ├── layouts │ └── application.html.erb │ ├── sites │ ├── index.html.erb │ └── opps.html.erb │ ├── subdomains │ ├── _form.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb │ └── users │ ├── index.html.erb │ └── show.html.erb ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── backtrace_silencers.rb │ ├── devise.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── secret_token.rb │ └── session_store.rb ├── locales │ ├── devise.en.yml │ └── en.yml └── routes.rb ├── db ├── migrate │ ├── 20100807190405_create_slugs.rb │ ├── 20100808194405_devise_create_users.rb │ ├── 20100808194652_create_subdomains.rb │ └── 20100930104337_migrate_subdomain_to_integer.rb ├── schema.rb └── seeds.rb ├── doc └── README_FOR_APP ├── lib ├── subdomain_route.rb └── tasks │ └── .gitkeep ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico ├── images │ └── rails.png ├── javascripts │ ├── application.js │ ├── controls.js │ ├── dragdrop.js │ ├── effects.js │ ├── prototype.js │ └── rails.js ├── robots.txt └── stylesheets │ ├── .gitkeep │ └── application.css ├── script └── rails ├── template.rb ├── test ├── fixtures │ ├── subdomains.yml │ └── users.yml ├── functional │ ├── home_controller_test.rb │ ├── sites_controller_test.rb │ ├── subdomains_controller_test.rb │ └── users_controller_test.rb ├── performance │ └── browsing_test.rb ├── test_helper.rb └── unit │ ├── helpers │ ├── home_helper_test.rb │ ├── sites_helper_test.rb │ ├── subdomains_helper_test.rb │ └── users_helper_test.rb │ ├── subdomain_test.rb │ └── user_test.rb └── vendor └── plugins └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | db/*.sqlite3 3 | log/*.log 4 | tmp/**/* 5 | .DS_Store 6 | .redcar 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | # Uncomment the following to use the GitHub master (unreleased) version of Rails 3 4 | # gem 'rails', :git => 'git://github.com/rails/rails.git' 5 | # Or use the Rails 3 release candidate 6 | gem 'rails', '3.0.0' 7 | gem 'sqlite3-ruby', :require => 'sqlite3' 8 | gem 'devise', '1.1.3' 9 | gem 'friendly_id', '3.1.7' 10 | # uncomment the next line if you wish to deploy to Heroku 11 | # gem 'heroku', '1.10.8', :group => :development 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | abstract (1.0.0) 5 | actionmailer (3.0.0) 6 | actionpack (= 3.0.0) 7 | mail (~> 2.2.5) 8 | actionpack (3.0.0) 9 | activemodel (= 3.0.0) 10 | activesupport (= 3.0.0) 11 | builder (~> 2.1.2) 12 | erubis (~> 2.6.6) 13 | i18n (~> 0.4.1) 14 | rack (~> 1.2.1) 15 | rack-mount (~> 0.6.12) 16 | rack-test (~> 0.5.4) 17 | tzinfo (~> 0.3.23) 18 | activemodel (3.0.0) 19 | activesupport (= 3.0.0) 20 | builder (~> 2.1.2) 21 | i18n (~> 0.4.1) 22 | activerecord (3.0.0) 23 | activemodel (= 3.0.0) 24 | activesupport (= 3.0.0) 25 | arel (~> 1.0.0) 26 | tzinfo (~> 0.3.23) 27 | activeresource (3.0.0) 28 | activemodel (= 3.0.0) 29 | activesupport (= 3.0.0) 30 | activesupport (3.0.0) 31 | arel (1.0.1) 32 | activesupport (~> 3.0.0) 33 | babosa (0.2.0) 34 | bcrypt-ruby (2.1.2) 35 | builder (2.1.2) 36 | devise (1.1.3) 37 | bcrypt-ruby (~> 2.1.2) 38 | warden (~> 0.10.7) 39 | erubis (2.6.6) 40 | abstract (>= 1.0.0) 41 | friendly_id (3.1.7) 42 | babosa (~> 0.2.0) 43 | i18n (0.4.1) 44 | mail (2.2.6.1) 45 | activesupport (>= 2.3.6) 46 | mime-types 47 | treetop (>= 1.4.5) 48 | mime-types (1.16) 49 | polyglot (0.3.1) 50 | rack (1.2.1) 51 | rack-mount (0.6.13) 52 | rack (>= 1.0.0) 53 | rack-test (0.5.6) 54 | rack (>= 1.0) 55 | rails (3.0.0) 56 | actionmailer (= 3.0.0) 57 | actionpack (= 3.0.0) 58 | activerecord (= 3.0.0) 59 | activeresource (= 3.0.0) 60 | activesupport (= 3.0.0) 61 | bundler (~> 1.0.0) 62 | railties (= 3.0.0) 63 | railties (3.0.0) 64 | actionpack (= 3.0.0) 65 | activesupport (= 3.0.0) 66 | rake (>= 0.8.4) 67 | thor (~> 0.14.0) 68 | rake (0.8.7) 69 | sqlite3-ruby (1.3.1) 70 | thor (0.14.2) 71 | treetop (1.4.8) 72 | polyglot (>= 0.3.1) 73 | tzinfo (0.3.23) 74 | warden (0.10.7) 75 | rack (>= 1.0.0) 76 | 77 | PLATFORMS 78 | ruby 79 | 80 | DEPENDENCIES 81 | devise (= 1.1.3) 82 | friendly_id (= 3.1.7) 83 | rails (= 3.0.0) 84 | sqlite3-ruby 85 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Rails3-Subdomain-Devise 2 | ======================== 3 | 4 | Use this project as a starting point for a Rails 3 application that uses subdomains and authentication. User management and authentication is implemented using Devise http://github.com/plataformatec/devise. 5 | ________________________ 6 | 7 | Tutorial 8 | 9 | A complete "walkthrough" tutorial is available on the GitHub wiki: 10 | 11 | http://wiki.github.com/fortuity/rails3-subdomain-devise/tutorial-walkthrough 12 | 13 | ________________________ 14 | 15 | See the README file on GitHub 16 | 17 | For more information, please see the updated README file on GitHub: 18 | 19 | http://github.com/fortuity/rails3-subdomain-devise 20 | 21 | ________________________ 22 | 23 | Public Domain Dedication 24 | 25 | This work is a compilation and derivation from other previously released works. With the exception of various included works, which may be restricted by other licenses, the author or authors of this code dedicate any and all copyright interest in this code to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this code under copyright law. 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. Fork of Rails3-Subdomain-Devise modified for basecamp-like subdomaims 2 | 3 | h4. Note 4 | 5 | _This was the initial version of implementing subdomains using a BaseCamp like approach. While it is functional, I really didn't get everything quite right. Quite right is really up to your application. I have posted another repository that cleans up a few thing and adds Devise Invitable to implement invitations and CanCan to do some basic authorization. Pick your poison! "rails3-Devise-BCSD-Can-Invite":http://github.com/salex/rails3-Devise-BCSD-Can-Invite._ 6 | 7 | Use this project as a starting point for a Rails 3 application that uses basecamp-like subdomains and authentication. User management and authentication is implemented using "Devise":http://github.com/plataformatec/devise. 8 | 9 | This is a stab of modifying "Rails3-Subdomain-Devise":http://github.com/fortuity/rails3-subdomain-devise/ to provide basecamp-like subdomains. 10 | 11 | Thanks to "babinho's fork":http://github.com/babinho/rails3-subdomain-devise/ for cleaning up some things up and going back to a subdomain id instead of name as the primary key. 12 | 13 | The major changes are: 14 | 15 | h3. Routing 16 | 17 | Routes and associations were modified: 18 | 19 |
 20 |   devise_for :users
 21 |   resources :users, :only => [:index, :show]   do
 22 |     member do
 23 |       get :valid
 24 |     end
 25 |   end
 26 |   resources :subdomains, :only => [:index, :show]
 27 |   constraints(SubdomainRoute) do
 28 |     match '/' => 'sites#index'
 29 |     match '/opps' => 'sites#opps'
 30 |   end
 31 |   
 32 |   root :to => "home#index"
 33 | 
34 | 35 | User and subdomains are no longer nested, but associated. 36 | 37 |

 38 | 	class Subdomain < ActiveRecord::Base
 39 | 	  has_many :users
 40 | 	  validates_uniqueness_of :name, :case_sensitive => false
 41 | 	  validates_presence_of :name
 42 | 	end
 43 | 	
 44 | 	class User < ActiveRecord::Base
 45 | 	  belongs_to :subdomain
 46 | 	  devise :database_authenticatable, :registerable,
 47 | 	         :recoverable, :rememberable, :trackable, :validatable
 48 | 	  validates_presence_of :name
 49 | 	  validates_presence_of :subdomain_name, :on => :create # used to create a subdomain
 50 | 	  validates_uniqueness_of  :email, :case_sensitive => false
 51 | 	  attr_accessor :subdomain_name  # used to create a subdomain
 52 | 	  attr_accessible :name, :subdomain_name, :email, :password, :password_confirmation, :loginable_token
 53 | 	  before_create :create_subdomain
 54 | 	  after_create :update_subdomain_owner
 55 | 
 56 | 	  def self.valid?(params)
 57 | 	    token_user = self.where(:loginable_token => params[:id]).first
 58 | 	    if token_user
 59 | 	      token_user.loginable_token = nil
 60 | 	      token_user.save
 61 | 	    end
 62 | 	    return token_user
 63 | 	  end
 64 | 
 65 | 	  private
 66 | 
 67 | 	  def create_subdomain
 68 | 	    # get or create a subdomain on creating a new user
 69 | 	    self.subdomain = Subdomain.find_by_name(self.subdomain_name) 
 70 | 	    self.subdomain ||= Subdomain.create!(:name => self.subdomain_name)
 71 | 	  end
 72 | 
 73 | 	  def update_subdomain_owner
 74 | 	    # set owner of subdomain to user that created it
 75 | 	    subdomain = self.subdomain
 76 | 	    if subdomain && subdomain.user_id.nil?
 77 | 	      subdomain.user_id = self.id
 78 | 	      subdomain.save
 79 | 	    end
 80 | 	  end
 81 | 
 82 | 	end
 83 | 	
 84 | 	
 85 | 
86 | 87 | The majority of the subdomains controller has been stubbed in that "sign_up" will create a subdomain if it does not exist. Code was not removed in that an Admin user might want to delete subdomains and associated users. 88 | 89 | The model names were not changed, but take on different roles. List of other changes: 90 | 91 | * Users#index probably should not be there, but it lists all users 92 | * Site would be the home for the site and currently list users for the subdomain 93 | * Subdomain still has user_id and indicates who created the subdomain (assume subdomain admin) 94 | * A helper method "current_subdomain" was added to controllers/application.rb to check if subdomain exists 95 | * Another helper method "check_my_domain(subdomain)" will check a passed subdomain against the current domain. Subdomain is kind of the root table, all major tables should be belong to subdomain and this check is there to prevent url modification (edit member not belonging to your domain). It will redirect to an "opps" action in the site controller. 96 | * Sign-in has been removed if no subdomain exists. If you modifiy the url and sign_in without a subdomain. It will log you in, but then immediately log you out and redirect to the subdomain sign_in form. I can't seem to get the flash notice to work in this area. 97 | * If you sign-in to the root domain. 98 | ** A token is saved in the users record. You are signed_out of the root domain and redirected a user/valid action in the subdomin. 99 | ** The valid action will check the token and if found, will clear the toke and sign-in the user to the subdomain. 100 | * If you register without a subdomain, it will be created when the user is created. If it exists, you are added as a user of that subdomain (TODO this should be fixed to reject adding user from sign_up without subdomain, if it exists) 101 | * If you register in a subdomain, you are added as a the users of that subdomain. 102 | 103 | 104 | P.S. I am not a novice at Rails, but don't consider myself experienced. 105 | 106 | Installation 107 | 108 | * git clone 109 | * bundle install 110 | * rake db:create 111 | * rake db:migrate 112 | * rake db:seed 113 | 114 | 115 | h1. Rails3-Subdomain-Devise 116 | 117 | Please visit "Rails3-Subdomain-Devise":http://github.com/fortuity/rails3-subdomain-devise for the excellent step-by-step tutorial. You should still be able to follow the tutorial for this fork if you address the above changes. 118 | 119 | h1. Forks 120 | 121 | Version (fork) that uses Mongodb with Mongoid by millisami at "http://github.com/millisami/rails3-subdomain-devise":http://github.com/millisami/rails3-subdomain-devise 122 | 123 | h2. License 124 | 125 | h3. Public Domain Dedication 126 | 127 | This work is a compilation and derivation from other previously released works. With the exception of various included works, which may be restricted by other licenses, the author or authors of this code dedicate any and all copyright interest in this code to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this code under copyright law. 128 | -------------------------------------------------------------------------------- /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 | Rails3SubdomainDevise::Application.load_tasks 8 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include UrlHelper 3 | protect_from_forgery 4 | helper_method :current_subdomain, :check_my_subdomain 5 | before_filter :current_subdomain 6 | before_filter :set_mailer_url_options 7 | 8 | 9 | def current_subdomain 10 | if request.subdomains.first.present? && request.subdomains.first != "www" 11 | current_subdomain = Subdomain.find_by_name(request.subdomains.first) 12 | else 13 | current_subdomain = nil 14 | end 15 | return current_subdomain 16 | end 17 | 18 | def check_my_subdomain(subdomain) 19 | if subdomain != current_subdomain.name 20 | redirect_to "/opps" , :alert => "Sorry, resource is not part of your subdomain" 21 | end 22 | end 23 | 24 | 25 | def after_sign_in_path_for(resource_or_scope) 26 | scope = Devise::Mapping.find_scope!(resource_or_scope) 27 | subdomain_name = current_user.subdomain.name 28 | if current_subdomain.nil? 29 | # logout of root domain and login by token to subdomain 30 | token = Devise.friendly_token 31 | current_user.loginable_token = token 32 | current_user.save 33 | sign_out(current_user) 34 | flash[:notice] = nil 35 | home_path = valid_user_url(token, :subdomain => subdomain_name) 36 | return home_path 37 | else 38 | if subdomain_name != current_subdomain.name 39 | # user not part of current_subdomain 40 | sign_out(current_user) 41 | flash[:notice] = nil 42 | flash[:alert] = "Sorry, invalid user or password for subdomain" 43 | end 44 | end 45 | super 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | def index 3 | end 4 | 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/sites_controller.rb: -------------------------------------------------------------------------------- 1 | class SitesController < ApplicationController 2 | 3 | def index 4 | @site = Site.find_by_name(current_subdomain.name) 5 | end 6 | 7 | def opps 8 | @site = Site.find_by_name(current_subdomain.name) 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/subdomains_controller.rb: -------------------------------------------------------------------------------- 1 | class SubdomainsController < ApplicationController 2 | before_filter :authenticate_user!, :except => [:index, :show] 3 | before_filter :find_user, :except => [:index, :show] 4 | respond_to :html 5 | 6 | def index 7 | @subdomains = Subdomain.all 8 | respond_with(@subdomains) 9 | end 10 | 11 | def show 12 | @subdomain = Subdomain.find(params[:id]) 13 | @admin = User.find(@subdomain.user_id) 14 | respond_with(@subdomain) 15 | end 16 | 17 | def new 18 | @subdomain = Subdomain.new(:user => @user) 19 | respond_with(@subdomain) 20 | end 21 | 22 | def create 23 | @subdomain = Subdomain.new(params[:subdomain]) 24 | if @subdomain.save 25 | flash[:notice] = "Successfully created subdomain." 26 | end 27 | redirect_to @user 28 | end 29 | 30 | def edit 31 | @subdomain = Subdomain.find(params[:id]) 32 | respond_with(@subdomain) 33 | end 34 | 35 | def update 36 | @subdomain = Subdomain.find(params[:id]) 37 | if @subdomain.update_attributes(params[:subdomain]) 38 | flash[:notice] = "Successfully updated subdomain." 39 | end 40 | respond_with(@subdomain) 41 | end 42 | 43 | def destroy 44 | @subdomain = Subdomain.find(params[:id]) 45 | @subdomain.destroy 46 | flash[:notice] = "Successfully destroyed subdomain." 47 | redirect_to @user 48 | end 49 | 50 | protected 51 | 52 | def find_user 53 | if params[:user_id] 54 | @user = User.find(params[:user_id]) 55 | else 56 | @subdomain = Subdomain.find(params[:id]) 57 | @user = @subdomain.user 58 | end 59 | unless current_user == @user 60 | redirect_to @user, :alert => "Are you logged in properly? You are not allowed to create or change someone else's subdomain." 61 | end 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | 3 | def index 4 | @users = current_subdomain.nil? ? User.all : current_subdomain.users 5 | end 6 | 7 | def show 8 | @user = User.find(params[:id]) 9 | if !current_subdomain.nil? 10 | check_my_subdomain(@user.subdomain.name) 11 | end 12 | end 13 | 14 | def valid 15 | token_user = User.valid?(params) 16 | if token_user 17 | sign_in(:user, token_user) 18 | flash[:notice] = "You have been logged in" 19 | else 20 | flash[:alert] = "Login could not be validated" 21 | end 22 | redirect_to :root 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | 3 | end 4 | -------------------------------------------------------------------------------- /app/helpers/home_helper.rb: -------------------------------------------------------------------------------- 1 | module HomeHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/sites_helper.rb: -------------------------------------------------------------------------------- 1 | module SitesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/subdomains_helper.rb: -------------------------------------------------------------------------------- 1 | module SubdomainsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/url_helper.rb: -------------------------------------------------------------------------------- 1 | module UrlHelper 2 | def with_subdomain(subdomain) 3 | 4 | subdomain = (subdomain || "") 5 | subdomain += "." unless subdomain.empty? 6 | [subdomain, request.domain, request.port_string].join 7 | end 8 | 9 | def url_for(options = nil) 10 | if options.kind_of?(Hash) && options.has_key?(:subdomain) 11 | options[:host] = with_subdomain(options.delete(:subdomain)) 12 | end 13 | super 14 | end 15 | def set_mailer_url_options 16 | ActionMailer::Base.default_url_options[:host] = with_subdomain(request.subdomains.first) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/models/site.rb: -------------------------------------------------------------------------------- 1 | class Site < Subdomain 2 | end 3 | -------------------------------------------------------------------------------- /app/models/subdomain.rb: -------------------------------------------------------------------------------- 1 | class Subdomain < ActiveRecord::Base 2 | has_many :users 3 | validates_uniqueness_of :name, :case_sensitive => false 4 | validates_presence_of :name 5 | end 6 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | belongs_to :subdomain 3 | devise :database_authenticatable, :registerable, 4 | :recoverable, :rememberable, :trackable, :validatable 5 | validates_presence_of :name 6 | validates_presence_of :subdomain_name, :on => :create # used to create a subdomain 7 | validates_uniqueness_of :email, :case_sensitive => false 8 | attr_accessor :subdomain_name # used to create a subdomain 9 | attr_accessible :name, :subdomain_name, :email, :password, :password_confirmation, :loginable_token 10 | before_create :create_subdomain 11 | after_create :update_subdomain_owner 12 | 13 | def self.valid?(params) 14 | token_user = self.where(:loginable_token => params[:id]).first 15 | if token_user 16 | token_user.loginable_token = nil 17 | token_user.save 18 | end 19 | return token_user 20 | end 21 | 22 | private 23 | 24 | def create_subdomain 25 | # get or create a subdomain on creating a new user 26 | self.subdomain = Subdomain.find_by_name(self.subdomain_name) 27 | self.subdomain ||= Subdomain.create!(:name => self.subdomain_name) 28 | end 29 | 30 | def update_subdomain_owner 31 | # set owner of subdomain to user that created it 32 | subdomain = self.subdomain 33 | if subdomain && subdomain.user_id.nil? 34 | subdomain.user_id = self.id 35 | subdomain.save 36 | end 37 | end 38 | 39 | end 40 | 41 | 42 | # def self.find_for_authentication(conditions={}) 43 | # #conditions[:active] = true 44 | # logger.info conditions.inspect 45 | # super 46 | # end 47 | -------------------------------------------------------------------------------- /app/views/devise/confirmations/new.html.erb: -------------------------------------------------------------------------------- 1 |

Resend confirmation instructions

2 | 3 | <%= form_for(resource, :as => resource_name, :url => confirmation_path(resource_name), :html => { :method => :post }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |

<%= f.label :email %>
7 | <%= f.text_field :email %>

8 | 9 |

<%= f.submit "Resend confirmation instructions" %>

10 | <% end %> 11 | 12 | <%= render :partial => "devise/shared/links" %> -------------------------------------------------------------------------------- /app/views/devise/mailer/confirmation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome <%= @resource.email %>!

2 | 3 |

You can confirm your account through the link below:

4 | 5 |

<%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %>

6 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Someone has requested a link to change your password, and you can do this through the link below.

4 | 5 |

<%= link_to 'Change my password', edit_password_url(@resource, :reset_password_token => @resource.reset_password_token) %>

6 | 7 |

If you didn't request this, please ignore this email.

8 |

Your password won't change until you access the link above and create a new one.

9 | -------------------------------------------------------------------------------- /app/views/devise/mailer/unlock_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Your account has been locked due to an excessive amount of unsuccessful sign in attempts.

4 | 5 |

Click the link below to unlock your account:

6 | 7 |

<%= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @resource.unlock_token) %>

8 | -------------------------------------------------------------------------------- /app/views/devise/menu/_login_items.html.erb: -------------------------------------------------------------------------------- 1 | <% if user_signed_in? %> 2 |
  • 3 | <%= link_to('Logout', destroy_user_session_path) %> 4 |
  • 5 | <% end %> 6 | <% if !current_subdomain.nil?%> 7 |
  • 8 | User: 9 | <% if current_user %> 10 | <%= current_user.name %> 11 | <% else %> 12 | (not logged in) 13 | <% end %> 14 |
  • 15 | <% end %> 16 | -------------------------------------------------------------------------------- /app/views/devise/menu/_registration_items.html.erb: -------------------------------------------------------------------------------- 1 | <% if user_signed_in? %> 2 |
  • 3 | <%= link_to('Edit account', edit_user_registration_path) %> 4 |
  • 5 | <% else %> 6 |
  • 7 | <%= link_to('Sign up', new_user_registration_path) %> 8 |
  • 9 |
  • 10 | <%= link_to('Login', new_user_session_path) %> 11 |
  • 12 | <% end %> 13 | -------------------------------------------------------------------------------- /app/views/devise/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |

    Change your password

    2 | 3 | <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put }) do |f| %> 4 | <%= devise_error_messages! %> 5 | <%= f.hidden_field :reset_password_token %> 6 | 7 |

    <%= f.label :password %>
    8 | <%= f.password_field :password %>

    9 | 10 |

    <%= f.label :password_confirmation %>
    11 | <%= f.password_field :password_confirmation %>

    12 | 13 |

    <%= f.submit "Change my password" %>

    14 | <% end %> 15 | 16 | <%= render :partial => "devise/shared/links" %> -------------------------------------------------------------------------------- /app/views/devise/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Forgot your password?

    2 | 3 | <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |

    <%= f.label :email %>
    7 | <%= f.text_field :email %>

    8 | 9 |

    <%= f.submit "Send me reset password instructions" %>

    10 | <% end %> 11 | 12 | <%= render :partial => "devise/shared/links" %> -------------------------------------------------------------------------------- /app/views/devise/registrations/edit.html.erb: -------------------------------------------------------------------------------- 1 |

    Edit <%= resource_name.to_s.humanize %>

    2 | 3 | <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |

    <%= f.hidden_field :subdomain_name, :value => resource.subdomain.name %>

    7 | 8 |

    <%= f.label :name %>
    9 | <%= f.text_field :name %>

    10 | 11 |

    <%= f.label :email %>
    12 | <%= f.text_field :email %>

    13 | 14 |

    <%= f.label :password %> (leave blank if you don't want to change it)
    15 | <%= f.password_field :password %>

    16 | 17 |

    <%= f.label :password_confirmation %>
    18 | <%= f.password_field :password_confirmation %>

    19 | 20 |

    <%= f.label :current_password %> (we need your current password to confirm your changes)
    21 | <%= f.password_field :current_password %>

    22 | 23 |

    <%= f.submit "Update" %>

    24 | <% end %> 25 | 26 |

    Cancel my account

    27 | 28 |

    Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), :confirm => "Are you sure?", :method => :delete %>.

    29 | 30 | <%= link_to "Back", :back %> 31 | -------------------------------------------------------------------------------- /app/views/devise/registrations/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Sign up

    2 | 3 | <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 | <% if current_subdomain %> 7 | <%= f.hidden_field :subdomain_name, :value => current_subdomain.name %> 8 | <% else%> 9 |

    <%= f.label :subdomain %> 10 |
    <%= f.text_field :subdomain_name%>

    11 | <% end %> 12 | 13 |

    <%= f.label :name %>
    14 | <%= f.text_field :name %>

    15 | 16 |

    <%= f.label :email %>
    17 | <%= f.text_field :email %>

    18 | 19 |

    <%= f.label :password %>
    20 | <%= f.password_field :password %>

    21 | 22 |

    <%= f.label :password_confirmation %>
    23 | <%= f.password_field :password_confirmation %>

    24 | 25 |

    <%= f.submit "Sign up" %>

    26 | <% end %> 27 | 28 | <%= render :partial => "devise/shared/links" %> 29 | -------------------------------------------------------------------------------- /app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Sign in

    2 | 3 | <%= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %> 4 |

    <%= f.label :email %>
    5 | <%= f.text_field :email %>

    6 | 7 |

    <%= f.label :password %>
    8 | <%= f.password_field :password %>

    9 | 10 | <% if devise_mapping.rememberable? -%> 11 |

    <%= f.check_box :remember_me %> <%= f.label :remember_me %>

    12 | <% end -%> 13 | 14 |

    <%= f.submit "Sign in" %>

    15 | <% end %> 16 | 17 | <%= render :partial => "devise/shared/links" %> -------------------------------------------------------------------------------- /app/views/devise/shared/_links.erb: -------------------------------------------------------------------------------- 1 | <%- if controller_name != 'sessions' %> 2 | <% if !current_subdomain.nil? %> 3 | <%= link_to "Sign in", new_session_path(resource_name) %>
    4 | <% end %> 5 | <% end -%> 6 | 7 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %> 8 | <%= link_to "Sign up", new_registration_path(resource_name) %>
    9 | <% end -%> 10 | 11 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' %> 12 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
    13 | <% end -%> 14 | 15 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> 16 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
    17 | <% end -%> 18 | 19 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> 20 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
    21 | <% end -%> 22 | -------------------------------------------------------------------------------- /app/views/devise/unlocks/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Resend unlock instructions

    2 | 3 | <%= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |

    <%= f.label :email %>
    7 | <%= f.text_field :email %>

    8 | 9 |

    <%= f.submit "Resend unlock instructions" %>

    10 | <% end %> 11 | 12 | <%= render :partial => "devise/shared/links" %> -------------------------------------------------------------------------------- /app/views/home/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Rails3-Subdomain-Devise

    2 |

    <%= link_to "View List of all Users", users_path %>

    3 |

    <%= link_to "View List of subdomains", subdomains_path %>

    4 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rails3-Subdomain-Devise 5 | <%= stylesheet_link_tag :all %> 6 | <%= javascript_include_tag :defaults %> 7 | <%= csrf_meta_tag %> 8 | 9 | 10 | 14 |

    <%= notice %>

    15 |

    <%= alert %>

    16 | <%= yield %> 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/views/sites/index.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= link_to 'Home', root_url(:subdomain => false) %>

    2 |

    Site: <%= @site.name %> Users

    3 | 4 | 5 | <% @site.users.each do |user| %> 6 | 7 | 12 | 13 | <% end %> 14 |
    <%= link_to user.name, user %> 8 | <%if user.id == @site.user_id%> 9 | isSiteAdmin 10 | <% end %> 11 |
    15 | -------------------------------------------------------------------------------- /app/views/sites/opps.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= link_to 'Home', root_url(:subdomain => false) %>

    2 |

    Opps: <%= @site.name %>

    3 | 4 | <%= flash[:alert] %> 5 | -------------------------------------------------------------------------------- /app/views/subdomains/_form.html.erb: -------------------------------------------------------------------------------- 1 | <% if @subdomain.errors.any? %> 2 |
    3 |

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

    4 | 9 |
    10 | <% end %> 11 | <%= fields_for @subdomain do |f| %> 12 |
    13 | <%= f.label :name %> 14 | <%= f.text_field :name %> 15 | <%= f.hidden_field (:user_id, :value => @subdomain.user_id) %> 16 |
    17 |
    18 |
    19 | <%= f.submit %> 20 |
    21 | <% end %> 22 | -------------------------------------------------------------------------------- /app/views/subdomains/edit.html.erb: -------------------------------------------------------------------------------- 1 |

    Editing subdomain

    2 | <%= form_for(@subdomain) do |f| %> 3 | <%= render 'form' %> 4 | <% end %><%= link_to 'Show', @subdomain %> | 5 | <%= link_to @subdomain.user.name, user_path(@subdomain.user) %> 6 | -------------------------------------------------------------------------------- /app/views/subdomains/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Subdomains

    2 | 3 | <% @subdomains.each do |subdomain| %> 4 | 5 | 6 | 7 | <% end %> 8 |
    <%= link_to subdomain.name, subdomain %>
    9 | -------------------------------------------------------------------------------- /app/views/subdomains/new.html.erb: -------------------------------------------------------------------------------- 1 |

    New subdomain

    2 | <%= form_for([@user, @subdomain]) do |f| %> 3 | <%= render 'form' %> 4 | <% end %> 5 | <%= link_to @subdomain.user.name, user_path(@subdomain.user) %> 6 | -------------------------------------------------------------------------------- /app/views/subdomains/show.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= @subdomain.name %>

    2 |

    Belongs to: <%= link_to @admin.name, user_url(@admin) %>

    3 | -------------------------------------------------------------------------------- /app/views/users/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Users

    2 | 3 | <% @users.each do |user| %> 4 | 5 | 7 | 8 | <% end %> 9 |
    <%= link_to user.name, user %> 6 |
    10 | -------------------------------------------------------------------------------- /app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 |

    <%= @user.name %>

    2 |

    Email: <%= @user.email %>

    3 | <%= link_to 'List of Users', users_path %> 4 |

    <%= @user.name %>'s Subdomains

    5 | 6 | <% subdomain = @user.subdomain %> 7 | 8 | 9 | 10 | 11 |
    <%= link_to subdomain.name, subdomain %><%= link_to "Visit #{root_url(:subdomain => subdomain.name)}", root_url(:subdomain => subdomain.name) %>
    12 |
    13 | -------------------------------------------------------------------------------- /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 Rails3SubdomainDevise::Application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # If you have a Gemfile, require the gems listed there, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(:default, Rails.env) if defined?(Bundler) 8 | 9 | module Rails3SubdomainDevise 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Custom directories with classes and modules you want to be autoloadable. 16 | config.autoload_paths += %W(#{config.root}/lib) 17 | 18 | # Only load the plugins named here, in the order given (default is alphabetical). 19 | # :all can be used as a placeholder for all plugins not explicitly named. 20 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 21 | 22 | # Activate observers that should always be running. 23 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 24 | 25 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 26 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 27 | # config.time_zone = 'Central Time (US & Canada)' 28 | 29 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 30 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 31 | # config.i18n.default_locale = :de 32 | 33 | # JavaScript files you want as :defaults (application.js is always included). 34 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 35 | 36 | # Configure the default encoding used in templates for Ruby 1.9. 37 | config.encoding = "utf-8" 38 | 39 | # Configure sensitive parameters which will be filtered from the log file. 40 | config.filter_parameters += [:password, :password_confirmation] 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Rails3SubdomainDevise::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails3SubdomainDevise::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_view.debug_rjs = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Don't care if the mailer can't send 18 | # config.action_mailer.raise_delivery_errors = false 19 | 20 | # Print deprecation notices to the Rails logger 21 | config.active_support.deprecation = :log 22 | 23 | ### ActionMailer Config 24 | config.action_mailer.default_url_options = { :host => 'localhost:3000' } 25 | # A dummy setup for development - no deliveries, but logged 26 | config.action_mailer.delivery_method = :smtp 27 | config.action_mailer.perform_deliveries = false 28 | config.action_mailer.raise_delivery_errors = true 29 | config.action_mailer.default :charset => "utf-8" 30 | end 31 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails3SubdomainDevise::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 | 50 | config.action_mailer.default_url_options = { :host => 'yourhost.com' } 51 | ### ActionMailer Config 52 | # Setup for production - deliveries, no errors raised 53 | config.action_mailer.delivery_method = :smtp 54 | config.action_mailer.perform_deliveries = true 55 | config.action_mailer.raise_delivery_errors = false 56 | config.action_mailer.default :charset => "utf-8" 57 | end 58 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails3SubdomainDevise::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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # Use this hook to configure devise mailer, warden hooks and so forth. The first 2 | # four configuration values can also be set straight in your models. 3 | Devise.setup do |config| 4 | # ==> Mailer Configuration 5 | # Configure the e-mail address which will be shown in DeviseMailer. 6 | config.mailer_sender = "please-change-me@config-initializers-devise.com" 7 | 8 | # Configure the class responsible to send e-mails. 9 | # config.mailer = "Devise::Mailer" 10 | 11 | # ==> ORM configuration 12 | # Load and configure the ORM. Supports :active_record (default) and 13 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 14 | # available as additional gems. 15 | require 'devise/orm/active_record' 16 | 17 | # ==> Configuration for any authentication mechanism 18 | # Configure which keys are used when authenticating an user. By default is 19 | # just :email. You can configure it to use [:username, :subdomain], so for 20 | # authenticating an user, both parameters are required. Remember that those 21 | # parameters are used only when authenticating and not when retrieving from 22 | # session. If you need permissions, you should implement that in a before filter. 23 | # config.authentication_keys = [ :email ] 24 | 25 | # Tell if authentication through request.params is enabled. True by default. 26 | # config.params_authenticatable = true 27 | 28 | # Tell if authentication through HTTP Basic Auth is enabled. True by default. 29 | # config.http_authenticatable = true 30 | 31 | # Set this to true to use Basic Auth for AJAX requests. True by default. 32 | # config.http_authenticatable_on_xhr = true 33 | 34 | # The realm used in Http Basic Authentication 35 | # config.http_authentication_realm = "Application" 36 | 37 | # ==> Configuration for :database_authenticatable 38 | # For bcrypt, this is the cost for hashing the password and defaults to 10. If 39 | # using other encryptors, it sets how many times you want the password re-encrypted. 40 | config.stretches = 10 41 | 42 | # Define which will be the encryption algorithm. Devise also supports encryptors 43 | # from others authentication tools as :clearance_sha1, :authlogic_sha512 (then 44 | # you should set stretches above to 20 for default behavior) and :restful_authentication_sha1 45 | # (then you should set stretches to 10, and copy REST_AUTH_SITE_KEY to pepper) 46 | config.encryptor = :bcrypt 47 | 48 | # Setup a pepper to generate the encrypted password. 49 | config.pepper = "a4ba75d182812bf1eb76a8456e959051eaf77fdeba87e467119a925c5186d0183c2e44eddbcb99cb4490bcb55a61f427bd91b33280bd272779d9543c4bc23723" 50 | 51 | # ==> Configuration for :confirmable 52 | # The time you want to give your user to confirm his account. During this time 53 | # he will be able to access your application without confirming. Default is nil. 54 | # When confirm_within is zero, the user won't be able to sign in without confirming. 55 | # You can use this to let your user access some features of your application 56 | # without confirming the account, but blocking it after a certain period 57 | # (ie 2 days). 58 | # config.confirm_within = 2.days 59 | 60 | # ==> Configuration for :rememberable 61 | # The time the user will be remembered without asking for credentials again. 62 | # config.remember_for = 2.weeks 63 | 64 | # If true, a valid remember token can be re-used between multiple browsers. 65 | # config.remember_across_browsers = true 66 | 67 | # If true, extends the user's remember period when remembered via cookie. 68 | # config.extend_remember_period = false 69 | 70 | # ==> Configuration for :validatable 71 | # Range for password length 72 | # config.password_length = 6..20 73 | 74 | # Regex to use to validate the email address 75 | # config.email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i 76 | 77 | # ==> Configuration for :timeoutable 78 | # The time you want to timeout the user session without activity. After this 79 | # time the user will be asked for credentials again. 80 | # config.timeout_in = 10.minutes 81 | 82 | # ==> Configuration for :lockable 83 | # Defines which strategy will be used to lock an account. 84 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 85 | # :none = No lock strategy. You should handle locking by yourself. 86 | # config.lock_strategy = :failed_attempts 87 | 88 | # Defines which strategy will be used to unlock an account. 89 | # :email = Sends an unlock link to the user email 90 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 91 | # :both = Enables both strategies 92 | # :none = No unlock strategy. You should handle unlocking by yourself. 93 | # config.unlock_strategy = :both 94 | 95 | # Number of authentication tries before locking an account if lock_strategy 96 | # is failed attempts. 97 | # config.maximum_attempts = 20 98 | 99 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 100 | # config.unlock_in = 1.hour 101 | 102 | # ==> Configuration for :token_authenticatable 103 | # Defines name of the authentication token params key 104 | # config.token_authentication_key = :auth_token 105 | 106 | # ==> Scopes configuration 107 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 108 | # "users/sessions/new". It's turned off by default because it's slower if you 109 | # are using only default views. 110 | # config.scoped_views = true 111 | 112 | # Configure the default scope given to Warden. By default it's the first 113 | # devise role declared in your routes. 114 | # config.default_scope = :user 115 | 116 | # Configure sign_out behavior. 117 | # By default sign_out is scoped (i.e. /users/sign_out affects only :user scope). 118 | # In case of sign_out_all_scopes set to true any logout action will sign out all active scopes. 119 | # config.sign_out_all_scopes = false 120 | 121 | # ==> Navigation configuration 122 | # Lists the formats that should be treated as navigational. Formats like 123 | # :html, should redirect to the sign in page when the user does not have 124 | # access, but formats like :xml or :json, should return 401. 125 | # If you have any extra navigational formats, like :iphone or :mobile, you 126 | # should add them to the navigational formats lists. Default is [:html] 127 | # config.navigational_formats = [:html, :iphone] 128 | 129 | # ==> Warden configuration 130 | # If you want to use other strategies, that are not (yet) supported by Devise, 131 | # you can configure them inside the config.warden block. The example below 132 | # allows you to setup OAuth, using http://github.com/roman/warden_oauth 133 | # 134 | # config.warden do |manager| 135 | # manager.oauth(:twitter) do |twitter| 136 | # twitter.consumer_secret = 137 | # twitter.consumer_key = 138 | # twitter.options :site => 'http://twitter.com' 139 | # end 140 | # manager.default_strategies(:scope => :user).unshift :twitter_oauth 141 | # end 142 | end 143 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | Rails3SubdomainDevise::Application.config.secret_token = '1fef09f03b01ba6aa2fbad70aa34b3a1d28076a40fd507b6805260edd6f06220621f7f09ef0091747afb50e06f424ad042bb05651520f4d1c0aec23bac4946ad' 8 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails3SubdomainDevise::Application.config.session_store :cookie_store, :key => '_rails3-subdomain-devise_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 | # Rails3SubdomainDevise::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | not_found: "not found" 5 | already_confirmed: "was already confirmed" 6 | not_locked: "was not locked" 7 | 8 | devise: 9 | failure: 10 | unauthenticated: 'You need to sign in or sign up before continuing.' 11 | unconfirmed: 'You have to confirm your account before continuing.' 12 | locked: 'Your account is locked.' 13 | invalid: 'Invalid email or password.' 14 | invalid_token: 'Invalid authentication token.' 15 | timeout: 'Your session expired, please sign in again to continue.' 16 | inactive: 'Your account was not activated yet.' 17 | sessions: 18 | signed_in: 'Signed in successfully.' 19 | signed_out: 'Signed out successfully.' 20 | passwords: 21 | send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.' 22 | updated: 'Your password was changed successfully. You are now signed in.' 23 | confirmations: 24 | send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.' 25 | confirmed: 'Your account was successfully confirmed. You are now signed in.' 26 | registrations: 27 | signed_up: 'You have signed up successfully. If enabled, a confirmation was sent to your e-mail.' 28 | updated: 'You updated your account successfully.' 29 | destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' 30 | unlocks: 31 | send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' 32 | unlocked: 'Your account was successfully unlocked. You are now signed in.' 33 | mailer: 34 | confirmation_instructions: 35 | subject: 'Confirmation instructions' 36 | reset_password_instructions: 37 | subject: 'Reset password instructions' 38 | unlock_instructions: 39 | subject: 'Unlock Instructions' 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails3SubdomainDevise::Application.routes.draw do 2 | devise_for :users 3 | resources :users, :only => [:index, :show] do 4 | member do 5 | get :valid 6 | end 7 | end 8 | resources :subdomains, :only => [:index, :show] 9 | constraints(SubdomainRoute) do 10 | match '/' => 'sites#index' 11 | match '/opps' => 'sites#opps' 12 | end 13 | 14 | root :to => "home#index" 15 | 16 | # The priority is based upon order of creation: 17 | # first created -> highest priority. 18 | 19 | # Sample of regular route: 20 | # match 'products/:id' => 'catalog#view' 21 | # Keep in mind you can assign values other than :controller and :action 22 | 23 | # Sample of named route: 24 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 25 | # This route can be invoked with purchase_url(:id => product.id) 26 | 27 | # Sample resource route (maps HTTP verbs to controller actions automatically): 28 | # resources :products 29 | 30 | # Sample resource route with options: 31 | # resources :products do 32 | # member do 33 | # get :short 34 | # post :toggle 35 | # end 36 | # 37 | # collection do 38 | # get :sold 39 | # end 40 | # end 41 | 42 | # Sample resource route with sub-resources: 43 | # resources :products do 44 | # resources :comments, :sales 45 | # resource :seller 46 | # end 47 | 48 | # Sample resource route with more complex sub-resources 49 | # resources :products do 50 | # resources :comments 51 | # resources :sales do 52 | # get :recent, :on => :collection 53 | # end 54 | # end 55 | 56 | # Sample resource route within a namespace: 57 | # namespace :admin do 58 | # # Directs /admin/products/* to Admin::ProductsController 59 | # # (app/controllers/admin/products_controller.rb) 60 | # resources :products 61 | # end 62 | 63 | # You can have the root of your site routed with "root" 64 | # just remember to delete public/index.html. 65 | # root :to => "welcome#index" 66 | 67 | # See how all your routes lay out with "rake routes" 68 | 69 | # This is a legacy wild controller route that's not recommended for RESTful applications. 70 | # Note: This route will make all actions in every controller accessible via GET requests. 71 | # match ':controller(/:action(/:id(.:format)))' 72 | end 73 | -------------------------------------------------------------------------------- /db/migrate/20100807190405_create_slugs.rb: -------------------------------------------------------------------------------- 1 | class CreateSlugs < ActiveRecord::Migration 2 | def self.up 3 | create_table :slugs do |t| 4 | t.string :name 5 | t.integer :sluggable_id 6 | t.integer :sequence, :null => false, :default => 1 7 | t.string :sluggable_type, :limit => 40 8 | t.string :scope 9 | t.datetime :created_at 10 | end 11 | add_index :slugs, :sluggable_id 12 | add_index :slugs, [:name, :sluggable_type, :sequence, :scope], :name => "index_slugs_on_n_s_s_and_s", :unique => true 13 | end 14 | 15 | def self.down 16 | drop_table :slugs 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20100808194405_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateUsers < ActiveRecord::Migration 2 | def self.up 3 | create_table(:users) do |t| 4 | t.database_authenticatable :null => false 5 | t.recoverable 6 | t.rememberable 7 | t.trackable 8 | t.string :name 9 | t.string :subdomain_name, :limit => 40 10 | t.string :loginable_type, :limit => 40 11 | t.integer :loginable_id 12 | # t.confirmable 13 | # t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both 14 | # t.token_authenticatable 15 | 16 | 17 | t.timestamps 18 | end 19 | 20 | add_index :users, :email, :unique => true 21 | add_index :users, :reset_password_token, :unique => true 22 | # add_index :users, :confirmation_token, :unique => true 23 | # add_index :users, :unlock_token, :unique => true 24 | end 25 | 26 | def self.down 27 | drop_table :users 28 | end 29 | end -------------------------------------------------------------------------------- /db/migrate/20100808194652_create_subdomains.rb: -------------------------------------------------------------------------------- 1 | class CreateSubdomains < ActiveRecord::Migration 2 | def self.up 3 | create_table :subdomains do |t| 4 | t.string :name 5 | t.references :user 6 | 7 | t.timestamps 8 | end 9 | end 10 | 11 | def self.down 12 | drop_table :subdomains 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20100930104337_migrate_subdomain_to_integer.rb: -------------------------------------------------------------------------------- 1 | class MigrateSubdomainToInteger < ActiveRecord::Migration 2 | def self.up 3 | remove_column :users, :subdomain_name 4 | add_column :users, :subdomain_id, :integer 5 | add_index :users, :subdomain_id 6 | add_index :subdomains, :name 7 | end 8 | 9 | def self.down 10 | add_column :users, :subdomain_name, :limit => 40 11 | remove_column :users, :subdomain_id 12 | remove_index :users, :subdomain_id 13 | remove_index :subdomains, :name 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended to check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(:version => 20101024200552) do 14 | 15 | create_table "subdomains", :force => true do |t| 16 | t.string "name" 17 | t.integer "user_id" 18 | t.datetime "created_at" 19 | t.datetime "updated_at" 20 | end 21 | 22 | add_index "subdomains", ["name"], :name => "index_subdomains_on_name" 23 | 24 | create_table "users", :force => true do |t| 25 | t.string "email", :default => "", :null => false 26 | t.string "encrypted_password", :limit => 128, :default => "", :null => false 27 | t.string "password_salt", :default => "", :null => false 28 | t.string "reset_password_token" 29 | t.string "remember_token" 30 | t.datetime "remember_created_at" 31 | t.integer "sign_in_count", :default => 0 32 | t.datetime "current_sign_in_at" 33 | t.datetime "last_sign_in_at" 34 | t.string "current_sign_in_ip" 35 | t.string "last_sign_in_ip" 36 | t.string "name" 37 | t.datetime "created_at" 38 | t.datetime "updated_at" 39 | t.integer "subdomain_id" 40 | t.string "loginable_token" 41 | end 42 | 43 | add_index "users", ["email"], :name => "index_users_on_email", :unique => true 44 | add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true 45 | add_index "users", ["subdomain_id"], :name => "index_users_on_subdomain_id" 46 | add_index "users", ["loginable_token"], :name => "index_users_on_loginable_token" 47 | 48 | end 49 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) 7 | # Mayor.create(:name => 'Daley', :city => cities.first) 8 | puts 'SETTING UP EXAMPLE USERS' 9 | user1 = User.create! :name => 'Foo First User', :subdomain_name => "foo", :email => 'user1@test.com', :password => 'please', :password_confirmation => 'please' 10 | puts 'New user created: ' << user1.name 11 | user2 = User.create! :name => 'Bar First User', :subdomain_name => "bar", :email => 'user2@test.com', :password => 'please', :password_confirmation => 'please' 12 | puts 'New user created: ' << user2.name 13 | user3 = User.create! :name => 'Foo Second User', :subdomain_name => "foo", :email => 'user3@test.com', :password => 'please', :password_confirmation => 'please' 14 | puts 'New user created: ' << user3.name 15 | user4 = User.create! :name => 'Bar Second User', :subdomain_name => "bar", :email => 'user4@test.com', :password => 'please', :password_confirmation => 'please' 16 | puts 'New user created: ' << user4.name 17 | 18 | # subdomains creation removed because they are created automaticaly by user signup left only the display 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/subdomain_route.rb: -------------------------------------------------------------------------------- 1 | class SubdomainRoute 2 | def self.matches?(request) 3 | request.subdomain.present? && request.subdomain != "www" 4 | end 5 | end -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salex/rails3-subdomain-devise/a80e2b816d15c49ebe896eb157abd9c7d03dfb72/lib/tasks/.gitkeep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salex/rails3-subdomain-devise/a80e2b816d15c49ebe896eb157abd9c7d03dfb72/public/favicon.ico -------------------------------------------------------------------------------- /public/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salex/rails3-subdomain-devise/a80e2b816d15c49ebe896eb157abd9c7d03dfb72/public/images/rails.png -------------------------------------------------------------------------------- /public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // Place your application-specific JavaScript functions and classes here 2 | // This file is automatically included by javascript_include_tag :defaults 3 | -------------------------------------------------------------------------------- /public/javascripts/controls.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 2 | 3 | // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan) 5 | // (c) 2005-2009 Jon Tirsen (http://www.tirsen.com) 6 | // Contributors: 7 | // Richard Livsey 8 | // Rahul Bhargava 9 | // Rob Wills 10 | // 11 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 12 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 13 | 14 | // Autocompleter.Base handles all the autocompletion functionality 15 | // that's independent of the data source for autocompletion. This 16 | // includes drawing the autocompletion menu, observing keyboard 17 | // and mouse events, and similar. 18 | // 19 | // Specific autocompleters need to provide, at the very least, 20 | // a getUpdatedChoices function that will be invoked every time 21 | // the text inside the monitored textbox changes. This method 22 | // should get the text for which to provide autocompletion by 23 | // invoking this.getToken(), NOT by directly accessing 24 | // this.element.value. This is to allow incremental tokenized 25 | // autocompletion. Specific auto-completion logic (AJAX, etc) 26 | // belongs in getUpdatedChoices. 27 | // 28 | // Tokenized incremental autocompletion is enabled automatically 29 | // when an autocompleter is instantiated with the 'tokens' option 30 | // in the options parameter, e.g.: 31 | // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); 32 | // will incrementally autocomplete with a comma as the token. 33 | // Additionally, ',' in the above example can be replaced with 34 | // a token array, e.g. { tokens: [',', '\n'] } which 35 | // enables autocompletion on multiple tokens. This is most 36 | // useful when one of the tokens is \n (a newline), as it 37 | // allows smart autocompletion after linebreaks. 38 | 39 | if(typeof Effect == 'undefined') 40 | throw("controls.js requires including script.aculo.us' effects.js library"); 41 | 42 | var Autocompleter = { }; 43 | Autocompleter.Base = Class.create({ 44 | baseInitialize: function(element, update, options) { 45 | element = $(element); 46 | this.element = element; 47 | this.update = $(update); 48 | this.hasFocus = false; 49 | this.changed = false; 50 | this.active = false; 51 | this.index = 0; 52 | this.entryCount = 0; 53 | this.oldElementValue = this.element.value; 54 | 55 | if(this.setOptions) 56 | this.setOptions(options); 57 | else 58 | this.options = options || { }; 59 | 60 | this.options.paramName = this.options.paramName || this.element.name; 61 | this.options.tokens = this.options.tokens || []; 62 | this.options.frequency = this.options.frequency || 0.4; 63 | this.options.minChars = this.options.minChars || 1; 64 | this.options.onShow = this.options.onShow || 65 | function(element, update){ 66 | if(!update.style.position || update.style.position=='absolute') { 67 | update.style.position = 'absolute'; 68 | Position.clone(element, update, { 69 | setHeight: false, 70 | offsetTop: element.offsetHeight 71 | }); 72 | } 73 | Effect.Appear(update,{duration:0.15}); 74 | }; 75 | this.options.onHide = this.options.onHide || 76 | function(element, update){ new Effect.Fade(update,{duration:0.15}) }; 77 | 78 | if(typeof(this.options.tokens) == 'string') 79 | this.options.tokens = new Array(this.options.tokens); 80 | // Force carriage returns as token delimiters anyway 81 | if (!this.options.tokens.include('\n')) 82 | this.options.tokens.push('\n'); 83 | 84 | this.observer = null; 85 | 86 | this.element.setAttribute('autocomplete','off'); 87 | 88 | Element.hide(this.update); 89 | 90 | Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); 91 | Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); 92 | }, 93 | 94 | show: function() { 95 | if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); 96 | if(!this.iefix && 97 | (Prototype.Browser.IE) && 98 | (Element.getStyle(this.update, 'position')=='absolute')) { 99 | new Insertion.After(this.update, 100 | ''); 103 | this.iefix = $(this.update.id+'_iefix'); 104 | } 105 | if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); 106 | }, 107 | 108 | fixIEOverlapping: function() { 109 | Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); 110 | this.iefix.style.zIndex = 1; 111 | this.update.style.zIndex = 2; 112 | Element.show(this.iefix); 113 | }, 114 | 115 | hide: function() { 116 | this.stopIndicator(); 117 | if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); 118 | if(this.iefix) Element.hide(this.iefix); 119 | }, 120 | 121 | startIndicator: function() { 122 | if(this.options.indicator) Element.show(this.options.indicator); 123 | }, 124 | 125 | stopIndicator: function() { 126 | if(this.options.indicator) Element.hide(this.options.indicator); 127 | }, 128 | 129 | onKeyPress: function(event) { 130 | if(this.active) 131 | switch(event.keyCode) { 132 | case Event.KEY_TAB: 133 | case Event.KEY_RETURN: 134 | this.selectEntry(); 135 | Event.stop(event); 136 | case Event.KEY_ESC: 137 | this.hide(); 138 | this.active = false; 139 | Event.stop(event); 140 | return; 141 | case Event.KEY_LEFT: 142 | case Event.KEY_RIGHT: 143 | return; 144 | case Event.KEY_UP: 145 | this.markPrevious(); 146 | this.render(); 147 | Event.stop(event); 148 | return; 149 | case Event.KEY_DOWN: 150 | this.markNext(); 151 | this.render(); 152 | Event.stop(event); 153 | return; 154 | } 155 | else 156 | if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 157 | (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; 158 | 159 | this.changed = true; 160 | this.hasFocus = true; 161 | 162 | if(this.observer) clearTimeout(this.observer); 163 | this.observer = 164 | setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); 165 | }, 166 | 167 | activate: function() { 168 | this.changed = false; 169 | this.hasFocus = true; 170 | this.getUpdatedChoices(); 171 | }, 172 | 173 | onHover: function(event) { 174 | var element = Event.findElement(event, 'LI'); 175 | if(this.index != element.autocompleteIndex) 176 | { 177 | this.index = element.autocompleteIndex; 178 | this.render(); 179 | } 180 | Event.stop(event); 181 | }, 182 | 183 | onClick: function(event) { 184 | var element = Event.findElement(event, 'LI'); 185 | this.index = element.autocompleteIndex; 186 | this.selectEntry(); 187 | this.hide(); 188 | }, 189 | 190 | onBlur: function(event) { 191 | // needed to make click events working 192 | setTimeout(this.hide.bind(this), 250); 193 | this.hasFocus = false; 194 | this.active = false; 195 | }, 196 | 197 | render: function() { 198 | if(this.entryCount > 0) { 199 | for (var i = 0; i < this.entryCount; i++) 200 | this.index==i ? 201 | Element.addClassName(this.getEntry(i),"selected") : 202 | Element.removeClassName(this.getEntry(i),"selected"); 203 | if(this.hasFocus) { 204 | this.show(); 205 | this.active = true; 206 | } 207 | } else { 208 | this.active = false; 209 | this.hide(); 210 | } 211 | }, 212 | 213 | markPrevious: function() { 214 | if(this.index > 0) this.index--; 215 | else this.index = this.entryCount-1; 216 | this.getEntry(this.index).scrollIntoView(true); 217 | }, 218 | 219 | markNext: function() { 220 | if(this.index < this.entryCount-1) this.index++; 221 | else this.index = 0; 222 | this.getEntry(this.index).scrollIntoView(false); 223 | }, 224 | 225 | getEntry: function(index) { 226 | return this.update.firstChild.childNodes[index]; 227 | }, 228 | 229 | getCurrentEntry: function() { 230 | return this.getEntry(this.index); 231 | }, 232 | 233 | selectEntry: function() { 234 | this.active = false; 235 | this.updateElement(this.getCurrentEntry()); 236 | }, 237 | 238 | updateElement: function(selectedElement) { 239 | if (this.options.updateElement) { 240 | this.options.updateElement(selectedElement); 241 | return; 242 | } 243 | var value = ''; 244 | if (this.options.select) { 245 | var nodes = $(selectedElement).select('.' + this.options.select) || []; 246 | if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); 247 | } else 248 | value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); 249 | 250 | var bounds = this.getTokenBounds(); 251 | if (bounds[0] != -1) { 252 | var newValue = this.element.value.substr(0, bounds[0]); 253 | var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); 254 | if (whitespace) 255 | newValue += whitespace[0]; 256 | this.element.value = newValue + value + this.element.value.substr(bounds[1]); 257 | } else { 258 | this.element.value = value; 259 | } 260 | this.oldElementValue = this.element.value; 261 | this.element.focus(); 262 | 263 | if (this.options.afterUpdateElement) 264 | this.options.afterUpdateElement(this.element, selectedElement); 265 | }, 266 | 267 | updateChoices: function(choices) { 268 | if(!this.changed && this.hasFocus) { 269 | this.update.innerHTML = choices; 270 | Element.cleanWhitespace(this.update); 271 | Element.cleanWhitespace(this.update.down()); 272 | 273 | if(this.update.firstChild && this.update.down().childNodes) { 274 | this.entryCount = 275 | this.update.down().childNodes.length; 276 | for (var i = 0; i < this.entryCount; i++) { 277 | var entry = this.getEntry(i); 278 | entry.autocompleteIndex = i; 279 | this.addObservers(entry); 280 | } 281 | } else { 282 | this.entryCount = 0; 283 | } 284 | 285 | this.stopIndicator(); 286 | this.index = 0; 287 | 288 | if(this.entryCount==1 && this.options.autoSelect) { 289 | this.selectEntry(); 290 | this.hide(); 291 | } else { 292 | this.render(); 293 | } 294 | } 295 | }, 296 | 297 | addObservers: function(element) { 298 | Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); 299 | Event.observe(element, "click", this.onClick.bindAsEventListener(this)); 300 | }, 301 | 302 | onObserverEvent: function() { 303 | this.changed = false; 304 | this.tokenBounds = null; 305 | if(this.getToken().length>=this.options.minChars) { 306 | this.getUpdatedChoices(); 307 | } else { 308 | this.active = false; 309 | this.hide(); 310 | } 311 | this.oldElementValue = this.element.value; 312 | }, 313 | 314 | getToken: function() { 315 | var bounds = this.getTokenBounds(); 316 | return this.element.value.substring(bounds[0], bounds[1]).strip(); 317 | }, 318 | 319 | getTokenBounds: function() { 320 | if (null != this.tokenBounds) return this.tokenBounds; 321 | var value = this.element.value; 322 | if (value.strip().empty()) return [-1, 0]; 323 | var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); 324 | var offset = (diff == this.oldElementValue.length ? 1 : 0); 325 | var prevTokenPos = -1, nextTokenPos = value.length; 326 | var tp; 327 | for (var index = 0, l = this.options.tokens.length; index < l; ++index) { 328 | tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); 329 | if (tp > prevTokenPos) prevTokenPos = tp; 330 | tp = value.indexOf(this.options.tokens[index], diff + offset); 331 | if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; 332 | } 333 | return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); 334 | } 335 | }); 336 | 337 | Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { 338 | var boundary = Math.min(newS.length, oldS.length); 339 | for (var index = 0; index < boundary; ++index) 340 | if (newS[index] != oldS[index]) 341 | return index; 342 | return boundary; 343 | }; 344 | 345 | Ajax.Autocompleter = Class.create(Autocompleter.Base, { 346 | initialize: function(element, update, url, options) { 347 | this.baseInitialize(element, update, options); 348 | this.options.asynchronous = true; 349 | this.options.onComplete = this.onComplete.bind(this); 350 | this.options.defaultParams = this.options.parameters || null; 351 | this.url = url; 352 | }, 353 | 354 | getUpdatedChoices: function() { 355 | this.startIndicator(); 356 | 357 | var entry = encodeURIComponent(this.options.paramName) + '=' + 358 | encodeURIComponent(this.getToken()); 359 | 360 | this.options.parameters = this.options.callback ? 361 | this.options.callback(this.element, entry) : entry; 362 | 363 | if(this.options.defaultParams) 364 | this.options.parameters += '&' + this.options.defaultParams; 365 | 366 | new Ajax.Request(this.url, this.options); 367 | }, 368 | 369 | onComplete: function(request) { 370 | this.updateChoices(request.responseText); 371 | } 372 | }); 373 | 374 | // The local array autocompleter. Used when you'd prefer to 375 | // inject an array of autocompletion options into the page, rather 376 | // than sending out Ajax queries, which can be quite slow sometimes. 377 | // 378 | // The constructor takes four parameters. The first two are, as usual, 379 | // the id of the monitored textbox, and id of the autocompletion menu. 380 | // The third is the array you want to autocomplete from, and the fourth 381 | // is the options block. 382 | // 383 | // Extra local autocompletion options: 384 | // - choices - How many autocompletion choices to offer 385 | // 386 | // - partialSearch - If false, the autocompleter will match entered 387 | // text only at the beginning of strings in the 388 | // autocomplete array. Defaults to true, which will 389 | // match text at the beginning of any *word* in the 390 | // strings in the autocomplete array. If you want to 391 | // search anywhere in the string, additionally set 392 | // the option fullSearch to true (default: off). 393 | // 394 | // - fullSsearch - Search anywhere in autocomplete array strings. 395 | // 396 | // - partialChars - How many characters to enter before triggering 397 | // a partial match (unlike minChars, which defines 398 | // how many characters are required to do any match 399 | // at all). Defaults to 2. 400 | // 401 | // - ignoreCase - Whether to ignore case when autocompleting. 402 | // Defaults to true. 403 | // 404 | // It's possible to pass in a custom function as the 'selector' 405 | // option, if you prefer to write your own autocompletion logic. 406 | // In that case, the other options above will not apply unless 407 | // you support them. 408 | 409 | Autocompleter.Local = Class.create(Autocompleter.Base, { 410 | initialize: function(element, update, array, options) { 411 | this.baseInitialize(element, update, options); 412 | this.options.array = array; 413 | }, 414 | 415 | getUpdatedChoices: function() { 416 | this.updateChoices(this.options.selector(this)); 417 | }, 418 | 419 | setOptions: function(options) { 420 | this.options = Object.extend({ 421 | choices: 10, 422 | partialSearch: true, 423 | partialChars: 2, 424 | ignoreCase: true, 425 | fullSearch: false, 426 | selector: function(instance) { 427 | var ret = []; // Beginning matches 428 | var partial = []; // Inside matches 429 | var entry = instance.getToken(); 430 | var count = 0; 431 | 432 | for (var i = 0; i < instance.options.array.length && 433 | ret.length < instance.options.choices ; i++) { 434 | 435 | var elem = instance.options.array[i]; 436 | var foundPos = instance.options.ignoreCase ? 437 | elem.toLowerCase().indexOf(entry.toLowerCase()) : 438 | elem.indexOf(entry); 439 | 440 | while (foundPos != -1) { 441 | if (foundPos == 0 && elem.length != entry.length) { 442 | ret.push("
  • " + elem.substr(0, entry.length) + "" + 443 | elem.substr(entry.length) + "
  • "); 444 | break; 445 | } else if (entry.length >= instance.options.partialChars && 446 | instance.options.partialSearch && foundPos != -1) { 447 | if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { 448 | partial.push("
  • " + elem.substr(0, foundPos) + "" + 449 | elem.substr(foundPos, entry.length) + "" + elem.substr( 450 | foundPos + entry.length) + "
  • "); 451 | break; 452 | } 453 | } 454 | 455 | foundPos = instance.options.ignoreCase ? 456 | elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 457 | elem.indexOf(entry, foundPos + 1); 458 | 459 | } 460 | } 461 | if (partial.length) 462 | ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); 463 | return ""; 464 | } 465 | }, options || { }); 466 | } 467 | }); 468 | 469 | // AJAX in-place editor and collection editor 470 | // Full rewrite by Christophe Porteneuve (April 2007). 471 | 472 | // Use this if you notice weird scrolling problems on some browsers, 473 | // the DOM might be a bit confused when this gets called so do this 474 | // waits 1 ms (with setTimeout) until it does the activation 475 | Field.scrollFreeActivate = function(field) { 476 | setTimeout(function() { 477 | Field.activate(field); 478 | }, 1); 479 | }; 480 | 481 | Ajax.InPlaceEditor = Class.create({ 482 | initialize: function(element, url, options) { 483 | this.url = url; 484 | this.element = element = $(element); 485 | this.prepareOptions(); 486 | this._controls = { }; 487 | arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! 488 | Object.extend(this.options, options || { }); 489 | if (!this.options.formId && this.element.id) { 490 | this.options.formId = this.element.id + '-inplaceeditor'; 491 | if ($(this.options.formId)) 492 | this.options.formId = ''; 493 | } 494 | if (this.options.externalControl) 495 | this.options.externalControl = $(this.options.externalControl); 496 | if (!this.options.externalControl) 497 | this.options.externalControlOnly = false; 498 | this._originalBackground = this.element.getStyle('background-color') || 'transparent'; 499 | this.element.title = this.options.clickToEditText; 500 | this._boundCancelHandler = this.handleFormCancellation.bind(this); 501 | this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); 502 | this._boundFailureHandler = this.handleAJAXFailure.bind(this); 503 | this._boundSubmitHandler = this.handleFormSubmission.bind(this); 504 | this._boundWrapperHandler = this.wrapUp.bind(this); 505 | this.registerListeners(); 506 | }, 507 | checkForEscapeOrReturn: function(e) { 508 | if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; 509 | if (Event.KEY_ESC == e.keyCode) 510 | this.handleFormCancellation(e); 511 | else if (Event.KEY_RETURN == e.keyCode) 512 | this.handleFormSubmission(e); 513 | }, 514 | createControl: function(mode, handler, extraClasses) { 515 | var control = this.options[mode + 'Control']; 516 | var text = this.options[mode + 'Text']; 517 | if ('button' == control) { 518 | var btn = document.createElement('input'); 519 | btn.type = 'submit'; 520 | btn.value = text; 521 | btn.className = 'editor_' + mode + '_button'; 522 | if ('cancel' == mode) 523 | btn.onclick = this._boundCancelHandler; 524 | this._form.appendChild(btn); 525 | this._controls[mode] = btn; 526 | } else if ('link' == control) { 527 | var link = document.createElement('a'); 528 | link.href = '#'; 529 | link.appendChild(document.createTextNode(text)); 530 | link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; 531 | link.className = 'editor_' + mode + '_link'; 532 | if (extraClasses) 533 | link.className += ' ' + extraClasses; 534 | this._form.appendChild(link); 535 | this._controls[mode] = link; 536 | } 537 | }, 538 | createEditField: function() { 539 | var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); 540 | var fld; 541 | if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { 542 | fld = document.createElement('input'); 543 | fld.type = 'text'; 544 | var size = this.options.size || this.options.cols || 0; 545 | if (0 < size) fld.size = size; 546 | } else { 547 | fld = document.createElement('textarea'); 548 | fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); 549 | fld.cols = this.options.cols || 40; 550 | } 551 | fld.name = this.options.paramName; 552 | fld.value = text; // No HTML breaks conversion anymore 553 | fld.className = 'editor_field'; 554 | if (this.options.submitOnBlur) 555 | fld.onblur = this._boundSubmitHandler; 556 | this._controls.editor = fld; 557 | if (this.options.loadTextURL) 558 | this.loadExternalText(); 559 | this._form.appendChild(this._controls.editor); 560 | }, 561 | createForm: function() { 562 | var ipe = this; 563 | function addText(mode, condition) { 564 | var text = ipe.options['text' + mode + 'Controls']; 565 | if (!text || condition === false) return; 566 | ipe._form.appendChild(document.createTextNode(text)); 567 | }; 568 | this._form = $(document.createElement('form')); 569 | this._form.id = this.options.formId; 570 | this._form.addClassName(this.options.formClassName); 571 | this._form.onsubmit = this._boundSubmitHandler; 572 | this.createEditField(); 573 | if ('textarea' == this._controls.editor.tagName.toLowerCase()) 574 | this._form.appendChild(document.createElement('br')); 575 | if (this.options.onFormCustomization) 576 | this.options.onFormCustomization(this, this._form); 577 | addText('Before', this.options.okControl || this.options.cancelControl); 578 | this.createControl('ok', this._boundSubmitHandler); 579 | addText('Between', this.options.okControl && this.options.cancelControl); 580 | this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); 581 | addText('After', this.options.okControl || this.options.cancelControl); 582 | }, 583 | destroy: function() { 584 | if (this._oldInnerHTML) 585 | this.element.innerHTML = this._oldInnerHTML; 586 | this.leaveEditMode(); 587 | this.unregisterListeners(); 588 | }, 589 | enterEditMode: function(e) { 590 | if (this._saving || this._editing) return; 591 | this._editing = true; 592 | this.triggerCallback('onEnterEditMode'); 593 | if (this.options.externalControl) 594 | this.options.externalControl.hide(); 595 | this.element.hide(); 596 | this.createForm(); 597 | this.element.parentNode.insertBefore(this._form, this.element); 598 | if (!this.options.loadTextURL) 599 | this.postProcessEditField(); 600 | if (e) Event.stop(e); 601 | }, 602 | enterHover: function(e) { 603 | if (this.options.hoverClassName) 604 | this.element.addClassName(this.options.hoverClassName); 605 | if (this._saving) return; 606 | this.triggerCallback('onEnterHover'); 607 | }, 608 | getText: function() { 609 | return this.element.innerHTML.unescapeHTML(); 610 | }, 611 | handleAJAXFailure: function(transport) { 612 | this.triggerCallback('onFailure', transport); 613 | if (this._oldInnerHTML) { 614 | this.element.innerHTML = this._oldInnerHTML; 615 | this._oldInnerHTML = null; 616 | } 617 | }, 618 | handleFormCancellation: function(e) { 619 | this.wrapUp(); 620 | if (e) Event.stop(e); 621 | }, 622 | handleFormSubmission: function(e) { 623 | var form = this._form; 624 | var value = $F(this._controls.editor); 625 | this.prepareSubmission(); 626 | var params = this.options.callback(form, value) || ''; 627 | if (Object.isString(params)) 628 | params = params.toQueryParams(); 629 | params.editorId = this.element.id; 630 | if (this.options.htmlResponse) { 631 | var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); 632 | Object.extend(options, { 633 | parameters: params, 634 | onComplete: this._boundWrapperHandler, 635 | onFailure: this._boundFailureHandler 636 | }); 637 | new Ajax.Updater({ success: this.element }, this.url, options); 638 | } else { 639 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 640 | Object.extend(options, { 641 | parameters: params, 642 | onComplete: this._boundWrapperHandler, 643 | onFailure: this._boundFailureHandler 644 | }); 645 | new Ajax.Request(this.url, options); 646 | } 647 | if (e) Event.stop(e); 648 | }, 649 | leaveEditMode: function() { 650 | this.element.removeClassName(this.options.savingClassName); 651 | this.removeForm(); 652 | this.leaveHover(); 653 | this.element.style.backgroundColor = this._originalBackground; 654 | this.element.show(); 655 | if (this.options.externalControl) 656 | this.options.externalControl.show(); 657 | this._saving = false; 658 | this._editing = false; 659 | this._oldInnerHTML = null; 660 | this.triggerCallback('onLeaveEditMode'); 661 | }, 662 | leaveHover: function(e) { 663 | if (this.options.hoverClassName) 664 | this.element.removeClassName(this.options.hoverClassName); 665 | if (this._saving) return; 666 | this.triggerCallback('onLeaveHover'); 667 | }, 668 | loadExternalText: function() { 669 | this._form.addClassName(this.options.loadingClassName); 670 | this._controls.editor.disabled = true; 671 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 672 | Object.extend(options, { 673 | parameters: 'editorId=' + encodeURIComponent(this.element.id), 674 | onComplete: Prototype.emptyFunction, 675 | onSuccess: function(transport) { 676 | this._form.removeClassName(this.options.loadingClassName); 677 | var text = transport.responseText; 678 | if (this.options.stripLoadedTextTags) 679 | text = text.stripTags(); 680 | this._controls.editor.value = text; 681 | this._controls.editor.disabled = false; 682 | this.postProcessEditField(); 683 | }.bind(this), 684 | onFailure: this._boundFailureHandler 685 | }); 686 | new Ajax.Request(this.options.loadTextURL, options); 687 | }, 688 | postProcessEditField: function() { 689 | var fpc = this.options.fieldPostCreation; 690 | if (fpc) 691 | $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); 692 | }, 693 | prepareOptions: function() { 694 | this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); 695 | Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); 696 | [this._extraDefaultOptions].flatten().compact().each(function(defs) { 697 | Object.extend(this.options, defs); 698 | }.bind(this)); 699 | }, 700 | prepareSubmission: function() { 701 | this._saving = true; 702 | this.removeForm(); 703 | this.leaveHover(); 704 | this.showSaving(); 705 | }, 706 | registerListeners: function() { 707 | this._listeners = { }; 708 | var listener; 709 | $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { 710 | listener = this[pair.value].bind(this); 711 | this._listeners[pair.key] = listener; 712 | if (!this.options.externalControlOnly) 713 | this.element.observe(pair.key, listener); 714 | if (this.options.externalControl) 715 | this.options.externalControl.observe(pair.key, listener); 716 | }.bind(this)); 717 | }, 718 | removeForm: function() { 719 | if (!this._form) return; 720 | this._form.remove(); 721 | this._form = null; 722 | this._controls = { }; 723 | }, 724 | showSaving: function() { 725 | this._oldInnerHTML = this.element.innerHTML; 726 | this.element.innerHTML = this.options.savingText; 727 | this.element.addClassName(this.options.savingClassName); 728 | this.element.style.backgroundColor = this._originalBackground; 729 | this.element.show(); 730 | }, 731 | triggerCallback: function(cbName, arg) { 732 | if ('function' == typeof this.options[cbName]) { 733 | this.options[cbName](this, arg); 734 | } 735 | }, 736 | unregisterListeners: function() { 737 | $H(this._listeners).each(function(pair) { 738 | if (!this.options.externalControlOnly) 739 | this.element.stopObserving(pair.key, pair.value); 740 | if (this.options.externalControl) 741 | this.options.externalControl.stopObserving(pair.key, pair.value); 742 | }.bind(this)); 743 | }, 744 | wrapUp: function(transport) { 745 | this.leaveEditMode(); 746 | // Can't use triggerCallback due to backward compatibility: requires 747 | // binding + direct element 748 | this._boundComplete(transport, this.element); 749 | } 750 | }); 751 | 752 | Object.extend(Ajax.InPlaceEditor.prototype, { 753 | dispose: Ajax.InPlaceEditor.prototype.destroy 754 | }); 755 | 756 | Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { 757 | initialize: function($super, element, url, options) { 758 | this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; 759 | $super(element, url, options); 760 | }, 761 | 762 | createEditField: function() { 763 | var list = document.createElement('select'); 764 | list.name = this.options.paramName; 765 | list.size = 1; 766 | this._controls.editor = list; 767 | this._collection = this.options.collection || []; 768 | if (this.options.loadCollectionURL) 769 | this.loadCollection(); 770 | else 771 | this.checkForExternalText(); 772 | this._form.appendChild(this._controls.editor); 773 | }, 774 | 775 | loadCollection: function() { 776 | this._form.addClassName(this.options.loadingClassName); 777 | this.showLoadingText(this.options.loadingCollectionText); 778 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 779 | Object.extend(options, { 780 | parameters: 'editorId=' + encodeURIComponent(this.element.id), 781 | onComplete: Prototype.emptyFunction, 782 | onSuccess: function(transport) { 783 | var js = transport.responseText.strip(); 784 | if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check 785 | throw('Server returned an invalid collection representation.'); 786 | this._collection = eval(js); 787 | this.checkForExternalText(); 788 | }.bind(this), 789 | onFailure: this.onFailure 790 | }); 791 | new Ajax.Request(this.options.loadCollectionURL, options); 792 | }, 793 | 794 | showLoadingText: function(text) { 795 | this._controls.editor.disabled = true; 796 | var tempOption = this._controls.editor.firstChild; 797 | if (!tempOption) { 798 | tempOption = document.createElement('option'); 799 | tempOption.value = ''; 800 | this._controls.editor.appendChild(tempOption); 801 | tempOption.selected = true; 802 | } 803 | tempOption.update((text || '').stripScripts().stripTags()); 804 | }, 805 | 806 | checkForExternalText: function() { 807 | this._text = this.getText(); 808 | if (this.options.loadTextURL) 809 | this.loadExternalText(); 810 | else 811 | this.buildOptionList(); 812 | }, 813 | 814 | loadExternalText: function() { 815 | this.showLoadingText(this.options.loadingText); 816 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 817 | Object.extend(options, { 818 | parameters: 'editorId=' + encodeURIComponent(this.element.id), 819 | onComplete: Prototype.emptyFunction, 820 | onSuccess: function(transport) { 821 | this._text = transport.responseText.strip(); 822 | this.buildOptionList(); 823 | }.bind(this), 824 | onFailure: this.onFailure 825 | }); 826 | new Ajax.Request(this.options.loadTextURL, options); 827 | }, 828 | 829 | buildOptionList: function() { 830 | this._form.removeClassName(this.options.loadingClassName); 831 | this._collection = this._collection.map(function(entry) { 832 | return 2 === entry.length ? entry : [entry, entry].flatten(); 833 | }); 834 | var marker = ('value' in this.options) ? this.options.value : this._text; 835 | var textFound = this._collection.any(function(entry) { 836 | return entry[0] == marker; 837 | }.bind(this)); 838 | this._controls.editor.update(''); 839 | var option; 840 | this._collection.each(function(entry, index) { 841 | option = document.createElement('option'); 842 | option.value = entry[0]; 843 | option.selected = textFound ? entry[0] == marker : 0 == index; 844 | option.appendChild(document.createTextNode(entry[1])); 845 | this._controls.editor.appendChild(option); 846 | }.bind(this)); 847 | this._controls.editor.disabled = false; 848 | Field.scrollFreeActivate(this._controls.editor); 849 | } 850 | }); 851 | 852 | //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** 853 | //**** This only exists for a while, in order to let **** 854 | //**** users adapt to the new API. Read up on the new **** 855 | //**** API and convert your code to it ASAP! **** 856 | 857 | Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { 858 | if (!options) return; 859 | function fallback(name, expr) { 860 | if (name in options || expr === undefined) return; 861 | options[name] = expr; 862 | }; 863 | fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : 864 | options.cancelLink == options.cancelButton == false ? false : undefined))); 865 | fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : 866 | options.okLink == options.okButton == false ? false : undefined))); 867 | fallback('highlightColor', options.highlightcolor); 868 | fallback('highlightEndColor', options.highlightendcolor); 869 | }; 870 | 871 | Object.extend(Ajax.InPlaceEditor, { 872 | DefaultOptions: { 873 | ajaxOptions: { }, 874 | autoRows: 3, // Use when multi-line w/ rows == 1 875 | cancelControl: 'link', // 'link'|'button'|false 876 | cancelText: 'cancel', 877 | clickToEditText: 'Click to edit', 878 | externalControl: null, // id|elt 879 | externalControlOnly: false, 880 | fieldPostCreation: 'activate', // 'activate'|'focus'|false 881 | formClassName: 'inplaceeditor-form', 882 | formId: null, // id|elt 883 | highlightColor: '#ffff99', 884 | highlightEndColor: '#ffffff', 885 | hoverClassName: '', 886 | htmlResponse: true, 887 | loadingClassName: 'inplaceeditor-loading', 888 | loadingText: 'Loading...', 889 | okControl: 'button', // 'link'|'button'|false 890 | okText: 'ok', 891 | paramName: 'value', 892 | rows: 1, // If 1 and multi-line, uses autoRows 893 | savingClassName: 'inplaceeditor-saving', 894 | savingText: 'Saving...', 895 | size: 0, 896 | stripLoadedTextTags: false, 897 | submitOnBlur: false, 898 | textAfterControls: '', 899 | textBeforeControls: '', 900 | textBetweenControls: '' 901 | }, 902 | DefaultCallbacks: { 903 | callback: function(form) { 904 | return Form.serialize(form); 905 | }, 906 | onComplete: function(transport, element) { 907 | // For backward compatibility, this one is bound to the IPE, and passes 908 | // the element directly. It was too often customized, so we don't break it. 909 | new Effect.Highlight(element, { 910 | startcolor: this.options.highlightColor, keepBackgroundImage: true }); 911 | }, 912 | onEnterEditMode: null, 913 | onEnterHover: function(ipe) { 914 | ipe.element.style.backgroundColor = ipe.options.highlightColor; 915 | if (ipe._effect) 916 | ipe._effect.cancel(); 917 | }, 918 | onFailure: function(transport, ipe) { 919 | alert('Error communication with the server: ' + transport.responseText.stripTags()); 920 | }, 921 | onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. 922 | onLeaveEditMode: null, 923 | onLeaveHover: function(ipe) { 924 | ipe._effect = new Effect.Highlight(ipe.element, { 925 | startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, 926 | restorecolor: ipe._originalBackground, keepBackgroundImage: true 927 | }); 928 | } 929 | }, 930 | Listeners: { 931 | click: 'enterEditMode', 932 | keydown: 'checkForEscapeOrReturn', 933 | mouseover: 'enterHover', 934 | mouseout: 'leaveHover' 935 | } 936 | }); 937 | 938 | Ajax.InPlaceCollectionEditor.DefaultOptions = { 939 | loadingCollectionText: 'Loading options...' 940 | }; 941 | 942 | // Delayed observer, like Form.Element.Observer, 943 | // but waits for delay after last key input 944 | // Ideal for live-search fields 945 | 946 | Form.Element.DelayedObserver = Class.create({ 947 | initialize: function(element, delay, callback) { 948 | this.delay = delay || 0.5; 949 | this.element = $(element); 950 | this.callback = callback; 951 | this.timer = null; 952 | this.lastValue = $F(this.element); 953 | Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); 954 | }, 955 | delayedListener: function(event) { 956 | if(this.lastValue == $F(this.element)) return; 957 | if(this.timer) clearTimeout(this.timer); 958 | this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); 959 | this.lastValue = $F(this.element); 960 | }, 961 | onTimerEvent: function() { 962 | this.timer = null; 963 | this.callback(this.element, $F(this.element)); 964 | } 965 | }); -------------------------------------------------------------------------------- /public/javascripts/dragdrop.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 2 | 3 | // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // 5 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 6 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 7 | 8 | if(Object.isUndefined(Effect)) 9 | throw("dragdrop.js requires including script.aculo.us' effects.js library"); 10 | 11 | var Droppables = { 12 | drops: [], 13 | 14 | remove: function(element) { 15 | this.drops = this.drops.reject(function(d) { return d.element==$(element) }); 16 | }, 17 | 18 | add: function(element) { 19 | element = $(element); 20 | var options = Object.extend({ 21 | greedy: true, 22 | hoverclass: null, 23 | tree: false 24 | }, arguments[1] || { }); 25 | 26 | // cache containers 27 | if(options.containment) { 28 | options._containers = []; 29 | var containment = options.containment; 30 | if(Object.isArray(containment)) { 31 | containment.each( function(c) { options._containers.push($(c)) }); 32 | } else { 33 | options._containers.push($(containment)); 34 | } 35 | } 36 | 37 | if(options.accept) options.accept = [options.accept].flatten(); 38 | 39 | Element.makePositioned(element); // fix IE 40 | options.element = element; 41 | 42 | this.drops.push(options); 43 | }, 44 | 45 | findDeepestChild: function(drops) { 46 | deepest = drops[0]; 47 | 48 | for (i = 1; i < drops.length; ++i) 49 | if (Element.isParent(drops[i].element, deepest.element)) 50 | deepest = drops[i]; 51 | 52 | return deepest; 53 | }, 54 | 55 | isContained: function(element, drop) { 56 | var containmentNode; 57 | if(drop.tree) { 58 | containmentNode = element.treeNode; 59 | } else { 60 | containmentNode = element.parentNode; 61 | } 62 | return drop._containers.detect(function(c) { return containmentNode == c }); 63 | }, 64 | 65 | isAffected: function(point, element, drop) { 66 | return ( 67 | (drop.element!=element) && 68 | ((!drop._containers) || 69 | this.isContained(element, drop)) && 70 | ((!drop.accept) || 71 | (Element.classNames(element).detect( 72 | function(v) { return drop.accept.include(v) } ) )) && 73 | Position.within(drop.element, point[0], point[1]) ); 74 | }, 75 | 76 | deactivate: function(drop) { 77 | if(drop.hoverclass) 78 | Element.removeClassName(drop.element, drop.hoverclass); 79 | this.last_active = null; 80 | }, 81 | 82 | activate: function(drop) { 83 | if(drop.hoverclass) 84 | Element.addClassName(drop.element, drop.hoverclass); 85 | this.last_active = drop; 86 | }, 87 | 88 | show: function(point, element) { 89 | if(!this.drops.length) return; 90 | var drop, affected = []; 91 | 92 | this.drops.each( function(drop) { 93 | if(Droppables.isAffected(point, element, drop)) 94 | affected.push(drop); 95 | }); 96 | 97 | if(affected.length>0) 98 | drop = Droppables.findDeepestChild(affected); 99 | 100 | if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); 101 | if (drop) { 102 | Position.within(drop.element, point[0], point[1]); 103 | if(drop.onHover) 104 | drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); 105 | 106 | if (drop != this.last_active) Droppables.activate(drop); 107 | } 108 | }, 109 | 110 | fire: function(event, element) { 111 | if(!this.last_active) return; 112 | Position.prepare(); 113 | 114 | if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) 115 | if (this.last_active.onDrop) { 116 | this.last_active.onDrop(element, this.last_active.element, event); 117 | return true; 118 | } 119 | }, 120 | 121 | reset: function() { 122 | if(this.last_active) 123 | this.deactivate(this.last_active); 124 | } 125 | }; 126 | 127 | var Draggables = { 128 | drags: [], 129 | observers: [], 130 | 131 | register: function(draggable) { 132 | if(this.drags.length == 0) { 133 | this.eventMouseUp = this.endDrag.bindAsEventListener(this); 134 | this.eventMouseMove = this.updateDrag.bindAsEventListener(this); 135 | this.eventKeypress = this.keyPress.bindAsEventListener(this); 136 | 137 | Event.observe(document, "mouseup", this.eventMouseUp); 138 | Event.observe(document, "mousemove", this.eventMouseMove); 139 | Event.observe(document, "keypress", this.eventKeypress); 140 | } 141 | this.drags.push(draggable); 142 | }, 143 | 144 | unregister: function(draggable) { 145 | this.drags = this.drags.reject(function(d) { return d==draggable }); 146 | if(this.drags.length == 0) { 147 | Event.stopObserving(document, "mouseup", this.eventMouseUp); 148 | Event.stopObserving(document, "mousemove", this.eventMouseMove); 149 | Event.stopObserving(document, "keypress", this.eventKeypress); 150 | } 151 | }, 152 | 153 | activate: function(draggable) { 154 | if(draggable.options.delay) { 155 | this._timeout = setTimeout(function() { 156 | Draggables._timeout = null; 157 | window.focus(); 158 | Draggables.activeDraggable = draggable; 159 | }.bind(this), draggable.options.delay); 160 | } else { 161 | window.focus(); // allows keypress events if window isn't currently focused, fails for Safari 162 | this.activeDraggable = draggable; 163 | } 164 | }, 165 | 166 | deactivate: function() { 167 | this.activeDraggable = null; 168 | }, 169 | 170 | updateDrag: function(event) { 171 | if(!this.activeDraggable) return; 172 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 173 | // Mozilla-based browsers fire successive mousemove events with 174 | // the same coordinates, prevent needless redrawing (moz bug?) 175 | if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; 176 | this._lastPointer = pointer; 177 | 178 | this.activeDraggable.updateDrag(event, pointer); 179 | }, 180 | 181 | endDrag: function(event) { 182 | if(this._timeout) { 183 | clearTimeout(this._timeout); 184 | this._timeout = null; 185 | } 186 | if(!this.activeDraggable) return; 187 | this._lastPointer = null; 188 | this.activeDraggable.endDrag(event); 189 | this.activeDraggable = null; 190 | }, 191 | 192 | keyPress: function(event) { 193 | if(this.activeDraggable) 194 | this.activeDraggable.keyPress(event); 195 | }, 196 | 197 | addObserver: function(observer) { 198 | this.observers.push(observer); 199 | this._cacheObserverCallbacks(); 200 | }, 201 | 202 | removeObserver: function(element) { // element instead of observer fixes mem leaks 203 | this.observers = this.observers.reject( function(o) { return o.element==element }); 204 | this._cacheObserverCallbacks(); 205 | }, 206 | 207 | notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' 208 | if(this[eventName+'Count'] > 0) 209 | this.observers.each( function(o) { 210 | if(o[eventName]) o[eventName](eventName, draggable, event); 211 | }); 212 | if(draggable.options[eventName]) draggable.options[eventName](draggable, event); 213 | }, 214 | 215 | _cacheObserverCallbacks: function() { 216 | ['onStart','onEnd','onDrag'].each( function(eventName) { 217 | Draggables[eventName+'Count'] = Draggables.observers.select( 218 | function(o) { return o[eventName]; } 219 | ).length; 220 | }); 221 | } 222 | }; 223 | 224 | /*--------------------------------------------------------------------------*/ 225 | 226 | var Draggable = Class.create({ 227 | initialize: function(element) { 228 | var defaults = { 229 | handle: false, 230 | reverteffect: function(element, top_offset, left_offset) { 231 | var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; 232 | new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, 233 | queue: {scope:'_draggable', position:'end'} 234 | }); 235 | }, 236 | endeffect: function(element) { 237 | var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; 238 | new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 239 | queue: {scope:'_draggable', position:'end'}, 240 | afterFinish: function(){ 241 | Draggable._dragging[element] = false 242 | } 243 | }); 244 | }, 245 | zindex: 1000, 246 | revert: false, 247 | quiet: false, 248 | scroll: false, 249 | scrollSensitivity: 20, 250 | scrollSpeed: 15, 251 | snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } 252 | delay: 0 253 | }; 254 | 255 | if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) 256 | Object.extend(defaults, { 257 | starteffect: function(element) { 258 | element._opacity = Element.getOpacity(element); 259 | Draggable._dragging[element] = true; 260 | new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 261 | } 262 | }); 263 | 264 | var options = Object.extend(defaults, arguments[1] || { }); 265 | 266 | this.element = $(element); 267 | 268 | if(options.handle && Object.isString(options.handle)) 269 | this.handle = this.element.down('.'+options.handle, 0); 270 | 271 | if(!this.handle) this.handle = $(options.handle); 272 | if(!this.handle) this.handle = this.element; 273 | 274 | if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { 275 | options.scroll = $(options.scroll); 276 | this._isScrollChild = Element.childOf(this.element, options.scroll); 277 | } 278 | 279 | Element.makePositioned(this.element); // fix IE 280 | 281 | this.options = options; 282 | this.dragging = false; 283 | 284 | this.eventMouseDown = this.initDrag.bindAsEventListener(this); 285 | Event.observe(this.handle, "mousedown", this.eventMouseDown); 286 | 287 | Draggables.register(this); 288 | }, 289 | 290 | destroy: function() { 291 | Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); 292 | Draggables.unregister(this); 293 | }, 294 | 295 | currentDelta: function() { 296 | return([ 297 | parseInt(Element.getStyle(this.element,'left') || '0'), 298 | parseInt(Element.getStyle(this.element,'top') || '0')]); 299 | }, 300 | 301 | initDrag: function(event) { 302 | if(!Object.isUndefined(Draggable._dragging[this.element]) && 303 | Draggable._dragging[this.element]) return; 304 | if(Event.isLeftClick(event)) { 305 | // abort on form elements, fixes a Firefox issue 306 | var src = Event.element(event); 307 | if((tag_name = src.tagName.toUpperCase()) && ( 308 | tag_name=='INPUT' || 309 | tag_name=='SELECT' || 310 | tag_name=='OPTION' || 311 | tag_name=='BUTTON' || 312 | tag_name=='TEXTAREA')) return; 313 | 314 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 315 | var pos = this.element.cumulativeOffset(); 316 | this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); 317 | 318 | Draggables.activate(this); 319 | Event.stop(event); 320 | } 321 | }, 322 | 323 | startDrag: function(event) { 324 | this.dragging = true; 325 | if(!this.delta) 326 | this.delta = this.currentDelta(); 327 | 328 | if(this.options.zindex) { 329 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); 330 | this.element.style.zIndex = this.options.zindex; 331 | } 332 | 333 | if(this.options.ghosting) { 334 | this._clone = this.element.cloneNode(true); 335 | this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); 336 | if (!this._originallyAbsolute) 337 | Position.absolutize(this.element); 338 | this.element.parentNode.insertBefore(this._clone, this.element); 339 | } 340 | 341 | if(this.options.scroll) { 342 | if (this.options.scroll == window) { 343 | var where = this._getWindowScroll(this.options.scroll); 344 | this.originalScrollLeft = where.left; 345 | this.originalScrollTop = where.top; 346 | } else { 347 | this.originalScrollLeft = this.options.scroll.scrollLeft; 348 | this.originalScrollTop = this.options.scroll.scrollTop; 349 | } 350 | } 351 | 352 | Draggables.notify('onStart', this, event); 353 | 354 | if(this.options.starteffect) this.options.starteffect(this.element); 355 | }, 356 | 357 | updateDrag: function(event, pointer) { 358 | if(!this.dragging) this.startDrag(event); 359 | 360 | if(!this.options.quiet){ 361 | Position.prepare(); 362 | Droppables.show(pointer, this.element); 363 | } 364 | 365 | Draggables.notify('onDrag', this, event); 366 | 367 | this.draw(pointer); 368 | if(this.options.change) this.options.change(this); 369 | 370 | if(this.options.scroll) { 371 | this.stopScrolling(); 372 | 373 | var p; 374 | if (this.options.scroll == window) { 375 | with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } 376 | } else { 377 | p = Position.page(this.options.scroll); 378 | p[0] += this.options.scroll.scrollLeft + Position.deltaX; 379 | p[1] += this.options.scroll.scrollTop + Position.deltaY; 380 | p.push(p[0]+this.options.scroll.offsetWidth); 381 | p.push(p[1]+this.options.scroll.offsetHeight); 382 | } 383 | var speed = [0,0]; 384 | if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); 385 | if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); 386 | if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); 387 | if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); 388 | this.startScrolling(speed); 389 | } 390 | 391 | // fix AppleWebKit rendering 392 | if(Prototype.Browser.WebKit) window.scrollBy(0,0); 393 | 394 | Event.stop(event); 395 | }, 396 | 397 | finishDrag: function(event, success) { 398 | this.dragging = false; 399 | 400 | if(this.options.quiet){ 401 | Position.prepare(); 402 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 403 | Droppables.show(pointer, this.element); 404 | } 405 | 406 | if(this.options.ghosting) { 407 | if (!this._originallyAbsolute) 408 | Position.relativize(this.element); 409 | delete this._originallyAbsolute; 410 | Element.remove(this._clone); 411 | this._clone = null; 412 | } 413 | 414 | var dropped = false; 415 | if(success) { 416 | dropped = Droppables.fire(event, this.element); 417 | if (!dropped) dropped = false; 418 | } 419 | if(dropped && this.options.onDropped) this.options.onDropped(this.element); 420 | Draggables.notify('onEnd', this, event); 421 | 422 | var revert = this.options.revert; 423 | if(revert && Object.isFunction(revert)) revert = revert(this.element); 424 | 425 | var d = this.currentDelta(); 426 | if(revert && this.options.reverteffect) { 427 | if (dropped == 0 || revert != 'failure') 428 | this.options.reverteffect(this.element, 429 | d[1]-this.delta[1], d[0]-this.delta[0]); 430 | } else { 431 | this.delta = d; 432 | } 433 | 434 | if(this.options.zindex) 435 | this.element.style.zIndex = this.originalZ; 436 | 437 | if(this.options.endeffect) 438 | this.options.endeffect(this.element); 439 | 440 | Draggables.deactivate(this); 441 | Droppables.reset(); 442 | }, 443 | 444 | keyPress: function(event) { 445 | if(event.keyCode!=Event.KEY_ESC) return; 446 | this.finishDrag(event, false); 447 | Event.stop(event); 448 | }, 449 | 450 | endDrag: function(event) { 451 | if(!this.dragging) return; 452 | this.stopScrolling(); 453 | this.finishDrag(event, true); 454 | Event.stop(event); 455 | }, 456 | 457 | draw: function(point) { 458 | var pos = this.element.cumulativeOffset(); 459 | if(this.options.ghosting) { 460 | var r = Position.realOffset(this.element); 461 | pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; 462 | } 463 | 464 | var d = this.currentDelta(); 465 | pos[0] -= d[0]; pos[1] -= d[1]; 466 | 467 | if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { 468 | pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; 469 | pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; 470 | } 471 | 472 | var p = [0,1].map(function(i){ 473 | return (point[i]-pos[i]-this.offset[i]) 474 | }.bind(this)); 475 | 476 | if(this.options.snap) { 477 | if(Object.isFunction(this.options.snap)) { 478 | p = this.options.snap(p[0],p[1],this); 479 | } else { 480 | if(Object.isArray(this.options.snap)) { 481 | p = p.map( function(v, i) { 482 | return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); 483 | } else { 484 | p = p.map( function(v) { 485 | return (v/this.options.snap).round()*this.options.snap }.bind(this)); 486 | } 487 | }} 488 | 489 | var style = this.element.style; 490 | if((!this.options.constraint) || (this.options.constraint=='horizontal')) 491 | style.left = p[0] + "px"; 492 | if((!this.options.constraint) || (this.options.constraint=='vertical')) 493 | style.top = p[1] + "px"; 494 | 495 | if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering 496 | }, 497 | 498 | stopScrolling: function() { 499 | if(this.scrollInterval) { 500 | clearInterval(this.scrollInterval); 501 | this.scrollInterval = null; 502 | Draggables._lastScrollPointer = null; 503 | } 504 | }, 505 | 506 | startScrolling: function(speed) { 507 | if(!(speed[0] || speed[1])) return; 508 | this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; 509 | this.lastScrolled = new Date(); 510 | this.scrollInterval = setInterval(this.scroll.bind(this), 10); 511 | }, 512 | 513 | scroll: function() { 514 | var current = new Date(); 515 | var delta = current - this.lastScrolled; 516 | this.lastScrolled = current; 517 | if(this.options.scroll == window) { 518 | with (this._getWindowScroll(this.options.scroll)) { 519 | if (this.scrollSpeed[0] || this.scrollSpeed[1]) { 520 | var d = delta / 1000; 521 | this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); 522 | } 523 | } 524 | } else { 525 | this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; 526 | this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; 527 | } 528 | 529 | Position.prepare(); 530 | Droppables.show(Draggables._lastPointer, this.element); 531 | Draggables.notify('onDrag', this); 532 | if (this._isScrollChild) { 533 | Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); 534 | Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; 535 | Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; 536 | if (Draggables._lastScrollPointer[0] < 0) 537 | Draggables._lastScrollPointer[0] = 0; 538 | if (Draggables._lastScrollPointer[1] < 0) 539 | Draggables._lastScrollPointer[1] = 0; 540 | this.draw(Draggables._lastScrollPointer); 541 | } 542 | 543 | if(this.options.change) this.options.change(this); 544 | }, 545 | 546 | _getWindowScroll: function(w) { 547 | var T, L, W, H; 548 | with (w.document) { 549 | if (w.document.documentElement && documentElement.scrollTop) { 550 | T = documentElement.scrollTop; 551 | L = documentElement.scrollLeft; 552 | } else if (w.document.body) { 553 | T = body.scrollTop; 554 | L = body.scrollLeft; 555 | } 556 | if (w.innerWidth) { 557 | W = w.innerWidth; 558 | H = w.innerHeight; 559 | } else if (w.document.documentElement && documentElement.clientWidth) { 560 | W = documentElement.clientWidth; 561 | H = documentElement.clientHeight; 562 | } else { 563 | W = body.offsetWidth; 564 | H = body.offsetHeight; 565 | } 566 | } 567 | return { top: T, left: L, width: W, height: H }; 568 | } 569 | }); 570 | 571 | Draggable._dragging = { }; 572 | 573 | /*--------------------------------------------------------------------------*/ 574 | 575 | var SortableObserver = Class.create({ 576 | initialize: function(element, observer) { 577 | this.element = $(element); 578 | this.observer = observer; 579 | this.lastValue = Sortable.serialize(this.element); 580 | }, 581 | 582 | onStart: function() { 583 | this.lastValue = Sortable.serialize(this.element); 584 | }, 585 | 586 | onEnd: function() { 587 | Sortable.unmark(); 588 | if(this.lastValue != Sortable.serialize(this.element)) 589 | this.observer(this.element) 590 | } 591 | }); 592 | 593 | var Sortable = { 594 | SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, 595 | 596 | sortables: { }, 597 | 598 | _findRootElement: function(element) { 599 | while (element.tagName.toUpperCase() != "BODY") { 600 | if(element.id && Sortable.sortables[element.id]) return element; 601 | element = element.parentNode; 602 | } 603 | }, 604 | 605 | options: function(element) { 606 | element = Sortable._findRootElement($(element)); 607 | if(!element) return; 608 | return Sortable.sortables[element.id]; 609 | }, 610 | 611 | destroy: function(element){ 612 | element = $(element); 613 | var s = Sortable.sortables[element.id]; 614 | 615 | if(s) { 616 | Draggables.removeObserver(s.element); 617 | s.droppables.each(function(d){ Droppables.remove(d) }); 618 | s.draggables.invoke('destroy'); 619 | 620 | delete Sortable.sortables[s.element.id]; 621 | } 622 | }, 623 | 624 | create: function(element) { 625 | element = $(element); 626 | var options = Object.extend({ 627 | element: element, 628 | tag: 'li', // assumes li children, override with tag: 'tagname' 629 | dropOnEmpty: false, 630 | tree: false, 631 | treeTag: 'ul', 632 | overlap: 'vertical', // one of 'vertical', 'horizontal' 633 | constraint: 'vertical', // one of 'vertical', 'horizontal', false 634 | containment: element, // also takes array of elements (or id's); or false 635 | handle: false, // or a CSS class 636 | only: false, 637 | delay: 0, 638 | hoverclass: null, 639 | ghosting: false, 640 | quiet: false, 641 | scroll: false, 642 | scrollSensitivity: 20, 643 | scrollSpeed: 15, 644 | format: this.SERIALIZE_RULE, 645 | 646 | // these take arrays of elements or ids and can be 647 | // used for better initialization performance 648 | elements: false, 649 | handles: false, 650 | 651 | onChange: Prototype.emptyFunction, 652 | onUpdate: Prototype.emptyFunction 653 | }, arguments[1] || { }); 654 | 655 | // clear any old sortable with same element 656 | this.destroy(element); 657 | 658 | // build options for the draggables 659 | var options_for_draggable = { 660 | revert: true, 661 | quiet: options.quiet, 662 | scroll: options.scroll, 663 | scrollSpeed: options.scrollSpeed, 664 | scrollSensitivity: options.scrollSensitivity, 665 | delay: options.delay, 666 | ghosting: options.ghosting, 667 | constraint: options.constraint, 668 | handle: options.handle }; 669 | 670 | if(options.starteffect) 671 | options_for_draggable.starteffect = options.starteffect; 672 | 673 | if(options.reverteffect) 674 | options_for_draggable.reverteffect = options.reverteffect; 675 | else 676 | if(options.ghosting) options_for_draggable.reverteffect = function(element) { 677 | element.style.top = 0; 678 | element.style.left = 0; 679 | }; 680 | 681 | if(options.endeffect) 682 | options_for_draggable.endeffect = options.endeffect; 683 | 684 | if(options.zindex) 685 | options_for_draggable.zindex = options.zindex; 686 | 687 | // build options for the droppables 688 | var options_for_droppable = { 689 | overlap: options.overlap, 690 | containment: options.containment, 691 | tree: options.tree, 692 | hoverclass: options.hoverclass, 693 | onHover: Sortable.onHover 694 | }; 695 | 696 | var options_for_tree = { 697 | onHover: Sortable.onEmptyHover, 698 | overlap: options.overlap, 699 | containment: options.containment, 700 | hoverclass: options.hoverclass 701 | }; 702 | 703 | // fix for gecko engine 704 | Element.cleanWhitespace(element); 705 | 706 | options.draggables = []; 707 | options.droppables = []; 708 | 709 | // drop on empty handling 710 | if(options.dropOnEmpty || options.tree) { 711 | Droppables.add(element, options_for_tree); 712 | options.droppables.push(element); 713 | } 714 | 715 | (options.elements || this.findElements(element, options) || []).each( function(e,i) { 716 | var handle = options.handles ? $(options.handles[i]) : 717 | (options.handle ? $(e).select('.' + options.handle)[0] : e); 718 | options.draggables.push( 719 | new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); 720 | Droppables.add(e, options_for_droppable); 721 | if(options.tree) e.treeNode = element; 722 | options.droppables.push(e); 723 | }); 724 | 725 | if(options.tree) { 726 | (Sortable.findTreeElements(element, options) || []).each( function(e) { 727 | Droppables.add(e, options_for_tree); 728 | e.treeNode = element; 729 | options.droppables.push(e); 730 | }); 731 | } 732 | 733 | // keep reference 734 | this.sortables[element.identify()] = options; 735 | 736 | // for onupdate 737 | Draggables.addObserver(new SortableObserver(element, options.onUpdate)); 738 | 739 | }, 740 | 741 | // return all suitable-for-sortable elements in a guaranteed order 742 | findElements: function(element, options) { 743 | return Element.findChildren( 744 | element, options.only, options.tree ? true : false, options.tag); 745 | }, 746 | 747 | findTreeElements: function(element, options) { 748 | return Element.findChildren( 749 | element, options.only, options.tree ? true : false, options.treeTag); 750 | }, 751 | 752 | onHover: function(element, dropon, overlap) { 753 | if(Element.isParent(dropon, element)) return; 754 | 755 | if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { 756 | return; 757 | } else if(overlap>0.5) { 758 | Sortable.mark(dropon, 'before'); 759 | if(dropon.previousSibling != element) { 760 | var oldParentNode = element.parentNode; 761 | element.style.visibility = "hidden"; // fix gecko rendering 762 | dropon.parentNode.insertBefore(element, dropon); 763 | if(dropon.parentNode!=oldParentNode) 764 | Sortable.options(oldParentNode).onChange(element); 765 | Sortable.options(dropon.parentNode).onChange(element); 766 | } 767 | } else { 768 | Sortable.mark(dropon, 'after'); 769 | var nextElement = dropon.nextSibling || null; 770 | if(nextElement != element) { 771 | var oldParentNode = element.parentNode; 772 | element.style.visibility = "hidden"; // fix gecko rendering 773 | dropon.parentNode.insertBefore(element, nextElement); 774 | if(dropon.parentNode!=oldParentNode) 775 | Sortable.options(oldParentNode).onChange(element); 776 | Sortable.options(dropon.parentNode).onChange(element); 777 | } 778 | } 779 | }, 780 | 781 | onEmptyHover: function(element, dropon, overlap) { 782 | var oldParentNode = element.parentNode; 783 | var droponOptions = Sortable.options(dropon); 784 | 785 | if(!Element.isParent(dropon, element)) { 786 | var index; 787 | 788 | var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); 789 | var child = null; 790 | 791 | if(children) { 792 | var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); 793 | 794 | for (index = 0; index < children.length; index += 1) { 795 | if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { 796 | offset -= Element.offsetSize (children[index], droponOptions.overlap); 797 | } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { 798 | child = index + 1 < children.length ? children[index + 1] : null; 799 | break; 800 | } else { 801 | child = children[index]; 802 | break; 803 | } 804 | } 805 | } 806 | 807 | dropon.insertBefore(element, child); 808 | 809 | Sortable.options(oldParentNode).onChange(element); 810 | droponOptions.onChange(element); 811 | } 812 | }, 813 | 814 | unmark: function() { 815 | if(Sortable._marker) Sortable._marker.hide(); 816 | }, 817 | 818 | mark: function(dropon, position) { 819 | // mark on ghosting only 820 | var sortable = Sortable.options(dropon.parentNode); 821 | if(sortable && !sortable.ghosting) return; 822 | 823 | if(!Sortable._marker) { 824 | Sortable._marker = 825 | ($('dropmarker') || Element.extend(document.createElement('DIV'))). 826 | hide().addClassName('dropmarker').setStyle({position:'absolute'}); 827 | document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); 828 | } 829 | var offsets = dropon.cumulativeOffset(); 830 | Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); 831 | 832 | if(position=='after') 833 | if(sortable.overlap == 'horizontal') 834 | Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); 835 | else 836 | Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); 837 | 838 | Sortable._marker.show(); 839 | }, 840 | 841 | _tree: function(element, options, parent) { 842 | var children = Sortable.findElements(element, options) || []; 843 | 844 | for (var i = 0; i < children.length; ++i) { 845 | var match = children[i].id.match(options.format); 846 | 847 | if (!match) continue; 848 | 849 | var child = { 850 | id: encodeURIComponent(match ? match[1] : null), 851 | element: element, 852 | parent: parent, 853 | children: [], 854 | position: parent.children.length, 855 | container: $(children[i]).down(options.treeTag) 856 | }; 857 | 858 | /* Get the element containing the children and recurse over it */ 859 | if (child.container) 860 | this._tree(child.container, options, child); 861 | 862 | parent.children.push (child); 863 | } 864 | 865 | return parent; 866 | }, 867 | 868 | tree: function(element) { 869 | element = $(element); 870 | var sortableOptions = this.options(element); 871 | var options = Object.extend({ 872 | tag: sortableOptions.tag, 873 | treeTag: sortableOptions.treeTag, 874 | only: sortableOptions.only, 875 | name: element.id, 876 | format: sortableOptions.format 877 | }, arguments[1] || { }); 878 | 879 | var root = { 880 | id: null, 881 | parent: null, 882 | children: [], 883 | container: element, 884 | position: 0 885 | }; 886 | 887 | return Sortable._tree(element, options, root); 888 | }, 889 | 890 | /* Construct a [i] index for a particular node */ 891 | _constructIndex: function(node) { 892 | var index = ''; 893 | do { 894 | if (node.id) index = '[' + node.position + ']' + index; 895 | } while ((node = node.parent) != null); 896 | return index; 897 | }, 898 | 899 | sequence: function(element) { 900 | element = $(element); 901 | var options = Object.extend(this.options(element), arguments[1] || { }); 902 | 903 | return $(this.findElements(element, options) || []).map( function(item) { 904 | return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; 905 | }); 906 | }, 907 | 908 | setSequence: function(element, new_sequence) { 909 | element = $(element); 910 | var options = Object.extend(this.options(element), arguments[2] || { }); 911 | 912 | var nodeMap = { }; 913 | this.findElements(element, options).each( function(n) { 914 | if (n.id.match(options.format)) 915 | nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; 916 | n.parentNode.removeChild(n); 917 | }); 918 | 919 | new_sequence.each(function(ident) { 920 | var n = nodeMap[ident]; 921 | if (n) { 922 | n[1].appendChild(n[0]); 923 | delete nodeMap[ident]; 924 | } 925 | }); 926 | }, 927 | 928 | serialize: function(element) { 929 | element = $(element); 930 | var options = Object.extend(Sortable.options(element), arguments[1] || { }); 931 | var name = encodeURIComponent( 932 | (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); 933 | 934 | if (options.tree) { 935 | return Sortable.tree(element, arguments[1]).children.map( function (item) { 936 | return [name + Sortable._constructIndex(item) + "[id]=" + 937 | encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); 938 | }).flatten().join('&'); 939 | } else { 940 | return Sortable.sequence(element, arguments[1]).map( function(item) { 941 | return name + "[]=" + encodeURIComponent(item); 942 | }).join('&'); 943 | } 944 | } 945 | }; 946 | 947 | // Returns true if child is contained within element 948 | Element.isParent = function(child, element) { 949 | if (!child.parentNode || child == element) return false; 950 | if (child.parentNode == element) return true; 951 | return Element.isParent(child.parentNode, element); 952 | }; 953 | 954 | Element.findChildren = function(element, only, recursive, tagName) { 955 | if(!element.hasChildNodes()) return null; 956 | tagName = tagName.toUpperCase(); 957 | if(only) only = [only].flatten(); 958 | var elements = []; 959 | $A(element.childNodes).each( function(e) { 960 | if(e.tagName && e.tagName.toUpperCase()==tagName && 961 | (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) 962 | elements.push(e); 963 | if(recursive) { 964 | var grandchildren = Element.findChildren(e, only, recursive, tagName); 965 | if(grandchildren) elements.push(grandchildren); 966 | } 967 | }); 968 | 969 | return (elements.length>0 ? elements.flatten() : []); 970 | }; 971 | 972 | Element.offsetSize = function (element, type) { 973 | return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; 974 | }; -------------------------------------------------------------------------------- /public/javascripts/effects.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us effects.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 2 | 3 | // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // Contributors: 5 | // Justin Palmer (http://encytemedia.com/) 6 | // Mark Pilgrim (http://diveintomark.org/) 7 | // Martin Bialasinki 8 | // 9 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 10 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 11 | 12 | // converts rgb() and #xxx to #xxxxxx format, 13 | // returns self (or first argument) if not convertable 14 | String.prototype.parseColor = function() { 15 | var color = '#'; 16 | if (this.slice(0,4) == 'rgb(') { 17 | var cols = this.slice(4,this.length-1).split(','); 18 | var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); 19 | } else { 20 | if (this.slice(0,1) == '#') { 21 | if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); 22 | if (this.length==7) color = this.toLowerCase(); 23 | } 24 | } 25 | return (color.length==7 ? color : (arguments[0] || this)); 26 | }; 27 | 28 | /*--------------------------------------------------------------------------*/ 29 | 30 | Element.collectTextNodes = function(element) { 31 | return $A($(element).childNodes).collect( function(node) { 32 | return (node.nodeType==3 ? node.nodeValue : 33 | (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); 34 | }).flatten().join(''); 35 | }; 36 | 37 | Element.collectTextNodesIgnoreClass = function(element, className) { 38 | return $A($(element).childNodes).collect( function(node) { 39 | return (node.nodeType==3 ? node.nodeValue : 40 | ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? 41 | Element.collectTextNodesIgnoreClass(node, className) : '')); 42 | }).flatten().join(''); 43 | }; 44 | 45 | Element.setContentZoom = function(element, percent) { 46 | element = $(element); 47 | element.setStyle({fontSize: (percent/100) + 'em'}); 48 | if (Prototype.Browser.WebKit) window.scrollBy(0,0); 49 | return element; 50 | }; 51 | 52 | Element.getInlineOpacity = function(element){ 53 | return $(element).style.opacity || ''; 54 | }; 55 | 56 | Element.forceRerendering = function(element) { 57 | try { 58 | element = $(element); 59 | var n = document.createTextNode(' '); 60 | element.appendChild(n); 61 | element.removeChild(n); 62 | } catch(e) { } 63 | }; 64 | 65 | /*--------------------------------------------------------------------------*/ 66 | 67 | var Effect = { 68 | _elementDoesNotExistError: { 69 | name: 'ElementDoesNotExistError', 70 | message: 'The specified DOM element does not exist, but is required for this effect to operate' 71 | }, 72 | Transitions: { 73 | linear: Prototype.K, 74 | sinoidal: function(pos) { 75 | return (-Math.cos(pos*Math.PI)/2) + .5; 76 | }, 77 | reverse: function(pos) { 78 | return 1-pos; 79 | }, 80 | flicker: function(pos) { 81 | var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; 82 | return pos > 1 ? 1 : pos; 83 | }, 84 | wobble: function(pos) { 85 | return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; 86 | }, 87 | pulse: function(pos, pulses) { 88 | return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; 89 | }, 90 | spring: function(pos) { 91 | return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); 92 | }, 93 | none: function(pos) { 94 | return 0; 95 | }, 96 | full: function(pos) { 97 | return 1; 98 | } 99 | }, 100 | DefaultOptions: { 101 | duration: 1.0, // seconds 102 | fps: 100, // 100= assume 66fps max. 103 | sync: false, // true for combining 104 | from: 0.0, 105 | to: 1.0, 106 | delay: 0.0, 107 | queue: 'parallel' 108 | }, 109 | tagifyText: function(element) { 110 | var tagifyStyle = 'position:relative'; 111 | if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; 112 | 113 | element = $(element); 114 | $A(element.childNodes).each( function(child) { 115 | if (child.nodeType==3) { 116 | child.nodeValue.toArray().each( function(character) { 117 | element.insertBefore( 118 | new Element('span', {style: tagifyStyle}).update( 119 | character == ' ' ? String.fromCharCode(160) : character), 120 | child); 121 | }); 122 | Element.remove(child); 123 | } 124 | }); 125 | }, 126 | multiple: function(element, effect) { 127 | var elements; 128 | if (((typeof element == 'object') || 129 | Object.isFunction(element)) && 130 | (element.length)) 131 | elements = element; 132 | else 133 | elements = $(element).childNodes; 134 | 135 | var options = Object.extend({ 136 | speed: 0.1, 137 | delay: 0.0 138 | }, arguments[2] || { }); 139 | var masterDelay = options.delay; 140 | 141 | $A(elements).each( function(element, index) { 142 | new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); 143 | }); 144 | }, 145 | PAIRS: { 146 | 'slide': ['SlideDown','SlideUp'], 147 | 'blind': ['BlindDown','BlindUp'], 148 | 'appear': ['Appear','Fade'] 149 | }, 150 | toggle: function(element, effect, options) { 151 | element = $(element); 152 | effect = (effect || 'appear').toLowerCase(); 153 | 154 | return Effect[ Effect.PAIRS[ effect ][ element.visible() ? 1 : 0 ] ](element, Object.extend({ 155 | queue: { position:'end', scope:(element.id || 'global'), limit: 1 } 156 | }, options || {})); 157 | } 158 | }; 159 | 160 | Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; 161 | 162 | /* ------------- core effects ------------- */ 163 | 164 | Effect.ScopedQueue = Class.create(Enumerable, { 165 | initialize: function() { 166 | this.effects = []; 167 | this.interval = null; 168 | }, 169 | _each: function(iterator) { 170 | this.effects._each(iterator); 171 | }, 172 | add: function(effect) { 173 | var timestamp = new Date().getTime(); 174 | 175 | var position = Object.isString(effect.options.queue) ? 176 | effect.options.queue : effect.options.queue.position; 177 | 178 | switch(position) { 179 | case 'front': 180 | // move unstarted effects after this effect 181 | this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { 182 | e.startOn += effect.finishOn; 183 | e.finishOn += effect.finishOn; 184 | }); 185 | break; 186 | case 'with-last': 187 | timestamp = this.effects.pluck('startOn').max() || timestamp; 188 | break; 189 | case 'end': 190 | // start effect after last queued effect has finished 191 | timestamp = this.effects.pluck('finishOn').max() || timestamp; 192 | break; 193 | } 194 | 195 | effect.startOn += timestamp; 196 | effect.finishOn += timestamp; 197 | 198 | if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) 199 | this.effects.push(effect); 200 | 201 | if (!this.interval) 202 | this.interval = setInterval(this.loop.bind(this), 15); 203 | }, 204 | remove: function(effect) { 205 | this.effects = this.effects.reject(function(e) { return e==effect }); 206 | if (this.effects.length == 0) { 207 | clearInterval(this.interval); 208 | this.interval = null; 209 | } 210 | }, 211 | loop: function() { 212 | var timePos = new Date().getTime(); 213 | for(var i=0, len=this.effects.length;i= this.startOn) { 274 | if (timePos >= this.finishOn) { 275 | this.render(1.0); 276 | this.cancel(); 277 | this.event('beforeFinish'); 278 | if (this.finish) this.finish(); 279 | this.event('afterFinish'); 280 | return; 281 | } 282 | var pos = (timePos - this.startOn) / this.totalTime, 283 | frame = (pos * this.totalFrames).round(); 284 | if (frame > this.currentFrame) { 285 | this.render(pos); 286 | this.currentFrame = frame; 287 | } 288 | } 289 | }, 290 | cancel: function() { 291 | if (!this.options.sync) 292 | Effect.Queues.get(Object.isString(this.options.queue) ? 293 | 'global' : this.options.queue.scope).remove(this); 294 | this.state = 'finished'; 295 | }, 296 | event: function(eventName) { 297 | if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); 298 | if (this.options[eventName]) this.options[eventName](this); 299 | }, 300 | inspect: function() { 301 | var data = $H(); 302 | for(property in this) 303 | if (!Object.isFunction(this[property])) data.set(property, this[property]); 304 | return '#'; 305 | } 306 | }); 307 | 308 | Effect.Parallel = Class.create(Effect.Base, { 309 | initialize: function(effects) { 310 | this.effects = effects || []; 311 | this.start(arguments[1]); 312 | }, 313 | update: function(position) { 314 | this.effects.invoke('render', position); 315 | }, 316 | finish: function(position) { 317 | this.effects.each( function(effect) { 318 | effect.render(1.0); 319 | effect.cancel(); 320 | effect.event('beforeFinish'); 321 | if (effect.finish) effect.finish(position); 322 | effect.event('afterFinish'); 323 | }); 324 | } 325 | }); 326 | 327 | Effect.Tween = Class.create(Effect.Base, { 328 | initialize: function(object, from, to) { 329 | object = Object.isString(object) ? $(object) : object; 330 | var args = $A(arguments), method = args.last(), 331 | options = args.length == 5 ? args[3] : null; 332 | this.method = Object.isFunction(method) ? method.bind(object) : 333 | Object.isFunction(object[method]) ? object[method].bind(object) : 334 | function(value) { object[method] = value }; 335 | this.start(Object.extend({ from: from, to: to }, options || { })); 336 | }, 337 | update: function(position) { 338 | this.method(position); 339 | } 340 | }); 341 | 342 | Effect.Event = Class.create(Effect.Base, { 343 | initialize: function() { 344 | this.start(Object.extend({ duration: 0 }, arguments[0] || { })); 345 | }, 346 | update: Prototype.emptyFunction 347 | }); 348 | 349 | Effect.Opacity = Class.create(Effect.Base, { 350 | initialize: function(element) { 351 | this.element = $(element); 352 | if (!this.element) throw(Effect._elementDoesNotExistError); 353 | // make this work on IE on elements without 'layout' 354 | if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) 355 | this.element.setStyle({zoom: 1}); 356 | var options = Object.extend({ 357 | from: this.element.getOpacity() || 0.0, 358 | to: 1.0 359 | }, arguments[1] || { }); 360 | this.start(options); 361 | }, 362 | update: function(position) { 363 | this.element.setOpacity(position); 364 | } 365 | }); 366 | 367 | Effect.Move = Class.create(Effect.Base, { 368 | initialize: function(element) { 369 | this.element = $(element); 370 | if (!this.element) throw(Effect._elementDoesNotExistError); 371 | var options = Object.extend({ 372 | x: 0, 373 | y: 0, 374 | mode: 'relative' 375 | }, arguments[1] || { }); 376 | this.start(options); 377 | }, 378 | setup: function() { 379 | this.element.makePositioned(); 380 | this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); 381 | this.originalTop = parseFloat(this.element.getStyle('top') || '0'); 382 | if (this.options.mode == 'absolute') { 383 | this.options.x = this.options.x - this.originalLeft; 384 | this.options.y = this.options.y - this.originalTop; 385 | } 386 | }, 387 | update: function(position) { 388 | this.element.setStyle({ 389 | left: (this.options.x * position + this.originalLeft).round() + 'px', 390 | top: (this.options.y * position + this.originalTop).round() + 'px' 391 | }); 392 | } 393 | }); 394 | 395 | // for backwards compatibility 396 | Effect.MoveBy = function(element, toTop, toLeft) { 397 | return new Effect.Move(element, 398 | Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); 399 | }; 400 | 401 | Effect.Scale = Class.create(Effect.Base, { 402 | initialize: function(element, percent) { 403 | this.element = $(element); 404 | if (!this.element) throw(Effect._elementDoesNotExistError); 405 | var options = Object.extend({ 406 | scaleX: true, 407 | scaleY: true, 408 | scaleContent: true, 409 | scaleFromCenter: false, 410 | scaleMode: 'box', // 'box' or 'contents' or { } with provided values 411 | scaleFrom: 100.0, 412 | scaleTo: percent 413 | }, arguments[2] || { }); 414 | this.start(options); 415 | }, 416 | setup: function() { 417 | this.restoreAfterFinish = this.options.restoreAfterFinish || false; 418 | this.elementPositioning = this.element.getStyle('position'); 419 | 420 | this.originalStyle = { }; 421 | ['top','left','width','height','fontSize'].each( function(k) { 422 | this.originalStyle[k] = this.element.style[k]; 423 | }.bind(this)); 424 | 425 | this.originalTop = this.element.offsetTop; 426 | this.originalLeft = this.element.offsetLeft; 427 | 428 | var fontSize = this.element.getStyle('font-size') || '100%'; 429 | ['em','px','%','pt'].each( function(fontSizeType) { 430 | if (fontSize.indexOf(fontSizeType)>0) { 431 | this.fontSize = parseFloat(fontSize); 432 | this.fontSizeType = fontSizeType; 433 | } 434 | }.bind(this)); 435 | 436 | this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; 437 | 438 | this.dims = null; 439 | if (this.options.scaleMode=='box') 440 | this.dims = [this.element.offsetHeight, this.element.offsetWidth]; 441 | if (/^content/.test(this.options.scaleMode)) 442 | this.dims = [this.element.scrollHeight, this.element.scrollWidth]; 443 | if (!this.dims) 444 | this.dims = [this.options.scaleMode.originalHeight, 445 | this.options.scaleMode.originalWidth]; 446 | }, 447 | update: function(position) { 448 | var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); 449 | if (this.options.scaleContent && this.fontSize) 450 | this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); 451 | this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); 452 | }, 453 | finish: function(position) { 454 | if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); 455 | }, 456 | setDimensions: function(height, width) { 457 | var d = { }; 458 | if (this.options.scaleX) d.width = width.round() + 'px'; 459 | if (this.options.scaleY) d.height = height.round() + 'px'; 460 | if (this.options.scaleFromCenter) { 461 | var topd = (height - this.dims[0])/2; 462 | var leftd = (width - this.dims[1])/2; 463 | if (this.elementPositioning == 'absolute') { 464 | if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; 465 | if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; 466 | } else { 467 | if (this.options.scaleY) d.top = -topd + 'px'; 468 | if (this.options.scaleX) d.left = -leftd + 'px'; 469 | } 470 | } 471 | this.element.setStyle(d); 472 | } 473 | }); 474 | 475 | Effect.Highlight = Class.create(Effect.Base, { 476 | initialize: function(element) { 477 | this.element = $(element); 478 | if (!this.element) throw(Effect._elementDoesNotExistError); 479 | var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); 480 | this.start(options); 481 | }, 482 | setup: function() { 483 | // Prevent executing on elements not in the layout flow 484 | if (this.element.getStyle('display')=='none') { this.cancel(); return; } 485 | // Disable background image during the effect 486 | this.oldStyle = { }; 487 | if (!this.options.keepBackgroundImage) { 488 | this.oldStyle.backgroundImage = this.element.getStyle('background-image'); 489 | this.element.setStyle({backgroundImage: 'none'}); 490 | } 491 | if (!this.options.endcolor) 492 | this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); 493 | if (!this.options.restorecolor) 494 | this.options.restorecolor = this.element.getStyle('background-color'); 495 | // init color calculations 496 | this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); 497 | this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); 498 | }, 499 | update: function(position) { 500 | this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ 501 | return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); 502 | }, 503 | finish: function() { 504 | this.element.setStyle(Object.extend(this.oldStyle, { 505 | backgroundColor: this.options.restorecolor 506 | })); 507 | } 508 | }); 509 | 510 | Effect.ScrollTo = function(element) { 511 | var options = arguments[1] || { }, 512 | scrollOffsets = document.viewport.getScrollOffsets(), 513 | elementOffsets = $(element).cumulativeOffset(); 514 | 515 | if (options.offset) elementOffsets[1] += options.offset; 516 | 517 | return new Effect.Tween(null, 518 | scrollOffsets.top, 519 | elementOffsets[1], 520 | options, 521 | function(p){ scrollTo(scrollOffsets.left, p.round()); } 522 | ); 523 | }; 524 | 525 | /* ------------- combination effects ------------- */ 526 | 527 | Effect.Fade = function(element) { 528 | element = $(element); 529 | var oldOpacity = element.getInlineOpacity(); 530 | var options = Object.extend({ 531 | from: element.getOpacity() || 1.0, 532 | to: 0.0, 533 | afterFinishInternal: function(effect) { 534 | if (effect.options.to!=0) return; 535 | effect.element.hide().setStyle({opacity: oldOpacity}); 536 | } 537 | }, arguments[1] || { }); 538 | return new Effect.Opacity(element,options); 539 | }; 540 | 541 | Effect.Appear = function(element) { 542 | element = $(element); 543 | var options = Object.extend({ 544 | from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), 545 | to: 1.0, 546 | // force Safari to render floated elements properly 547 | afterFinishInternal: function(effect) { 548 | effect.element.forceRerendering(); 549 | }, 550 | beforeSetup: function(effect) { 551 | effect.element.setOpacity(effect.options.from).show(); 552 | }}, arguments[1] || { }); 553 | return new Effect.Opacity(element,options); 554 | }; 555 | 556 | Effect.Puff = function(element) { 557 | element = $(element); 558 | var oldStyle = { 559 | opacity: element.getInlineOpacity(), 560 | position: element.getStyle('position'), 561 | top: element.style.top, 562 | left: element.style.left, 563 | width: element.style.width, 564 | height: element.style.height 565 | }; 566 | return new Effect.Parallel( 567 | [ new Effect.Scale(element, 200, 568 | { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 569 | new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 570 | Object.extend({ duration: 1.0, 571 | beforeSetupInternal: function(effect) { 572 | Position.absolutize(effect.effects[0].element); 573 | }, 574 | afterFinishInternal: function(effect) { 575 | effect.effects[0].element.hide().setStyle(oldStyle); } 576 | }, arguments[1] || { }) 577 | ); 578 | }; 579 | 580 | Effect.BlindUp = function(element) { 581 | element = $(element); 582 | element.makeClipping(); 583 | return new Effect.Scale(element, 0, 584 | Object.extend({ scaleContent: false, 585 | scaleX: false, 586 | restoreAfterFinish: true, 587 | afterFinishInternal: function(effect) { 588 | effect.element.hide().undoClipping(); 589 | } 590 | }, arguments[1] || { }) 591 | ); 592 | }; 593 | 594 | Effect.BlindDown = function(element) { 595 | element = $(element); 596 | var elementDimensions = element.getDimensions(); 597 | return new Effect.Scale(element, 100, Object.extend({ 598 | scaleContent: false, 599 | scaleX: false, 600 | scaleFrom: 0, 601 | scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, 602 | restoreAfterFinish: true, 603 | afterSetup: function(effect) { 604 | effect.element.makeClipping().setStyle({height: '0px'}).show(); 605 | }, 606 | afterFinishInternal: function(effect) { 607 | effect.element.undoClipping(); 608 | } 609 | }, arguments[1] || { })); 610 | }; 611 | 612 | Effect.SwitchOff = function(element) { 613 | element = $(element); 614 | var oldOpacity = element.getInlineOpacity(); 615 | return new Effect.Appear(element, Object.extend({ 616 | duration: 0.4, 617 | from: 0, 618 | transition: Effect.Transitions.flicker, 619 | afterFinishInternal: function(effect) { 620 | new Effect.Scale(effect.element, 1, { 621 | duration: 0.3, scaleFromCenter: true, 622 | scaleX: false, scaleContent: false, restoreAfterFinish: true, 623 | beforeSetup: function(effect) { 624 | effect.element.makePositioned().makeClipping(); 625 | }, 626 | afterFinishInternal: function(effect) { 627 | effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); 628 | } 629 | }); 630 | } 631 | }, arguments[1] || { })); 632 | }; 633 | 634 | Effect.DropOut = function(element) { 635 | element = $(element); 636 | var oldStyle = { 637 | top: element.getStyle('top'), 638 | left: element.getStyle('left'), 639 | opacity: element.getInlineOpacity() }; 640 | return new Effect.Parallel( 641 | [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 642 | new Effect.Opacity(element, { sync: true, to: 0.0 }) ], 643 | Object.extend( 644 | { duration: 0.5, 645 | beforeSetup: function(effect) { 646 | effect.effects[0].element.makePositioned(); 647 | }, 648 | afterFinishInternal: function(effect) { 649 | effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); 650 | } 651 | }, arguments[1] || { })); 652 | }; 653 | 654 | Effect.Shake = function(element) { 655 | element = $(element); 656 | var options = Object.extend({ 657 | distance: 20, 658 | duration: 0.5 659 | }, arguments[1] || {}); 660 | var distance = parseFloat(options.distance); 661 | var split = parseFloat(options.duration) / 10.0; 662 | var oldStyle = { 663 | top: element.getStyle('top'), 664 | left: element.getStyle('left') }; 665 | return new Effect.Move(element, 666 | { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { 667 | new Effect.Move(effect.element, 668 | { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 669 | new Effect.Move(effect.element, 670 | { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 671 | new Effect.Move(effect.element, 672 | { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 673 | new Effect.Move(effect.element, 674 | { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 675 | new Effect.Move(effect.element, 676 | { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { 677 | effect.element.undoPositioned().setStyle(oldStyle); 678 | }}); }}); }}); }}); }}); }}); 679 | }; 680 | 681 | Effect.SlideDown = function(element) { 682 | element = $(element).cleanWhitespace(); 683 | // SlideDown need to have the content of the element wrapped in a container element with fixed height! 684 | var oldInnerBottom = element.down().getStyle('bottom'); 685 | var elementDimensions = element.getDimensions(); 686 | return new Effect.Scale(element, 100, Object.extend({ 687 | scaleContent: false, 688 | scaleX: false, 689 | scaleFrom: window.opera ? 0 : 1, 690 | scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, 691 | restoreAfterFinish: true, 692 | afterSetup: function(effect) { 693 | effect.element.makePositioned(); 694 | effect.element.down().makePositioned(); 695 | if (window.opera) effect.element.setStyle({top: ''}); 696 | effect.element.makeClipping().setStyle({height: '0px'}).show(); 697 | }, 698 | afterUpdateInternal: function(effect) { 699 | effect.element.down().setStyle({bottom: 700 | (effect.dims[0] - effect.element.clientHeight) + 'px' }); 701 | }, 702 | afterFinishInternal: function(effect) { 703 | effect.element.undoClipping().undoPositioned(); 704 | effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } 705 | }, arguments[1] || { }) 706 | ); 707 | }; 708 | 709 | Effect.SlideUp = function(element) { 710 | element = $(element).cleanWhitespace(); 711 | var oldInnerBottom = element.down().getStyle('bottom'); 712 | var elementDimensions = element.getDimensions(); 713 | return new Effect.Scale(element, window.opera ? 0 : 1, 714 | Object.extend({ scaleContent: false, 715 | scaleX: false, 716 | scaleMode: 'box', 717 | scaleFrom: 100, 718 | scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, 719 | restoreAfterFinish: true, 720 | afterSetup: function(effect) { 721 | effect.element.makePositioned(); 722 | effect.element.down().makePositioned(); 723 | if (window.opera) effect.element.setStyle({top: ''}); 724 | effect.element.makeClipping().show(); 725 | }, 726 | afterUpdateInternal: function(effect) { 727 | effect.element.down().setStyle({bottom: 728 | (effect.dims[0] - effect.element.clientHeight) + 'px' }); 729 | }, 730 | afterFinishInternal: function(effect) { 731 | effect.element.hide().undoClipping().undoPositioned(); 732 | effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); 733 | } 734 | }, arguments[1] || { }) 735 | ); 736 | }; 737 | 738 | // Bug in opera makes the TD containing this element expand for a instance after finish 739 | Effect.Squish = function(element) { 740 | return new Effect.Scale(element, window.opera ? 1 : 0, { 741 | restoreAfterFinish: true, 742 | beforeSetup: function(effect) { 743 | effect.element.makeClipping(); 744 | }, 745 | afterFinishInternal: function(effect) { 746 | effect.element.hide().undoClipping(); 747 | } 748 | }); 749 | }; 750 | 751 | Effect.Grow = function(element) { 752 | element = $(element); 753 | var options = Object.extend({ 754 | direction: 'center', 755 | moveTransition: Effect.Transitions.sinoidal, 756 | scaleTransition: Effect.Transitions.sinoidal, 757 | opacityTransition: Effect.Transitions.full 758 | }, arguments[1] || { }); 759 | var oldStyle = { 760 | top: element.style.top, 761 | left: element.style.left, 762 | height: element.style.height, 763 | width: element.style.width, 764 | opacity: element.getInlineOpacity() }; 765 | 766 | var dims = element.getDimensions(); 767 | var initialMoveX, initialMoveY; 768 | var moveX, moveY; 769 | 770 | switch (options.direction) { 771 | case 'top-left': 772 | initialMoveX = initialMoveY = moveX = moveY = 0; 773 | break; 774 | case 'top-right': 775 | initialMoveX = dims.width; 776 | initialMoveY = moveY = 0; 777 | moveX = -dims.width; 778 | break; 779 | case 'bottom-left': 780 | initialMoveX = moveX = 0; 781 | initialMoveY = dims.height; 782 | moveY = -dims.height; 783 | break; 784 | case 'bottom-right': 785 | initialMoveX = dims.width; 786 | initialMoveY = dims.height; 787 | moveX = -dims.width; 788 | moveY = -dims.height; 789 | break; 790 | case 'center': 791 | initialMoveX = dims.width / 2; 792 | initialMoveY = dims.height / 2; 793 | moveX = -dims.width / 2; 794 | moveY = -dims.height / 2; 795 | break; 796 | } 797 | 798 | return new Effect.Move(element, { 799 | x: initialMoveX, 800 | y: initialMoveY, 801 | duration: 0.01, 802 | beforeSetup: function(effect) { 803 | effect.element.hide().makeClipping().makePositioned(); 804 | }, 805 | afterFinishInternal: function(effect) { 806 | new Effect.Parallel( 807 | [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), 808 | new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), 809 | new Effect.Scale(effect.element, 100, { 810 | scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 811 | sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) 812 | ], Object.extend({ 813 | beforeSetup: function(effect) { 814 | effect.effects[0].element.setStyle({height: '0px'}).show(); 815 | }, 816 | afterFinishInternal: function(effect) { 817 | effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); 818 | } 819 | }, options) 820 | ); 821 | } 822 | }); 823 | }; 824 | 825 | Effect.Shrink = function(element) { 826 | element = $(element); 827 | var options = Object.extend({ 828 | direction: 'center', 829 | moveTransition: Effect.Transitions.sinoidal, 830 | scaleTransition: Effect.Transitions.sinoidal, 831 | opacityTransition: Effect.Transitions.none 832 | }, arguments[1] || { }); 833 | var oldStyle = { 834 | top: element.style.top, 835 | left: element.style.left, 836 | height: element.style.height, 837 | width: element.style.width, 838 | opacity: element.getInlineOpacity() }; 839 | 840 | var dims = element.getDimensions(); 841 | var moveX, moveY; 842 | 843 | switch (options.direction) { 844 | case 'top-left': 845 | moveX = moveY = 0; 846 | break; 847 | case 'top-right': 848 | moveX = dims.width; 849 | moveY = 0; 850 | break; 851 | case 'bottom-left': 852 | moveX = 0; 853 | moveY = dims.height; 854 | break; 855 | case 'bottom-right': 856 | moveX = dims.width; 857 | moveY = dims.height; 858 | break; 859 | case 'center': 860 | moveX = dims.width / 2; 861 | moveY = dims.height / 2; 862 | break; 863 | } 864 | 865 | return new Effect.Parallel( 866 | [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), 867 | new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), 868 | new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) 869 | ], Object.extend({ 870 | beforeStartInternal: function(effect) { 871 | effect.effects[0].element.makePositioned().makeClipping(); 872 | }, 873 | afterFinishInternal: function(effect) { 874 | effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } 875 | }, options) 876 | ); 877 | }; 878 | 879 | Effect.Pulsate = function(element) { 880 | element = $(element); 881 | var options = arguments[1] || { }, 882 | oldOpacity = element.getInlineOpacity(), 883 | transition = options.transition || Effect.Transitions.linear, 884 | reverser = function(pos){ 885 | return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); 886 | }; 887 | 888 | return new Effect.Opacity(element, 889 | Object.extend(Object.extend({ duration: 2.0, from: 0, 890 | afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } 891 | }, options), {transition: reverser})); 892 | }; 893 | 894 | Effect.Fold = function(element) { 895 | element = $(element); 896 | var oldStyle = { 897 | top: element.style.top, 898 | left: element.style.left, 899 | width: element.style.width, 900 | height: element.style.height }; 901 | element.makeClipping(); 902 | return new Effect.Scale(element, 5, Object.extend({ 903 | scaleContent: false, 904 | scaleX: false, 905 | afterFinishInternal: function(effect) { 906 | new Effect.Scale(element, 1, { 907 | scaleContent: false, 908 | scaleY: false, 909 | afterFinishInternal: function(effect) { 910 | effect.element.hide().undoClipping().setStyle(oldStyle); 911 | } }); 912 | }}, arguments[1] || { })); 913 | }; 914 | 915 | Effect.Morph = Class.create(Effect.Base, { 916 | initialize: function(element) { 917 | this.element = $(element); 918 | if (!this.element) throw(Effect._elementDoesNotExistError); 919 | var options = Object.extend({ 920 | style: { } 921 | }, arguments[1] || { }); 922 | 923 | if (!Object.isString(options.style)) this.style = $H(options.style); 924 | else { 925 | if (options.style.include(':')) 926 | this.style = options.style.parseStyle(); 927 | else { 928 | this.element.addClassName(options.style); 929 | this.style = $H(this.element.getStyles()); 930 | this.element.removeClassName(options.style); 931 | var css = this.element.getStyles(); 932 | this.style = this.style.reject(function(style) { 933 | return style.value == css[style.key]; 934 | }); 935 | options.afterFinishInternal = function(effect) { 936 | effect.element.addClassName(effect.options.style); 937 | effect.transforms.each(function(transform) { 938 | effect.element.style[transform.style] = ''; 939 | }); 940 | }; 941 | } 942 | } 943 | this.start(options); 944 | }, 945 | 946 | setup: function(){ 947 | function parseColor(color){ 948 | if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; 949 | color = color.parseColor(); 950 | return $R(0,2).map(function(i){ 951 | return parseInt( color.slice(i*2+1,i*2+3), 16 ); 952 | }); 953 | } 954 | this.transforms = this.style.map(function(pair){ 955 | var property = pair[0], value = pair[1], unit = null; 956 | 957 | if (value.parseColor('#zzzzzz') != '#zzzzzz') { 958 | value = value.parseColor(); 959 | unit = 'color'; 960 | } else if (property == 'opacity') { 961 | value = parseFloat(value); 962 | if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) 963 | this.element.setStyle({zoom: 1}); 964 | } else if (Element.CSS_LENGTH.test(value)) { 965 | var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); 966 | value = parseFloat(components[1]); 967 | unit = (components.length == 3) ? components[2] : null; 968 | } 969 | 970 | var originalValue = this.element.getStyle(property); 971 | return { 972 | style: property.camelize(), 973 | originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), 974 | targetValue: unit=='color' ? parseColor(value) : value, 975 | unit: unit 976 | }; 977 | }.bind(this)).reject(function(transform){ 978 | return ( 979 | (transform.originalValue == transform.targetValue) || 980 | ( 981 | transform.unit != 'color' && 982 | (isNaN(transform.originalValue) || isNaN(transform.targetValue)) 983 | ) 984 | ); 985 | }); 986 | }, 987 | update: function(position) { 988 | var style = { }, transform, i = this.transforms.length; 989 | while(i--) 990 | style[(transform = this.transforms[i]).style] = 991 | transform.unit=='color' ? '#'+ 992 | (Math.round(transform.originalValue[0]+ 993 | (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + 994 | (Math.round(transform.originalValue[1]+ 995 | (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + 996 | (Math.round(transform.originalValue[2]+ 997 | (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : 998 | (transform.originalValue + 999 | (transform.targetValue - transform.originalValue) * position).toFixed(3) + 1000 | (transform.unit === null ? '' : transform.unit); 1001 | this.element.setStyle(style, true); 1002 | } 1003 | }); 1004 | 1005 | Effect.Transform = Class.create({ 1006 | initialize: function(tracks){ 1007 | this.tracks = []; 1008 | this.options = arguments[1] || { }; 1009 | this.addTracks(tracks); 1010 | }, 1011 | addTracks: function(tracks){ 1012 | tracks.each(function(track){ 1013 | track = $H(track); 1014 | var data = track.values().first(); 1015 | this.tracks.push($H({ 1016 | ids: track.keys().first(), 1017 | effect: Effect.Morph, 1018 | options: { style: data } 1019 | })); 1020 | }.bind(this)); 1021 | return this; 1022 | }, 1023 | play: function(){ 1024 | return new Effect.Parallel( 1025 | this.tracks.map(function(track){ 1026 | var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); 1027 | var elements = [$(ids) || $$(ids)].flatten(); 1028 | return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); 1029 | }).flatten(), 1030 | this.options 1031 | ); 1032 | } 1033 | }); 1034 | 1035 | Element.CSS_PROPERTIES = $w( 1036 | 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + 1037 | 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + 1038 | 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + 1039 | 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + 1040 | 'fontSize fontWeight height left letterSpacing lineHeight ' + 1041 | 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ 1042 | 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + 1043 | 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + 1044 | 'right textIndent top width wordSpacing zIndex'); 1045 | 1046 | Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; 1047 | 1048 | String.__parseStyleElement = document.createElement('div'); 1049 | String.prototype.parseStyle = function(){ 1050 | var style, styleRules = $H(); 1051 | if (Prototype.Browser.WebKit) 1052 | style = new Element('div',{style:this}).style; 1053 | else { 1054 | String.__parseStyleElement.innerHTML = '
    '; 1055 | style = String.__parseStyleElement.childNodes[0].style; 1056 | } 1057 | 1058 | Element.CSS_PROPERTIES.each(function(property){ 1059 | if (style[property]) styleRules.set(property, style[property]); 1060 | }); 1061 | 1062 | if (Prototype.Browser.IE && this.include('opacity')) 1063 | styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); 1064 | 1065 | return styleRules; 1066 | }; 1067 | 1068 | if (document.defaultView && document.defaultView.getComputedStyle) { 1069 | Element.getStyles = function(element) { 1070 | var css = document.defaultView.getComputedStyle($(element), null); 1071 | return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { 1072 | styles[property] = css[property]; 1073 | return styles; 1074 | }); 1075 | }; 1076 | } else { 1077 | Element.getStyles = function(element) { 1078 | element = $(element); 1079 | var css = element.currentStyle, styles; 1080 | styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { 1081 | results[property] = css[property]; 1082 | return results; 1083 | }); 1084 | if (!styles.opacity) styles.opacity = element.getOpacity(); 1085 | return styles; 1086 | }; 1087 | } 1088 | 1089 | Effect.Methods = { 1090 | morph: function(element, style) { 1091 | element = $(element); 1092 | new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); 1093 | return element; 1094 | }, 1095 | visualEffect: function(element, effect, options) { 1096 | element = $(element); 1097 | var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); 1098 | new Effect[klass](element, options); 1099 | return element; 1100 | }, 1101 | highlight: function(element, options) { 1102 | element = $(element); 1103 | new Effect.Highlight(element, options); 1104 | return element; 1105 | } 1106 | }; 1107 | 1108 | $w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ 1109 | 'pulsate shake puff squish switchOff dropOut').each( 1110 | function(effect) { 1111 | Effect.Methods[effect] = function(element, options){ 1112 | element = $(element); 1113 | Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); 1114 | return element; 1115 | }; 1116 | } 1117 | ); 1118 | 1119 | $w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( 1120 | function(f) { Effect.Methods[f] = Element[f]; } 1121 | ); 1122 | 1123 | Element.addMethods(Effect.Methods); -------------------------------------------------------------------------------- /public/javascripts/rails.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // Technique from Juriy Zaytsev 3 | // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ 4 | function isEventSupported(eventName) { 5 | var el = document.createElement('div'); 6 | eventName = 'on' + eventName; 7 | var isSupported = (eventName in el); 8 | if (!isSupported) { 9 | el.setAttribute(eventName, 'return;'); 10 | isSupported = typeof el[eventName] == 'function'; 11 | } 12 | el = null; 13 | return isSupported; 14 | } 15 | 16 | function isForm(element) { 17 | return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM' 18 | } 19 | 20 | function isInput(element) { 21 | if (Object.isElement(element)) { 22 | var name = element.nodeName.toUpperCase() 23 | return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA' 24 | } 25 | else return false 26 | } 27 | 28 | var submitBubbles = isEventSupported('submit'), 29 | changeBubbles = isEventSupported('change') 30 | 31 | if (!submitBubbles || !changeBubbles) { 32 | // augment the Event.Handler class to observe custom events when needed 33 | Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap( 34 | function(init, element, eventName, selector, callback) { 35 | init(element, eventName, selector, callback) 36 | // is the handler being attached to an element that doesn't support this event? 37 | if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) || 38 | (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) { 39 | // "submit" => "emulated:submit" 40 | this.eventName = 'emulated:' + this.eventName 41 | } 42 | } 43 | ) 44 | } 45 | 46 | if (!submitBubbles) { 47 | // discover forms on the page by observing focus events which always bubble 48 | document.on('focusin', 'form', function(focusEvent, form) { 49 | // special handler for the real "submit" event (one-time operation) 50 | if (!form.retrieve('emulated:submit')) { 51 | form.on('submit', function(submitEvent) { 52 | var emulated = form.fire('emulated:submit', submitEvent, true) 53 | // if custom event received preventDefault, cancel the real one too 54 | if (emulated.returnValue === false) submitEvent.preventDefault() 55 | }) 56 | form.store('emulated:submit', true) 57 | } 58 | }) 59 | } 60 | 61 | if (!changeBubbles) { 62 | // discover form inputs on the page 63 | document.on('focusin', 'input, select, texarea', function(focusEvent, input) { 64 | // special handler for real "change" events 65 | if (!input.retrieve('emulated:change')) { 66 | input.on('change', function(changeEvent) { 67 | input.fire('emulated:change', changeEvent, true) 68 | }) 69 | input.store('emulated:change', true) 70 | } 71 | }) 72 | } 73 | 74 | function handleRemote(element) { 75 | var method, url, params; 76 | 77 | var event = element.fire("ajax:before"); 78 | if (event.stopped) return false; 79 | 80 | if (element.tagName.toLowerCase() === 'form') { 81 | method = element.readAttribute('method') || 'post'; 82 | url = element.readAttribute('action'); 83 | params = element.serialize(); 84 | } else { 85 | method = element.readAttribute('data-method') || 'get'; 86 | url = element.readAttribute('href'); 87 | params = {}; 88 | } 89 | 90 | new Ajax.Request(url, { 91 | method: method, 92 | parameters: params, 93 | evalScripts: true, 94 | 95 | onComplete: function(request) { element.fire("ajax:complete", request); }, 96 | onSuccess: function(request) { element.fire("ajax:success", request); }, 97 | onFailure: function(request) { element.fire("ajax:failure", request); } 98 | }); 99 | 100 | element.fire("ajax:after"); 101 | } 102 | 103 | function handleMethod(element) { 104 | var method = element.readAttribute('data-method'), 105 | url = element.readAttribute('href'), 106 | csrf_param = $$('meta[name=csrf-param]')[0], 107 | csrf_token = $$('meta[name=csrf-token]')[0]; 108 | 109 | var form = new Element('form', { method: "POST", action: url, style: "display: none;" }); 110 | element.parentNode.insert(form); 111 | 112 | if (method !== 'post') { 113 | var field = new Element('input', { type: 'hidden', name: '_method', value: method }); 114 | form.insert(field); 115 | } 116 | 117 | if (csrf_param) { 118 | var param = csrf_param.readAttribute('content'), 119 | token = csrf_token.readAttribute('content'), 120 | field = new Element('input', { type: 'hidden', name: param, value: token }); 121 | form.insert(field); 122 | } 123 | 124 | form.submit(); 125 | } 126 | 127 | 128 | document.on("click", "*[data-confirm]", function(event, element) { 129 | var message = element.readAttribute('data-confirm'); 130 | if (!confirm(message)) event.stop(); 131 | }); 132 | 133 | document.on("click", "a[data-remote]", function(event, element) { 134 | if (event.stopped) return; 135 | handleRemote(element); 136 | event.stop(); 137 | }); 138 | 139 | document.on("click", "a[data-method]", function(event, element) { 140 | if (event.stopped) return; 141 | handleMethod(element); 142 | event.stop(); 143 | }); 144 | 145 | document.on("submit", function(event) { 146 | var element = event.findElement(), 147 | message = element.readAttribute('data-confirm'); 148 | if (message && !confirm(message)) { 149 | event.stop(); 150 | return false; 151 | } 152 | 153 | var inputs = element.select("input[type=submit][data-disable-with]"); 154 | inputs.each(function(input) { 155 | input.disabled = true; 156 | input.writeAttribute('data-original-value', input.value); 157 | input.value = input.readAttribute('data-disable-with'); 158 | }); 159 | 160 | var element = event.findElement("form[data-remote]"); 161 | if (element) { 162 | handleRemote(element); 163 | event.stop(); 164 | } 165 | }); 166 | 167 | document.on("ajax:after", "form", function(event, element) { 168 | var inputs = element.select("input[type=submit][disabled=true][data-disable-with]"); 169 | inputs.each(function(input) { 170 | input.value = input.readAttribute('data-original-value'); 171 | input.removeAttribute('data-original-value'); 172 | input.disabled = false; 173 | }); 174 | }); 175 | })(); 176 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/stylesheets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salex/rails3-subdomain-devise/a80e2b816d15c49ebe896eb157abd9c7d03dfb72/public/stylesheets/.gitkeep -------------------------------------------------------------------------------- /public/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | ul.hmenu { 2 | list-style: none; 3 | margin: 0 0 2em; 4 | padding: 0; 5 | } 6 | 7 | ul.hmenu li { 8 | display: inline; 9 | } 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /template.rb: -------------------------------------------------------------------------------- 1 | # Application Generator Template 2 | # Modifies a Rails app to set up subdomains with Devise 3 | # Usage: rails new app_name -m http://github.com/fortuity/rails3-subdomain-devise/raw/master/template.rb 4 | 5 | # More info: http://github.com/fortuity/rails3-subdomain-devise/ 6 | 7 | # If you are customizing this template, you can use any methods provided by Thor::Actions 8 | # http://rdoc.info/rdoc/wycats/thor/blob/f939a3e8a854616784cac1dcff04ef4f3ee5f7ff/Thor/Actions.html 9 | # and Rails::Generators::Actions 10 | # http://github.com/rails/rails/blob/master/railties/lib/rails/generators/actions.rb 11 | 12 | puts "Modifying a new Rails app to set up subdomains with Devise..." 13 | puts "Any problems? See http://github.com/fortuity/rails3-subdomain-devise/issues" 14 | 15 | if yes?('Would you like to use the Haml template system? (yes/no)') 16 | haml_flag = true 17 | else 18 | haml_flag = false 19 | end 20 | 21 | if yes?('Do you want to install the Heroku gem so you can deploy to Heroku? (yes/no)') 22 | heroku_flag = true 23 | else 24 | heroku_flag = false 25 | end 26 | 27 | puts "setting up source control with 'git'..." 28 | # specific to Mac OS X 29 | append_file '.gitignore' do 30 | '.DS_Store' 31 | end 32 | git :init 33 | git :add => '.' 34 | git :commit => "-m 'Initial commit of unmodified new Rails app'" 35 | 36 | puts "removing unneeded files..." 37 | run 'rm public/index.html' 38 | run 'rm public/favicon.ico' 39 | run 'rm public/images/rails.png' 40 | run 'rm README' 41 | run 'touch README' 42 | 43 | puts "banning spiders from your site by changing robots.txt..." 44 | gsub_file 'public/robots.txt', /# User-Agent/, 'User-Agent' 45 | gsub_file 'public/robots.txt', /# Disallow/, 'Disallow' 46 | 47 | puts "setting up the Gemfile..." 48 | run 'rm Gemfile' 49 | create_file 'Gemfile', "source 'http://rubygems.org'\n" 50 | gem 'rails', '3.0.0.rc' 51 | gem 'sqlite3-ruby', :require => 'sqlite3' 52 | gem 'devise', '1.1.1' 53 | gem 'friendly_id', '3.1.1.1' 54 | 55 | if heroku_flag 56 | puts "adding Heroku gem to the Gemfile..." 57 | gem 'heroku', '1.9.13', :group => :development 58 | end 59 | 60 | if haml_flag 61 | puts "setting up Gemfile for Haml..." 62 | append_file 'Gemfile', "\n# Bundle gems needed for Haml\n" 63 | gem 'haml', '3.0.14' 64 | gem "rails3-generators", :group => :development 65 | # the folowing gems are used to generate Devise views for Haml 66 | gem "hpricot", :group => :development 67 | gem "ruby_parser", :group => :development 68 | end 69 | 70 | puts "installing gems (takes a few minutes!)..." 71 | run 'bundle install' 72 | 73 | if haml_flag 74 | puts "modifying 'config/application.rb' file for Haml..." 75 | inject_into_file 'config/application.rb', :after => "# Configure the default encoding used in templates for Ruby 1.9.\n" do 76 | <<-RUBY 77 | config.generators do |g| 78 | g.template_engine :haml 79 | end 80 | RUBY 81 | end 82 | end 83 | 84 | puts "prevent logging of passwords" 85 | gsub_file 'config/application.rb', /:password/, ':password, :password_confirmation' 86 | 87 | puts "setting up a migration for use with FriendlyId..." 88 | run 'rails generate friendly_id' 89 | 90 | puts "creating 'config/initializers/devise.rb' Devise configuration file..." 91 | run 'rails generate devise:install' 92 | run 'rails generate devise:views' 93 | 94 | puts "modifying environment configuration files for Devise..." 95 | gsub_file 'config/environments/development.rb', /# Don't care if the mailer can't send/, '### ActionMailer Config' 96 | gsub_file 'config/environments/development.rb', /config.action_mailer.raise_delivery_errors = false/ do 97 | <<-RUBY 98 | config.action_mailer.default_url_options = { :host => 'localhost:3000' } 99 | # A dummy setup for development - no deliveries, but logged 100 | config.action_mailer.delivery_method = :smtp 101 | config.action_mailer.perform_deliveries = false 102 | config.action_mailer.raise_delivery_errors = true 103 | config.action_mailer.default :charset => "utf-8" 104 | RUBY 105 | end 106 | gsub_file 'config/environments/production.rb', /config.i18n.fallbacks = true/ do 107 | <<-RUBY 108 | config.i18n.fallbacks = true 109 | 110 | config.action_mailer.default_url_options = { :host => 'yourhost.com' } 111 | ### ActionMailer Config 112 | # Setup for production - deliveries, no errors raised 113 | config.action_mailer.delivery_method = :smtp 114 | config.action_mailer.perform_deliveries = true 115 | config.action_mailer.raise_delivery_errors = false 116 | config.action_mailer.default :charset => "utf-8" 117 | RUBY 118 | end 119 | 120 | puts "creating a User model and modifying routes for Devise..." 121 | run 'rails generate devise User' 122 | 123 | puts "setting up the User model" 124 | run 'rm app/models/user.rb' 125 | create_file 'app/models/user.rb' do 126 | <<-RUBY 127 | class User < ActiveRecord::Base 128 | has_many :subdomains, :dependent => :destroy 129 | devise :database_authenticatable, :registerable, 130 | :recoverable, :rememberable, :trackable, :validatable 131 | validates_presence_of :name 132 | validates_uniqueness_of :name, :email, :case_sensitive => false 133 | attr_accessible :name, :email, :password, :password_confirmation, :remember_me 134 | has_friendly_id :name, :use_slug => true, :strip_non_ascii => true 135 | end 136 | RUBY 137 | end 138 | 139 | puts "creating a database migration to add 'name' to the User table" 140 | generate(:migration, "AddNameToUsers name:string") 141 | 142 | puts "modifying the default Devise user registration to add 'name'..." 143 | if haml_flag 144 | inject_into_file "app/views/devise/registrations/edit.html.haml", :after => "= devise_error_messages!\n" do 145 | <<-RUBY 146 | %p 147 | = f.label :name 148 | %br/ 149 | = f.text_field :name 150 | RUBY 151 | end 152 | else 153 | inject_into_file "app/views/devise/registrations/edit.html.erb", :after => "<%= devise_error_messages! %>\n" do 154 | <<-RUBY 155 |

    <%= f.label :name %>
    156 | <%= f.text_field :name %>

    157 | RUBY 158 | end 159 | end 160 | 161 | if haml_flag 162 | inject_into_file "app/views/devise/registrations/new.html.haml", :after => "= devise_error_messages!\n" do 163 | <<-RUBY 164 | %p 165 | = f.label :name 166 | %br/ 167 | = f.text_field :name 168 | RUBY 169 | end 170 | else 171 | inject_into_file "app/views/devise/registrations/new.html.erb", :after => "<%= devise_error_messages! %>\n" do 172 | <<-RUBY 173 |

    <%= f.label :name %>
    174 | <%= f.text_field :name %>

    175 | RUBY 176 | end 177 | end 178 | 179 | puts "creating a Subdomain model..." 180 | generate(:model, "Subdomain name:string user:references") 181 | 182 | puts "setting up the Subdomain model" 183 | run 'rm app/models/subdomain.rb' 184 | create_file 'app/models/subdomain.rb' do 185 | <<-RUBY 186 | class Subdomain < ActiveRecord::Base 187 | belongs_to :user 188 | has_friendly_id :name, :use_slug => true, :strip_non_ascii => true 189 | validates_uniqueness_of :name, :case_sensitive => false 190 | validates_presence_of :name 191 | end 192 | RUBY 193 | end 194 | 195 | puts "setting up the Site model" 196 | create_file 'app/models/site.rb' do 197 | <<-RUBY 198 | class Site < Subdomain 199 | end 200 | RUBY 201 | end 202 | 203 | puts "generating controller and views to display users" 204 | generate(:controller, "users index show") 205 | gsub_file 'config/routes.rb', /get \"users\/index\"/, '' 206 | gsub_file 'config/routes.rb', /get \"users\/show\"/, '' 207 | 208 | inject_into_file "app/controllers/users_controller.rb", :after => "def index\n" do 209 | <<-RUBY 210 | @users = User.all 211 | RUBY 212 | end 213 | 214 | inject_into_file "app/controllers/users_controller.rb", :after => "def show\n" do 215 | <<-RUBY 216 | @user = User.find(params[:id]) 217 | RUBY 218 | end 219 | 220 | if haml_flag 221 | run 'rm app/views/users/show.html.haml' 222 | # we have to use single-quote-style-heredoc to avoid interpolation 223 | create_file 'app/views/users/show.html.haml' do <<-'FILE' 224 | %h1= @user.name 225 | %p 226 | Email: #{@user.email} 227 | = link_to 'Edit', edit_user_registration_path 228 | | 229 | \#{link_to 'List of Users', users_path} 230 | %h3 231 | = @user.name 232 | Subdomains 233 | %table 234 | - @user.subdomains.each do |subdomain| 235 | %tr 236 | %td= link_to subdomain.name, subdomain 237 | %td= link_to 'Edit', edit_subdomain_path(subdomain) 238 | %td= link_to 'Destroy', subdomain, :confirm => 'Are you sure?', :method => :delete 239 | %td= link_to "Visit #{root_url(:subdomain => subdomain.name)}", root_url(:subdomain => subdomain.name) 240 | %br/ 241 | = link_to "Add New Subdomain", new_user_subdomain_path(@user) 242 | FILE 243 | end 244 | else 245 | run 'rm app/views/users/show.html.erb' 246 | # we have to use single-quote-style-heredoc to avoid interpolation 247 | create_file 'app/views/users/show.html.erb' do <<-'FILE' 248 |

    <%= @user.name %>

    249 |

    Email: <%= @user.email %>

    250 | <%= link_to 'Edit', edit_user_registration_path %> | 251 | <%= link_to 'List of Users', users_path %> 252 |

    <%= @user.name %>'s Subdomains

    253 | 254 | <% @user.subdomains.each do |subdomain| %> 255 | 256 | 257 | 258 | 259 | 260 | 261 | <% end %> 262 |
    <%= link_to subdomain.name, subdomain %><%= link_to 'Edit', edit_subdomain_path(subdomain) %><%= link_to 'Destroy', subdomain, :confirm => 'Are you sure?', :method => :delete %><%= link_to "Visit #{root_url(:subdomain => subdomain.name)}", root_url(:subdomain => subdomain.name) %>
    263 |
    264 | <%= link_to "Add New Subdomain", new_user_subdomain_path(@user) %> 265 | FILE 266 | end 267 | end 268 | 269 | if haml_flag 270 | run 'rm app/views/users/index.html.haml' 271 | # we have to use single-quote-style-heredoc to avoid interpolation 272 | create_file 'app/views/users/index.html.haml' do <<-'FILE' 273 | %h1 Users 274 | %table 275 | - @users.each do |user| 276 | %tr 277 | %td= link_to user.name, user 278 | FILE 279 | end 280 | else 281 | run 'rm app/views/users/index.html.erb' 282 | create_file 'app/views/users/index.html.erb' do <<-FILE 283 |

    Users

    284 | 285 | <% @users.each do |user| %> 286 | 287 | 288 | 289 | <% end %> 290 |
    <%= link_to user.name, user %>
    291 | FILE 292 | end 293 | end 294 | 295 | if haml_flag 296 | create_file "app/views/devise/menu/_login_items.html.haml" do <<-'FILE' 297 | - if user_signed_in? 298 | %li 299 | = link_to('Logout', destroy_user_session_path) 300 | - else 301 | %li 302 | = link_to('Login', new_user_session_path) 303 | %li 304 | User: 305 | - if current_user 306 | = current_user.name 307 | - else 308 | (not logged in) 309 | FILE 310 | end 311 | else 312 | create_file "app/views/devise/menu/_login_items.html.erb" do <<-FILE 313 | <% if user_signed_in? %> 314 |
  • 315 | <%= link_to('Logout', destroy_user_session_path) %> 316 |
  • 317 | <% else %> 318 |
  • 319 | <%= link_to('Login', new_user_session_path) %> 320 |
  • 321 | <% end %> 322 |
  • 323 | User: 324 | <% if current_user %> 325 | <%= current_user.name %> 326 | <% else %> 327 | (not logged in) 328 | <% end %> 329 |
  • 330 | FILE 331 | end 332 | end 333 | 334 | if haml_flag 335 | create_file "app/views/devise/menu/_registration_items.html.haml" do <<-'FILE' 336 | - if user_signed_in? 337 | %li 338 | = link_to('Edit account', edit_user_registration_path) 339 | - else 340 | %li 341 | = link_to('Sign up', new_user_registration_path) 342 | FILE 343 | end 344 | else 345 | create_file "app/views/devise/menu/_registration_items.html.erb" do <<-FILE 346 | <% if user_signed_in? %> 347 |
  • 348 | <%= link_to('Edit account', edit_user_registration_path) %> 349 |
  • 350 | <% else %> 351 |
  • 352 | <%= link_to('Sign up', new_user_registration_path) %> 353 |
  • 354 | <% end %> 355 | FILE 356 | end 357 | end 358 | 359 | if haml_flag 360 | run 'rm app/views/layouts/application.html.erb' 361 | create_file 'app/views/layouts/application.html.haml' do <<-FILE 362 | !!! 363 | %html 364 | %head 365 | %title Testapp 366 | = stylesheet_link_tag :all 367 | = javascript_include_tag :defaults 368 | = csrf_meta_tag 369 | %body 370 | %ul.hmenu 371 | = render 'devise/menu/registration_items' 372 | = render 'devise/menu/login_items' 373 | %p{:style => "color: green"}= notice 374 | %p{:style => "color: red"}= alert 375 | = yield 376 | FILE 377 | end 378 | else 379 | inject_into_file 'app/views/layouts/application.html.erb', :after => "\n" do 380 | <<-RUBY 381 |
      382 | <%= render 'devise/menu/registration_items' %> 383 | <%= render 'devise/menu/login_items' %> 384 |
    385 |

    <%= notice %>

    386 |

    <%= alert %>

    387 | RUBY 388 | end 389 | end 390 | 391 | create_file 'public/stylesheets/application.css' do <<-FILE 392 | ul.hmenu { 393 | list-style: none; 394 | margin: 0 0 2em; 395 | padding: 0; 396 | } 397 | 398 | ul.hmenu li { 399 | display: inline; 400 | } 401 | FILE 402 | end 403 | 404 | puts "create a home controller and view" 405 | generate(:controller, "home index") 406 | gsub_file 'config/routes.rb', /get \"home\/index\"/, '' 407 | 408 | if haml_flag 409 | run 'rm app/views/home/index.html.haml' 410 | # we have to use single-quote-style-heredoc to avoid interpolation 411 | create_file 'app/views/home/index.html.haml' do <<-'FILE' 412 | %h1 Rails3-Subdomain-Devise 413 | %p= link_to "View List of Users", users_path 414 | FILE 415 | end 416 | else 417 | run 'rm app/views/home/index.html.erb' 418 | create_file 'app/views/home/index.html.erb' do <<-FILE 419 |

    Rails3-Subdomain-Devise

    420 |

    <%= link_to "View List of Users", users_path %>

    421 | FILE 422 | end 423 | end 424 | 425 | puts "create a controller and views to manage subdomains" 426 | generate(:scaffold_controller, "Subdomains") 427 | run 'rm app/controllers/subdomains_controller.rb' 428 | create_file 'app/controllers/subdomains_controller.rb' do <<-FILE 429 | class SubdomainsController < ApplicationController 430 | before_filter :authenticate_user!, :except => [:index, :show] 431 | before_filter :find_user, :except => [:index, :show] 432 | respond_to :html 433 | 434 | def index 435 | @subdomains = Subdomain.all 436 | respond_with(@subdomains) 437 | end 438 | 439 | def show 440 | @subdomain = Subdomain.find(params[:id]) 441 | respond_with(@subdomain) 442 | end 443 | 444 | def new 445 | @subdomain = Subdomain.new(:user => @user) 446 | respond_with(@subdomain) 447 | end 448 | 449 | def create 450 | @subdomain = Subdomain.new(params[:subdomain]) 451 | if @subdomain.save 452 | flash[:notice] = "Successfully created subdomain." 453 | end 454 | redirect_to @user 455 | end 456 | 457 | def edit 458 | @subdomain = Subdomain.find(params[:id]) 459 | respond_with(@subdomain) 460 | end 461 | 462 | def update 463 | @subdomain = Subdomain.find(params[:id]) 464 | if @subdomain.update_attributes(params[:subdomain]) 465 | flash[:notice] = "Successfully updated subdomain." 466 | end 467 | respond_with(@subdomain) 468 | end 469 | 470 | def destroy 471 | @subdomain = Subdomain.find(params[:id]) 472 | @subdomain.destroy 473 | flash[:notice] = "Successfully destroyed subdomain." 474 | redirect_to @user 475 | end 476 | 477 | protected 478 | 479 | def find_user 480 | if params[:user_id] 481 | @user = User.find(params[:user_id]) 482 | else 483 | @subdomain = Subdomain.find(params[:id]) 484 | @user = @subdomain.user 485 | end 486 | unless current_user == @user 487 | redirect_to @user, :alert => "Are you logged in properly? You are not allowed to create or change someone else's subdomain." 488 | end 489 | end 490 | 491 | end 492 | FILE 493 | end 494 | 495 | if haml_flag 496 | run 'rm app/views/subdomains/_form.html.haml' 497 | # we have to use single-quote-style-heredoc to avoid interpolation 498 | create_file 'app/views/subdomains/_form.html.haml' do <<-'FILE' 499 | - if @subdomain.errors.any? 500 | #error_explanation 501 | %h2 502 | = pluralize(@subdomain.errors.count, "error") 503 | prohibited this subdomain from being saved: 504 | %ul 505 | - @subdomain.errors.full_messages.each do |msg| 506 | %li= msg 507 | = fields_for @subdomain do |f| 508 | %div 509 | = f.label :name 510 | = f.text_field :name 511 | = f.hidden_field (:user_id, :value => @subdomain.user_id) 512 | %br/ 513 | .actions 514 | = f.submit 515 | FILE 516 | end 517 | else 518 | run 'rm app/views/subdomains/_form.html.erb' 519 | create_file 'app/views/subdomains/_form.html.erb' do <<-FILE 520 | <% if @subdomain.errors.any? %> 521 |
    522 |

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

    523 |
      524 | <% @subdomain.errors.full_messages.each do |msg| %> 525 |
    • <%= msg %>
    • 526 | <% end %> 527 |
    528 |
    529 | <% end %> 530 | <%= fields_for @subdomain do |f| %> 531 |
    532 | <%= f.label :name %> 533 | <%= f.text_field :name %> 534 | <%= f.hidden_field (:user_id, :value => @subdomain.user_id) %> 535 |
    536 |
    537 |
    538 | <%= f.submit %> 539 |
    540 | <% end %> 541 | FILE 542 | end 543 | end 544 | 545 | if haml_flag 546 | run 'rm app/views/subdomains/edit.html.haml' 547 | # we have to use single-quote-style-heredoc to avoid interpolation 548 | create_file 'app/views/subdomains/edit.html.haml' do <<-'FILE' 549 | %h1 Editing subdomain 550 | = form_for(@subdomain) do |f| 551 | = render 'form' 552 | = link_to 'Show', @subdomain 553 | | 554 | \#{link_to @subdomain.user.name, user_path(@subdomain.user)} 555 | FILE 556 | end 557 | else 558 | run 'rm app/views/subdomains/edit.html.erb' 559 | create_file 'app/views/subdomains/edit.html.erb' do <<-FILE 560 |

    Editing subdomain

    561 | <%= form_for(@subdomain) do |f| %> 562 | <%= render 'form' %> 563 | <% end %><%= link_to 'Show', @subdomain %> | 564 | <%= link_to @subdomain.user.name, user_path(@subdomain.user) %> 565 | FILE 566 | end 567 | end 568 | 569 | if haml_flag 570 | run 'rm app/views/subdomains/index.html.haml' 571 | # we have to use single-quote-style-heredoc to avoid interpolation 572 | create_file 'app/views/subdomains/index.html.haml' do <<-'FILE' 573 | %h1 Subdomains 574 | %table 575 | - @subdomains.each do |subdomain| 576 | %tr 577 | %td= link_to subdomain.name, subdomain 578 | %td 579 | (belongs to #{link_to subdomain.user.name, user_url(subdomain.user)}) 580 | %td= link_to 'Edit', edit_subdomain_path(subdomain) 581 | %td= link_to 'Destroy', subdomain, :confirm => 'Are you sure?', :method => :delete 582 | FILE 583 | end 584 | else 585 | run 'rm app/views/subdomains/index.html.erb' 586 | create_file 'app/views/subdomains/index.html.erb' do <<-FILE 587 |

    Subdomains

    588 | 589 | <% @subdomains.each do |subdomain| %> 590 | 591 | 592 | 593 | 594 | 595 | 596 | <% end %> 597 |
    <%= link_to subdomain.name, subdomain %>(belongs to <%= link_to subdomain.user.name, user_url(subdomain.user) %>)<%= link_to 'Edit', edit_subdomain_path(subdomain) %><%= link_to 'Destroy', subdomain, :confirm => 'Are you sure?', :method => :delete %>
    598 | FILE 599 | end 600 | end 601 | 602 | if haml_flag 603 | run 'rm app/views/subdomains/new.html.haml' 604 | # we have to use single-quote-style-heredoc to avoid interpolation 605 | create_file 'app/views/subdomains/new.html.haml' do <<-'FILE' 606 | %h1 New subdomain 607 | = form_for([@user, @subdomain]) do |f| 608 | = render 'form' 609 | = link_to @subdomain.user.name, user_path(@subdomain.user) 610 | FILE 611 | end 612 | else 613 | run 'rm app/views/subdomains/new.html.erb' 614 | create_file 'app/views/subdomains/new.html.erb' do <<-FILE 615 |

    New subdomain

    616 | <%= form_for([@user, @subdomain]) do |f| %> 617 | <%= render 'form' %> 618 | <% end %> 619 | <%= link_to @subdomain.user.name, user_path(@subdomain.user) %> 620 | FILE 621 | end 622 | end 623 | 624 | if haml_flag 625 | run 'rm app/views/subdomains/show.html.haml' 626 | # we have to use single-quote-style-heredoc to avoid interpolation 627 | create_file 'app/views/subdomains/show.html.haml' do <<-'FILE' 628 | %h1= @subdomain.name 629 | %p 630 | Belongs to: #{link_to @subdomain.user.name, user_url(@subdomain.user)} 631 | = link_to 'Edit', edit_subdomain_path(@subdomain) 632 | FILE 633 | end 634 | else 635 | run 'rm app/views/subdomains/show.html.erb' 636 | create_file 'app/views/subdomains/show.html.erb' do <<-FILE 637 |

    <%= @subdomain.name %>

    638 |

    Belongs to: <%= link_to @subdomain.user.name, user_url(@subdomain.user) %>

    639 | <%= link_to 'Edit', edit_subdomain_path(@subdomain) %> 640 | FILE 641 | end 642 | end 643 | 644 | puts "create a controller and views to display subdomain sites" 645 | generate(:controller, "Sites show") 646 | gsub_file 'config/routes.rb', /get \"sites\/show\"/, '' 647 | inject_into_file "app/controllers/sites_controller.rb", :after => "ApplicationController\n" do 648 | <<-RUBY 649 | skip_before_filter :limit_subdomain_access 650 | RUBY 651 | end 652 | inject_into_file "app/controllers/sites_controller.rb", :after => "def show\n" do 653 | <<-RUBY 654 | @site = Site.find_by_name!(request.subdomain) 655 | RUBY 656 | end 657 | 658 | if haml_flag 659 | run 'rm app/views/sites/show.html.haml' 660 | # we have to use single-quote-style-heredoc to avoid interpolation 661 | create_file 'app/views/sites/show.html.haml' do <<-'FILE' 662 | %h1 663 | Site: #{@site.name} 664 | %p 665 | Belongs to: #{link_to @site.user.name, user_url(@site.user, :subdomain => false)} 666 | %p= link_to 'Home', root_url(:subdomain => false) 667 | FILE 668 | end 669 | else 670 | run 'rm app/views/sites/show.html.erb' 671 | create_file 'app/views/sites/show.html.erb' do <<-FILE 672 |

    Site: <%= @site.name %>

    673 |

    Belongs to: <%= link_to @site.user.name, user_url(@site.user, :subdomain => false) %>

    674 |

    <%= link_to 'Home', root_url(:subdomain => false) %>

    675 | FILE 676 | end 677 | end 678 | 679 | puts "create a URL helper for navigation between sites" 680 | create_file 'app/helpers/url_helper.rb' do < false) 714 | end 715 | end 716 | 717 | end 718 | RUBY 719 | end 720 | 721 | puts "creating routes" 722 | inject_into_file 'config/routes.rb', :after => "devise_for :users\n" do 723 | <<-RUBY 724 | resources :users, :only => [:index, :show] do 725 | resources :subdomains, :shallow => true 726 | end 727 | match '/' => 'sites#show', :constraints => { :subdomain => /.+/ } 728 | root :to => "home#index" 729 | RUBY 730 | end 731 | 732 | puts "create and migrate the database" 733 | run 'rake db:create' 734 | run 'rake db:migrate' 735 | 736 | puts "creating default users and subdomains" 737 | append_file 'db/seeds.rb' do <<-FILE 738 | puts 'SETTING UP EXAMPLE USERS' 739 | user1 = User.create! :name => 'First User', :email => 'user@test.com', :password => 'please', :password_confirmation => 'please' 740 | puts 'New user created: ' << user1.name 741 | user2 = User.create! :name => 'Other User', :email => 'otheruser@test.com', :password => 'please', :password_confirmation => 'please' 742 | puts 'New user created: ' << user2.name 743 | puts 'SETTING UP EXAMPLE SUBDOMAINS' 744 | subdomain1 = Subdomain.create! :name => 'foo', :user_id => user1.id 745 | puts 'Created subdomain: ' << subdomain1.name 746 | subdomain2 = Subdomain.create! :name => 'bar', :user_id => user2.id 747 | puts 'Created subdomain: ' << subdomain2.name 748 | FILE 749 | end 750 | run 'rake db:seed' 751 | 752 | puts "allow cookies to be shared across subdomains" 753 | inject_into_file 'config/initializers/session_store.rb', ":domain => :all, ", :after => ":cookie_store, " 754 | 755 | puts "checking everything into git..." 756 | git :add => '.' 757 | git :commit => "-m 'modified Rails app to use subdomains with Devise'" 758 | 759 | puts "Done setting up your Rails app using subdomains with Devise." 760 | -------------------------------------------------------------------------------- /test/fixtures/subdomains.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one: 4 | name: foo 5 | user_id: 1 6 | 7 | two: 8 | name: bar 9 | user_id: 2 10 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/functional/home_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class HomeControllerTest < ActionController::TestCase 4 | test "should get index" do 5 | get :index 6 | assert_response :success 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /test/functional/sites_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SitesControllerTest < ActionController::TestCase 4 | test "should get show" do 5 | get :show 6 | assert_response :success 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /test/functional/subdomains_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SubdomainsControllerTest < ActionController::TestCase 4 | setup do 5 | @subdomain = subdomains(:one) 6 | end 7 | 8 | test "should get index" do 9 | get :index 10 | assert_response :success 11 | assert_not_nil assigns(:subdomains) 12 | end 13 | 14 | test "should get new" do 15 | get :new 16 | assert_response :success 17 | end 18 | 19 | test "should create subdomain" do 20 | assert_difference('Subdomain.count') do 21 | post :create, :subdomain => @subdomain.attributes 22 | end 23 | 24 | assert_redirected_to subdomain_path(assigns(:subdomain)) 25 | end 26 | 27 | test "should show subdomain" do 28 | get :show, :id => @subdomain.to_param 29 | assert_response :success 30 | end 31 | 32 | test "should get edit" do 33 | get :edit, :id => @subdomain.to_param 34 | assert_response :success 35 | end 36 | 37 | test "should update subdomain" do 38 | put :update, :id => @subdomain.to_param, :subdomain => @subdomain.attributes 39 | assert_redirected_to subdomain_path(assigns(:subdomain)) 40 | end 41 | 42 | test "should destroy subdomain" do 43 | assert_difference('Subdomain.count', -1) do 44 | delete :destroy, :id => @subdomain.to_param 45 | end 46 | 47 | assert_redirected_to subdomains_path 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/functional/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersControllerTest < ActionController::TestCase 4 | test "should get index" do 5 | get :index 6 | assert_response :success 7 | end 8 | 9 | test "should get show" do 10 | get :show 11 | assert_response :success 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/unit/helpers/home_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class HomeHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/unit/helpers/sites_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SitesHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/unit/helpers/subdomains_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SubdomainsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/unit/helpers/users_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/unit/subdomain_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SubdomainTest < ActiveSupport::TestCase 4 | # Replace this with your real tests. 5 | test "the truth" do 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salex/rails3-subdomain-devise/a80e2b816d15c49ebe896eb157abd9c7d03dfb72/vendor/plugins/.gitkeep --------------------------------------------------------------------------------