├── .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 | <%= link_to user.name, user %>
8 | <%if user.id == @site.user_id%>
9 | isSiteAdmin
10 | <% end %>
11 | |
12 |
13 | <% end %>
14 |
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 |
5 | <% @subdomain.errors.full_messages.each do |msg| %>
6 | - <%= msg %>
7 | <% end %>
8 |
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 | <%= link_to subdomain.name, subdomain %> |
6 |
7 | <% end %>
8 |
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 | <%= link_to user.name, user %>
6 | |
7 |
8 | <% end %>
9 |
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 | <%= link_to subdomain.name, subdomain %> |
9 | <%= link_to "Visit #{root_url(:subdomain => subdomain.name)}", root_url(:subdomain => subdomain.name) %> |
10 |
11 |
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 | <%= link_to subdomain.name, subdomain %> |
257 | <%= link_to 'Edit', edit_subdomain_path(subdomain) %> |
258 | <%= link_to 'Destroy', subdomain, :confirm => 'Are you sure?', :method => :delete %> |
259 | <%= link_to "Visit #{root_url(:subdomain => subdomain.name)}", root_url(:subdomain => subdomain.name) %> |
260 |
261 | <% end %>
262 |
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 | <%= link_to user.name, user %> |
288 |
289 | <% end %>
290 |
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 |
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 | <%= link_to subdomain.name, subdomain %> |
592 | (belongs to <%= link_to subdomain.user.name, user_url(subdomain.user) %>) |
593 | <%= link_to 'Edit', edit_subdomain_path(subdomain) %> |
594 | <%= link_to 'Destroy', subdomain, :confirm => 'Are you sure?', :method => :delete %> |
595 |
596 | <% end %>
597 |
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
--------------------------------------------------------------------------------