├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README ├── README.textile ├── Rakefile ├── 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 │ └── show.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 ├── schema.rb └── seeds.rb ├── doc └── README_FOR_APP ├── lib └── 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 ├── 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 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'rails', '3.0.4' 4 | gem 'sqlite3' 5 | gem 'devise', '1.2.rc' 6 | gem 'friendly_id', '3.2.1' 7 | # uncomment the next line if you wish to deploy to Heroku 8 | # gem 'heroku', :group => :development 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | abstract (1.0.0) 5 | actionmailer (3.0.4) 6 | actionpack (= 3.0.4) 7 | mail (~> 2.2.15) 8 | actionpack (3.0.4) 9 | activemodel (= 3.0.4) 10 | activesupport (= 3.0.4) 11 | builder (~> 2.1.2) 12 | erubis (~> 2.6.6) 13 | i18n (~> 0.4) 14 | rack (~> 1.2.1) 15 | rack-mount (~> 0.6.13) 16 | rack-test (~> 0.5.7) 17 | tzinfo (~> 0.3.23) 18 | activemodel (3.0.4) 19 | activesupport (= 3.0.4) 20 | builder (~> 2.1.2) 21 | i18n (~> 0.4) 22 | activerecord (3.0.4) 23 | activemodel (= 3.0.4) 24 | activesupport (= 3.0.4) 25 | arel (~> 2.0.2) 26 | tzinfo (~> 0.3.23) 27 | activeresource (3.0.4) 28 | activemodel (= 3.0.4) 29 | activesupport (= 3.0.4) 30 | activesupport (3.0.4) 31 | arel (2.0.8) 32 | babosa (0.2.2) 33 | bcrypt-ruby (2.1.4) 34 | builder (2.1.2) 35 | devise (1.2.rc) 36 | bcrypt-ruby (~> 2.1.2) 37 | orm_adapter (~> 0.0.2) 38 | warden (~> 1.0.2) 39 | erubis (2.6.6) 40 | abstract (>= 1.0.0) 41 | friendly_id (3.2.1) 42 | babosa (~> 0.2.1) 43 | i18n (0.5.0) 44 | mail (2.2.15) 45 | activesupport (>= 2.3.6) 46 | i18n (>= 0.4.0) 47 | mime-types (~> 1.16) 48 | treetop (~> 1.4.8) 49 | mime-types (1.16) 50 | orm_adapter (0.0.4) 51 | polyglot (0.3.1) 52 | rack (1.2.1) 53 | rack-mount (0.6.13) 54 | rack (>= 1.0.0) 55 | rack-test (0.5.7) 56 | rack (>= 1.0) 57 | rails (3.0.4) 58 | actionmailer (= 3.0.4) 59 | actionpack (= 3.0.4) 60 | activerecord (= 3.0.4) 61 | activeresource (= 3.0.4) 62 | activesupport (= 3.0.4) 63 | bundler (~> 1.0) 64 | railties (= 3.0.4) 65 | railties (3.0.4) 66 | actionpack (= 3.0.4) 67 | activesupport (= 3.0.4) 68 | rake (>= 0.8.7) 69 | thor (~> 0.14.4) 70 | rake (0.8.7) 71 | sqlite3 (1.3.3) 72 | thor (0.14.6) 73 | treetop (1.4.9) 74 | polyglot (>= 0.3.1) 75 | tzinfo (0.3.24) 76 | warden (1.0.3) 77 | rack (>= 1.0.0) 78 | 79 | PLATFORMS 80 | ruby 81 | 82 | DEPENDENCIES 83 | devise (= 1.2.rc) 84 | friendly_id (= 3.2.1) 85 | rails (= 3.0.4) 86 | sqlite3 87 | -------------------------------------------------------------------------------- /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://github.com/fortuity/rails3-subdomain-devise/wiki/Tutorial-%28Walkthrough%29 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. Rails3-Subdomain-Devise 2 | 3 | This is an example *Rails 3 application* that shows how to use *Devise* with *subdomains*. The "Devise":http://github.com/plataformatec/devise gem gives you ready-made authentication and user management. 4 | 5 | h2. The Project Has Moved! 6 | 7 | Please visit the project at its "new home":https://github.com/RailsApps/rails3-subdomains: 8 | 9 | h4. "Rails 3.1 Subdomains and Devise":https://github.com/RailsApps/rails3-subdomains 10 | 11 | h2. !http://twitter-badges.s3.amazonaws.com/t_logo-a.png(Follow on Twitter)!:http://www.twitter.com/rails_apps Follow on Twitter 12 | 13 | To keep up to date, follow the project on Twitter: 14 | "http://twitter.com/rails_apps":http://twitter.com/rails_apps 15 | 16 | Please tweet some praise if you like what you've found. 17 | 18 | h2. "Building It" Tutorial 19 | 20 | A complete walkthrough tutorial is available on the GitHub wiki: 21 | 22 | h4. "View the Tutorial":http://github.com/fortuity/rails3-subdomain-devise/wiki/Tutorial-%28Walkthrough%29 23 | 24 | The tutorial documents each step that you must follow to create this application. Every step is documented concisely, so a complete beginner can create this application without any additional knowledge. However, no explanation is offered for any of the steps, so if you are a beginner, you're advised to look for an introduction to Rails elsewhere. 25 | 26 | If you simply wish to modify the application for your own project, you can download the application and set it up as described below, without following the tutorial. 27 | 28 | h2. Use Cases 29 | 30 | h4. What Is Implemented 31 | 32 | This example implements "blog-style subdomains in Rails." The example is similar to the application shown in Ryan Bates's screencast "Subdomains in Rails 3":http://railscasts.com/episodes/221-subdomains-in-rails-3 but adds authentication using "Devise":http://github.com/plataformatec/devise. In this example, there is a "main" domain where anyone can visit and create a user account. And registered users can create any number of subdomains which could host blogs or other types of sites. 33 | 34 | h4. What Is Not Implemented 35 | 36 | Another use of subdomains is often called "Basecamp-style subdomains in Rails." Visitors to the main site can create a user account which is then hosted at a subdomain that matches their user name. Each user has only one subdomain and when they log in, all their activity is confined to their subdomain. A user's home page and account info is accessed only through the subdomain that matches their user name. 37 | 38 | For an implementation of "Basecamp-style subdomains in Rails," see: 39 | 40 | "Steve Alex's Basecamp-style fork":http://github.com/salex/rails3-subdomain-devise. 41 | 42 | Sachin Sagar Rai (millisami) has revised Steve Alex's Basecamp-style fork to use Mongodb with Mongoid: 43 | 44 | "Millisami's Basecamp-style fork with Mongoid":http://github.com/millisami/rails3-subdomain-devise. 45 | 46 | (Got your own? Contact me and I will add it here.) 47 | 48 | No testing (RSpec or otherwise) is implemented. This app only serves to demonstrate Devise working with subdomains on Rails 3. 49 | 50 | h4. Similar Examples and Tutorials 51 | 52 | |_. Author |_. Example App |_. Comments | 53 | | Plataformatec | "Devise":http://github.com/plataformatec/devise_example | Simple authentication example using SQLite, no subdomains | 54 | | Daniel Kehoe | "Devise, RSpec, Cucumber":https://github.com/fortuity/rails3-devise-rspec-cucumber | Detailed tutorial, app template, starter app, using SQLite, no subdomains | 55 | | Daniel Kehoe | "OmniAuth, Mongoid":https://github.com/fortuity/rails3-mongoid-omniauth | Detailed tutorial, app template, starter app, using MongoDB, no subdomains | 56 | 57 | h2. Assumptions 58 | 59 | Before running this app, you need to install 60 | 61 | * The Ruby language (version 1.8.7 or 1.9.2) 62 | * Rails (version 3.0.4) 63 | * A working installation of SQLite (preferred), MySQL, or PostgreSQL 64 | 65 | I recommend installing rvm, the "Ruby Version Manager":http://rvm.beginrescueend.com/, to manage multiple versions of Rails. 66 | 67 | Check that appropriate versions of Ruby and Rails are installed in your development environment: 68 | @$ ruby -v@ 69 | @$ rails -v@ 70 | 71 | h2. Generating the Application 72 | 73 | To get started with a new Rails application based on this example, you can generate a new Rails app: 74 | 75 | @$ rails new app_name -m https://github.com/fortuity/rails3-application-templates/raw/master/rails3-subdomain-devise-template.rb@ 76 | 77 | bq. You MUST be using Rails 3.0.4. Generating a Rails application from an “HTTPS” URL does not work in Rails 3.0.3 and earlier versions. 78 | 79 | This creates a new Rails app (with the @app_name@ you provide) on your computer. 80 | 81 | The application template offers you the following options: 82 | 83 | * set up your view files using the Haml templating language 84 | * use jQuery instead of Prototype 85 | * install the heroku gem for deployment to Heroku 86 | * use Mongoid instead of Active Record for database access 87 | 88 | If you wish to "change the recipe" to generate the app with your own customized options, you can copy and edit the file *rails3-subdomain-devise-template.rb* found at the project "fortuity/rails3-application-templates":https://github.com/fortuity/rails3-application-templates. 89 | 90 | h2. Downloading the Example 91 | 92 | I recommend "Generating the Application" as described above. If that doesn't work, or you simply wish to examine the example code, you can download the app ("clone the repository") with the command 93 | 94 | @$ git clone git(at)github.com:fortuity/rails3-subdomain-devise.git@ 95 | 96 | The source code is managed with Git (a version control system). You'll need Git on your machine (install it from "http://git-scm.com/":http://git-scm.com/). 97 | 98 | h2. Set Up Gems 99 | 100 | h4. About Required Gems 101 | 102 | The application uses the following gems. 103 | 104 | The SQLite3 gem is used for the database. You can substitute a different database if you wish. 105 | 106 | The FriendlyId gem is used to give users and subdomains easily recognizable strings instead of numeric ids in URLs. 107 | 108 | I recommend checking for newer versions of these gems before proceeding: 109 | 110 | * rails (version 3.0.4) "(check rubygems.org for a newer gem)":http://rubygems.org/gems/rails 111 | * devise (version 1.2.rc) "(Check rubygems.org for a newer gem)":http://rubygems.org/gems/devise 112 | * friendly_id (version 3.2.1) "(Check rubygems.org for a newer gem)":http://rubygems.org/gems/friendly_id 113 | 114 | The app has been tested with the indicated versions. If you are able to build the app with a newer gem, please create an "issue":http://github.com/fortuity/rails3-subdomain-devise/issues on GitHub and I will update the app. 115 | 116 | h4. Install the Required Gems 117 | 118 | Install the required gems on your computer: 119 | 120 | @$ bundle install@ 121 | 122 | If you need to troubleshoot, you can check which gems are installed on your computer with: 123 | 124 | @$ gem list --local@ 125 | 126 | Keep in mind that you have installed these gems locally. When you deploy the app to another server, the same gems (and versions) must be available. 127 | 128 | h2. Getting Started 129 | 130 | h4. Configure Email 131 | 132 | Configure email by modifying 133 | 134 | *config/initializers/devise.rb* 135 | 136 | and setting the return email address for emails sent from the application. 137 | 138 | You may need to set values for your mailhost in 139 | 140 | *config/environments/development.rb* 141 | *config/environments/production.rb* 142 | 143 | h4. Set Up Configuration for Devise 144 | 145 | This app uses Devise for user management and authentication. Devise is at "http://github.com/plataformatec/devise":http://github.com/plataformatec/devise. 146 | 147 | You can modify the configuration file for Devise if you want to use something other than the defaults: 148 | 149 | *config/initializers/devise.rb* 150 | 151 | h2. Set Up the Database 152 | 153 | h4. Create a Database and Run Migrations 154 | 155 | Create an empty database. You can do this by running a rake command: 156 | 157 | @$ rake db:create@ 158 | 159 | Run the migrations: 160 | 161 | @$ rake db:migrate@ 162 | 163 | You can take a look at the database schema that's been created for you: 164 | 165 | *db/schema.rb* 166 | 167 | h4. Seed the Database With Users and Subdomains 168 | 169 | You'll want to set up a default user so you can easily log in to test the app. You can modify the file *db/seeds.rb* for your own name, email and password: 170 | 171 |
172 | puts 'SETTING UP EXAMPLE USERS'
173 | user1 = User.create! :name => 'First User', :email => 'user@test.com', :password => 'please', :password_confirmation => 'please'
174 | puts 'New user created: ' << user1.name
175 | user2 = User.create! :name => 'Other User', :email => 'otheruser@test.com', :password => 'please', :password_confirmation => 'please'
176 | puts 'New user created: ' << user2.name
177 | puts 'SETTING UP EXAMPLE SUBDOMAINS'
178 | subdomain1 = Subdomain.create! :name => 'foo'
179 | puts 'Created subdomain: ' << subdomain1.name
180 | subdomain2 = Subdomain.create! :name => 'bar'
181 | puts 'Created subdomain: ' << subdomain2.name
182 | user1.subdomains << subdomain1
183 | user1.save
184 | user2.subdomains << subdomain2
185 | user2.save
186 | 
187 | 188 | Run the rake task to seed the database: 189 | 190 | @$ rake db:seed@ 191 | 192 | h2. Test the App 193 | 194 | You can check that your app runs properly by entering the command 195 | 196 | @$ rails server@ (or, abbreviated: @$ rails s@) 197 | 198 | If you launch the application, it will be running at "http://localhost:3000/":http://localhost:3000/ or "http://0.0.0.0:3000/":http://0.0.0.0:3000/. However, unless you've made some configuration changes to your computer, you won't be able to resolve an address that uses a subdomain, such as "http://foo.localhost:3000/":http://foo.localhost:3000/. There are several complex solutions to this problem. You could set up your own domain name server on your localhost and create an A entry to catch all subdomains. You could modify your */etc/hosts* file (but it won't accommodate dynamically created subdomains). You can create a "proxy auto-config (PAC)":http://en.wikipedia.org/wiki/Proxy_auto-config file and set it up as the proxy in your web browser preferences. There's a far simpler solution that does not require reconfiguring your computer or web browser preferences. The developer Levi Cook registered a domain, "lvh.me":http://lvh.me:3000/ (short for: local virtual host me), that resolves to the localhost IP address 127.0.0.1 and supports wildcards (accommodating dynamically created subdomains). See "Tim Pope's blog post":http://tbaggery.com/2010/03/04/smack-a-ho-st.html for a NSFW alternative. 199 | 200 | To test the application, visit "http://lvh.me:3000/":http://lvh.me:3000/. You can also try "http://foo.lvh.me:3000/":http://foo.lvh.me:3000/ or "http://bar.lvh.me:3000/":http://bar.lvh.me:3000/. 201 | 202 | To sign in as the default user, (unless you've changed it) use 203 | 204 | * email: user@test.com 205 | * password: please 206 | 207 | You should delete or change the pre-configured logins before you deploy your application. 208 | 209 | h2. Deploying to Heroku 210 | 211 | h4. Set Up Heroku 212 | 213 | For your convenience, here are instructions for deploying your app to Heroku. Heroku provides low cost, easily configured Rails application hosting. 214 | 215 | To deploy this app to Heroku, you must have a Heroku account. If you need to obtain one, visit "http://heroku.com/":http://heroku.com/ to set up an account. 216 | 217 | Make sure the Heroku gem is in your *Gemfile*. If it's not, add it and run 218 | 219 | @$ bundle install@ 220 | 221 | to set up your gems again. 222 | 223 | If you've just created a Heroku account, add your public key immediately after installing the heroku gem so that you can use git to push or clone Heroku app repositories. See "http://docs.heroku.com/heroku-command":http://docs.heroku.com/heroku-command for details. 224 | 225 | h4. Create Your Application on Heroku 226 | 227 | Use the Heroku create command to create and name your new app. 228 | 229 | @$ heroku create _myapp_@ 230 | 231 | h4. Heroku Add-ons and DNS Configuration 232 | 233 | You will need the following Heroku add-ons to deploy your app using subdomains with your own custom domain: 234 | 235 | * Custom Domains (free) 236 | * Custom Domains + Wildcard ($5 per month) 237 | * Zerigo DNS Tier 1 ($7 per month) 238 | 239 | To enable the add-ons, you can use the Heroku web interface or you can enter the following commands: 240 | 241 | @$ heroku addons:add custom_domains@ 242 | 243 | @$ heroku domains:add mydomain.com@ 244 | 245 | @$ heroku addons:add wildcard_domains@ 246 | 247 | @$ heroku domains:add *.mydomain.com@ 248 | 249 | @$ heroku addons:add zerigo_dns:tier1@ 250 | 251 | If you are using the Zerigo DNS service, you will need to set the nameserver with your domain registrar. It can take a few minutes (or longer) for DNS changes to propagate. When DNS is set properly, you should be able to visit *mydomain.com* or *test.mydomain.com* in your web browser and see the Heroku default page: 252 | 253 | @Heroku | Welcome to your new app!@ 254 | 255 | You can check that everything has been added correctly by running: 256 | 257 | @$ heroku info --app myapp@ 258 | 259 | h4. Set Up Your Application on Heroku 260 | 261 | Push your application to Heroku: 262 | 263 | @$ git push heroku master@ 264 | 265 | Set up your Heroku database: 266 | 267 | @$ heroku rake db:migrate@ 268 | 269 | Initialize your application database: 270 | 271 | @$ heroku rake db:seed@ 272 | 273 | h4. Visit Your Site 274 | 275 | If you use the heroku command to open your default web browser to your site with 276 | 277 | @$ heroku open@ 278 | 279 | or if you visit your site with "http://myapp.heroku.com/":http://myapp.heroku.com/ you'll see the error, "The page you were looking for doesn't exist." That's because your app is trying to find a subdomain "myapp" that doesn't exist. You'll need to visit the site using your own domain name, such as "http://mydomain.com/":http://mydomain.com/. Domain name service must be set properly to use the Zerigo nameservers. 280 | 281 | h4. Troubleshooting 282 | 283 | If you get errors, you can troubleshoot by reviewing the log files: 284 | 285 | @$ heroku logs@ 286 | 287 | h2. Customizing 288 | 289 | You can use the Site model, controller, and views as a beginning point for customizing the app. For example, you could build a blog application that is displayed on the Site pages. 290 | 291 | "Devise":http://github.com/plataformatec/devise provides a variety of features for implementing authentication. See the Devise documentation for options. 292 | 293 | h2. Testing 294 | 295 | The application does not include tests (RSpec or otherwise). It relies on "Devise":http://github.com/plataformatec/devise which include extensive tests. This application is intended to be a basis for your own customized application and (in most cases) you will be writing your own tests for your required behavior. 296 | 297 | h2. Documentation and Support 298 | 299 | See the "Tutorial":http://github.com/fortuity/rails3-subdomain-devise/wiki/Tutorial-%28Walkthrough%29 for this app for details of how it was built. 300 | 301 | For an introduction to Rails 3 and subdomains, see Ryan Bates's screencast "Subdomains in Rails 3":http://railscasts.com/episodes/221-subdomains-in-rails-3 (a transcription is available from "ASCIIcasts":http://asciicasts.com/episodes/221-subdomains-in-rails-3). 302 | 303 | For a Devise introduction, Ryan Bates offers a "Railscast on Devise":http://railscasts.com/episodes/209-introducing-devise. You can find documentation for "Devise":http://github.com/plataformatec/devise at "http://github.com/plataformatec/devise":http://github.com/plataformatec/devise. There is an active "Devise mailing list":http://groups.google.com/group/plataformatec-devise and you can submit "Devise issues":http://github.com/plataformatec/devise/issues at GitHub. 304 | 305 | h4. Issues 306 | 307 | Please create an "issue on GitHub":http://github.com/fortuity/rails3-subdomain-devise/issues if you identify any problems or have suggestions for improvements. 308 | 309 | h2. Contributing 310 | 311 | If you make improvements to this application, please share with others. 312 | 313 | Send the author a message, create an "issue":http://github.com/fortuity/rails3-subdomain-devise/issues, or fork the project and submit a pull request. 314 | 315 | If you add functionality to this application, create an alternative implementation, or build an application that is similar, please contact me and I'll add a note to the README so that others can find your work. 316 | 317 | h2. Credits 318 | 319 | Daniel Kehoe ("http://danielkehoe.com/":http://danielkehoe.com/) implemented the application and wrote the tutorial. 320 | 321 | h4. Contributors 322 | 323 | Thank you to contributor Fred Schoeneman for improving the tutorial. 324 | Thank you to contributor Charlie Ussery for suggesting how to ignore the "www" subdomain. 325 | Thank you to contributor Tom Howlett for suggesting how to use subdomains in Devise emails. 326 | Thank you to contributor Tom Mornini for improvements to forms. 327 | 328 | h2. License 329 | 330 | h4. Public Domain Dedication 331 | 332 | 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. 333 | -------------------------------------------------------------------------------- /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 | before_filter :limit_subdomain_access 5 | before_filter :set_mailer_url_options 6 | 7 | protected 8 | 9 | def limit_subdomain_access 10 | if request.subdomain.present? 11 | # this error handling could be more sophisticated! 12 | # please make a suggestion :-) 13 | redirect_to root_url(:subdomain => false) 14 | end 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /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 | skip_before_filter :limit_subdomain_access 3 | 4 | def show 5 | @site = Site.find_by_name!(request.subdomain) 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /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 | respond_with(@subdomain) 14 | end 15 | 16 | def new 17 | @subdomain = Subdomain.new(:user => @user) 18 | respond_with(@subdomain) 19 | end 20 | 21 | def create 22 | @subdomain = Subdomain.new(params[:subdomain]) 23 | if @subdomain.save 24 | flash[:notice] = "Successfully created subdomain." 25 | end 26 | redirect_to @user 27 | end 28 | 29 | def edit 30 | @subdomain = Subdomain.find(params[:id]) 31 | respond_with(@subdomain) 32 | end 33 | 34 | def update 35 | @subdomain = Subdomain.find(params[:id]) 36 | if @subdomain.update_attributes(params[:subdomain]) 37 | flash[:notice] = "Successfully updated subdomain." 38 | end 39 | respond_with(@subdomain) 40 | end 41 | 42 | def destroy 43 | @subdomain = Subdomain.find(params[:id]) 44 | @subdomain.destroy 45 | flash[:notice] = "Successfully destroyed subdomain." 46 | redirect_to @user 47 | end 48 | 49 | protected 50 | 51 | def find_user 52 | if params[:user_id] 53 | @user = User.find(params[:user_id]) 54 | else 55 | @subdomain = Subdomain.find(params[:id]) 56 | @user = @subdomain.user 57 | end 58 | unless current_user == @user 59 | redirect_to @user, :alert => "Are you logged in properly? You are not allowed to create or change someone else's subdomain." 60 | end 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | def index 3 | @users = User.all 4 | end 5 | 6 | def show 7 | @user = User.find(params[:id]) 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /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 | subdomain = (subdomain || "") 4 | subdomain += "." unless subdomain.empty? 5 | [subdomain, request.domain, request.port_string].join 6 | end 7 | 8 | def url_for(options = nil) 9 | if options.kind_of?(Hash) && options.has_key?(:subdomain) 10 | options[:host] = with_subdomain(options.delete(:subdomain)) 11 | end 12 | super 13 | end 14 | 15 | def set_mailer_url_options 16 | ActionMailer::Base.default_url_options[:host] = with_subdomain(request.subdomain) 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /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 | belongs_to :user 3 | has_friendly_id :name, :use_slug => true, :strip_non_ascii => true 4 | validates_uniqueness_of :name, :case_sensitive => false 5 | validates_presence_of :name 6 | end 7 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_many :subdomains, :dependent => :destroy 3 | devise :database_authenticatable, :registerable, 4 | :recoverable, :rememberable, :trackable, :validatable 5 | validates_presence_of :name 6 | validates_uniqueness_of :name, :email, :case_sensitive => false 7 | attr_accessible :name, :email, :password, :password_confirmation, :remember_me 8 | has_friendly_id :name, :use_slug => true, :strip_non_ascii => true 9 | end 10 | -------------------------------------------------------------------------------- /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 | <% else %> 6 |
  • 7 | <%= link_to('Login', new_user_session_path) %> 8 |
  • 9 | <% end %> 10 |
  • 11 | User: 12 | <% if current_user %> 13 | <%= current_user.name %> 14 | <% else %> 15 | (not logged in) 16 | <% end %> 17 |
  • 18 | -------------------------------------------------------------------------------- /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 | <% end %> 10 | -------------------------------------------------------------------------------- /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.label :name %>
    7 | <%= f.text_field :name %>

    8 | 9 |

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

    11 | 12 |

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

    14 | 15 |

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

    17 | 18 |

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

    20 | 21 |

    <%= f.submit "Update" %>

    22 | <% end %> 23 | 24 |

    Cancel my account

    25 | 26 |

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

    27 | 28 | <%= link_to "Back", :back %> 29 | -------------------------------------------------------------------------------- /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 |

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

    8 | 9 |

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

    11 | 12 |

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

    14 | 15 |

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

    17 | 18 |

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

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

    3 | -------------------------------------------------------------------------------- /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/show.html.erb: -------------------------------------------------------------------------------- 1 |

    Site: <%= @site.name %>

    2 |

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

    3 |

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

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

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

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

    Editing subdomain

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

    Subdomains

    2 | 3 | <% @subdomains.each do |subdomain| %> 4 | 5 | 6 | 7 | 8 | 9 | 10 | <% end %> 11 |
    <%= link_to subdomain.name, subdomain %>(belongs to <%= link_to subdomain.user.name, user_url(subdomain.user) %>)<%= link_to 'Edit', edit_subdomain_path(subdomain) %><%= link_to 'Destroy', subdomain, :confirm => 'Are you sure?', :method => :delete %>
    12 | -------------------------------------------------------------------------------- /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 @subdomain.user.name, user_url(@subdomain.user) %>

    3 | <%= link_to 'Edit', edit_subdomain_path(@subdomain) %> 4 | -------------------------------------------------------------------------------- /app/views/users/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Users

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

    <%= @user.name %>

    2 |

    Email: <%= @user.email %>

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

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

    6 | 7 | <% @user.subdomains.each do |subdomain| %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% end %> 15 |
    <%= link_to subdomain.name, subdomain %><%= link_to 'Edit', edit_subdomain_path(subdomain) %><%= link_to 'Destroy', subdomain, :confirm => 'Are you sure?', :method => :delete %><%= link_to "Visit #{root_url(:subdomain => subdomain.name)}", root_url(:subdomain => subdomain.name) %>
    16 |
    17 | <%= link_to "Add New Subdomain", new_user_subdomain_path(@user) %> 18 | -------------------------------------------------------------------------------- /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}/extras) 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, :domain => :all, :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 | resources :subdomains, :shallow => true 5 | end 6 | match '/' => 'home#index', :constraints => { :subdomain => 'www' } 7 | match '/' => 'sites#show', :constraints => { :subdomain => /.+/ } 8 | root :to => "home#index" 9 | 10 | # The priority is based upon order of creation: 11 | # first created -> highest priority. 12 | 13 | # Sample of regular route: 14 | # match 'products/:id' => 'catalog#view' 15 | # Keep in mind you can assign values other than :controller and :action 16 | 17 | # Sample of named route: 18 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 19 | # This route can be invoked with purchase_url(:id => product.id) 20 | 21 | # Sample resource route (maps HTTP verbs to controller actions automatically): 22 | # resources :products 23 | 24 | # Sample resource route with options: 25 | # resources :products do 26 | # member do 27 | # get :short 28 | # post :toggle 29 | # end 30 | # 31 | # collection do 32 | # get :sold 33 | # end 34 | # end 35 | 36 | # Sample resource route with sub-resources: 37 | # resources :products do 38 | # resources :comments, :sales 39 | # resource :seller 40 | # end 41 | 42 | # Sample resource route with more complex sub-resources 43 | # resources :products do 44 | # resources :comments 45 | # resources :sales do 46 | # get :recent, :on => :collection 47 | # end 48 | # end 49 | 50 | # Sample resource route within a namespace: 51 | # namespace :admin do 52 | # # Directs /admin/products/* to Admin::ProductsController 53 | # # (app/controllers/admin/products_controller.rb) 54 | # resources :products 55 | # end 56 | 57 | # You can have the root of your site routed with "root" 58 | # just remember to delete public/index.html. 59 | # root :to => "welcome#index" 60 | 61 | # See how all your routes lay out with "rake routes" 62 | 63 | # This is a legacy wild controller route that's not recommended for RESTful applications. 64 | # Note: This route will make all actions in every controller accessible via GET requests. 65 | # match ':controller(/:action(/:id(.:format)))' 66 | end 67 | -------------------------------------------------------------------------------- /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.confirmable 10 | # t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both 11 | # t.token_authenticatable 12 | 13 | 14 | t.timestamps 15 | end 16 | 17 | add_index :users, :email, :unique => true 18 | add_index :users, :reset_password_token, :unique => true 19 | # add_index :users, :confirmation_token, :unique => true 20 | # add_index :users, :unlock_token, :unique => true 21 | end 22 | 23 | def self.down 24 | drop_table :users 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /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/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 => 20100808194652) do 14 | 15 | create_table "slugs", :force => true do |t| 16 | t.string "name" 17 | t.integer "sluggable_id" 18 | t.integer "sequence", :default => 1, :null => false 19 | t.string "sluggable_type", :limit => 40 20 | t.string "scope" 21 | t.datetime "created_at" 22 | end 23 | 24 | add_index "slugs", ["name", "sluggable_type", "sequence", "scope"], :name => "index_slugs_on_n_s_s_and_s", :unique => true 25 | add_index "slugs", ["sluggable_id"], :name => "index_slugs_on_sluggable_id" 26 | 27 | create_table "subdomains", :force => true do |t| 28 | t.string "name" 29 | t.integer "user_id" 30 | t.datetime "created_at" 31 | t.datetime "updated_at" 32 | end 33 | 34 | create_table "users", :force => true do |t| 35 | t.string "email", :default => "", :null => false 36 | t.string "encrypted_password", :limit => 128, :default => "", :null => false 37 | t.string "password_salt", :default => "", :null => false 38 | t.string "reset_password_token" 39 | t.string "remember_token" 40 | t.datetime "remember_created_at" 41 | t.integer "sign_in_count", :default => 0 42 | t.datetime "current_sign_in_at" 43 | t.datetime "last_sign_in_at" 44 | t.string "current_sign_in_ip" 45 | t.string "last_sign_in_ip" 46 | t.string "name" 47 | t.datetime "created_at" 48 | t.datetime "updated_at" 49 | end 50 | 51 | add_index "users", ["email"], :name => "index_users_on_email", :unique => true 52 | add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true 53 | 54 | end 55 | -------------------------------------------------------------------------------- /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 => 'First User', :email => 'user@test.com', :password => 'please', :password_confirmation => 'please' 10 | puts 'New user created: ' << user1.name 11 | user2 = User.create! :name => 'Other User', :email => 'otheruser@test.com', :password => 'please', :password_confirmation => 'please' 12 | puts 'New user created: ' << user2.name 13 | puts 'SETTING UP EXAMPLE SUBDOMAINS' 14 | subdomain1 = Subdomain.create! :name => 'foo' 15 | puts 'Created subdomain: ' << subdomain1.name 16 | subdomain2 = Subdomain.create! :name => 'bar' 17 | puts 'Created subdomain: ' << subdomain2.name 18 | user1.subdomains << subdomain1 19 | user1.save 20 | user2.subdomains << subdomain2 21 | user2.save 22 | -------------------------------------------------------------------------------- /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/tasks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fortuity/rails3-subdomain-devise/969679585aaf873d9c8323bde99641ba31febdd2/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/fortuity/rails3-subdomain-devise/969679585aaf873d9c8323bde99641ba31febdd2/public/favicon.ico -------------------------------------------------------------------------------- /public/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fortuity/rails3-subdomain-devise/969679585aaf873d9c8323bde99641ba31febdd2/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/fortuity/rails3-subdomain-devise/969679585aaf873d9c8323bde99641ba31febdd2/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 | -------------------------------------------------------------------------------- /test/fixtures/subdomains.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one: 4 | name: MyString 5 | user: 6 | 7 | two: 8 | name: MyString 9 | user: 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/fortuity/rails3-subdomain-devise/969679585aaf873d9c8323bde99641ba31febdd2/vendor/plugins/.gitkeep --------------------------------------------------------------------------------