├── lib └── tasks │ └── .gitkeep ├── public ├── favicon.ico ├── stylesheets │ ├── .gitkeep │ ├── michael_hintbuble.css │ └── screen.css ├── images │ ├── help_bubble_pointer.png │ └── error_bubble_pointer.png ├── javascripts │ ├── application.js │ ├── michael_hintbuble.js │ ├── dragdrop.js │ └── controls.js ├── robots.txt ├── 422.html ├── 404.html └── 500.html ├── vendor └── gems │ └── michael_hintbuble-1.0.5 │ ├── VERSION │ ├── .gitignore │ ├── rails │ └── init.rb │ ├── init.rb │ ├── generators │ └── michael_hintbuble │ │ ├── templates │ │ ├── error_bubble_pointer.png │ │ ├── help_bubble_pointer.png │ │ ├── michael_hintbuble.css │ │ └── michael_hintbuble.js │ │ └── michael_hintbuble_generator.rb │ ├── lib │ ├── generators │ │ └── michael_hintbuble │ │ │ ├── templates │ │ │ ├── help_bubble_pointer.png │ │ │ ├── error_bubble_pointer.png │ │ │ ├── michael_hintbuble.css │ │ │ └── michael_hintbuble.js │ │ │ └── michael_hintbuble_generator.rb │ ├── michael_hintbuble.rb │ └── michael_hintbuble │ │ └── helpers.rb │ ├── test │ ├── test_helper.rb │ └── michael_hintbuble │ │ └── helpers_test.rb │ ├── MIT-LICENSE │ ├── Rakefile │ ├── michael_hintbuble.gemspec │ ├── .specification │ └── README.rdoc ├── .gitignore ├── app ├── helpers │ └── application_helper.rb ├── views │ ├── examples │ │ ├── _partial_example.html.erb │ │ └── index.html.erb │ └── layouts │ │ └── application.html.erb └── controllers │ ├── application_controller.rb │ └── examples_controller.rb ├── config ├── routes.rb ├── environment.rb ├── initializers │ ├── mime_types.rb │ ├── inflections.rb │ ├── backtrace_silencers.rb │ ├── session_store.rb │ └── secret_token.rb ├── locales │ └── en.yml ├── boot.rb ├── database.yml ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── application.rb ├── config.ru ├── doc └── README_FOR_APP ├── test ├── performance │ └── browsing_test.rb └── test_helper.rb ├── Rakefile ├── script └── rails ├── db └── seeds.rb ├── Gemfile ├── Gemfile.lock └── README /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/stylesheets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/VERSION: -------------------------------------------------------------------------------- 1 | 1.0.5 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | db/*.sqlite3 3 | log/*.log 4 | tmp/**/* 5 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.gem -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/examples/_partial_example.html.erb: -------------------------------------------------------------------------------- 1 |
Hello, <%= name %>
-------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/rails/init.rb: -------------------------------------------------------------------------------- 1 | require "michael_hintbuble" -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/init.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/rails/init.rb" -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | MichaelHintbubleExample::Application.routes.draw do 2 | root :to => "examples#index" 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/examples_controller.rb: -------------------------------------------------------------------------------- 1 | class ExamplesController < ApplicationController 2 | 3 | def index 4 | end 5 | 6 | end -------------------------------------------------------------------------------- /public/images/help_bubble_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/michael_hintbuble_example/master/public/images/help_bubble_pointer.png -------------------------------------------------------------------------------- /public/images/error_bubble_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/michael_hintbuble_example/master/public/images/error_bubble_pointer.png -------------------------------------------------------------------------------- /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 MichaelHintbubleExample::Application 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | MichaelHintbubleExample::Application.initialize! 6 | -------------------------------------------------------------------------------- /public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | log_to_console = function(content) { 2 | var log = new Element("DIV"); 3 | log.update(content); 4 | 5 | var el = $("console").down(".log_container"); 6 | el.insert(log); 7 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/generators/michael_hintbuble/templates/error_bubble_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/michael_hintbuble_example/master/vendor/gems/michael_hintbuble-1.0.5/generators/michael_hintbuble/templates/error_bubble_pointer.png -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/generators/michael_hintbuble/templates/help_bubble_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/michael_hintbuble_example/master/vendor/gems/michael_hintbuble-1.0.5/generators/michael_hintbuble/templates/help_bubble_pointer.png -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/lib/generators/michael_hintbuble/templates/help_bubble_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/michael_hintbuble_example/master/vendor/gems/michael_hintbuble-1.0.5/lib/generators/michael_hintbuble/templates/help_bubble_pointer.png -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/lib/generators/michael_hintbuble/templates/error_bubble_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/michael_hintbuble_example/master/vendor/gems/michael_hintbuble-1.0.5/lib/generators/michael_hintbuble/templates/error_bubble_pointer.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | MichaelHintbubleExample::Application.load_tasks 8 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MichaelHintbubleExample 5 | <%= stylesheet_link_tag :all %> 6 | <%= javascript_include_tag :all %> 7 | <%= csrf_meta_tag %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/lib/michael_hintbuble.rb: -------------------------------------------------------------------------------- 1 | # external gems 2 | require "action_pack" 3 | 4 | 5 | # helpers 6 | require File.dirname(__FILE__) + "/michael_hintbuble/helpers" 7 | 8 | 9 | # add action view extensions 10 | ActionView::Base.module_eval { include Coroutine::MichaelHintbuble::Helpers } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | #---------------------------------------------- 2 | # gem sources 3 | #---------------------------------------------- 4 | 5 | source "http://rubygems.org" 6 | 7 | 8 | #---------------------------------------------- 9 | # common gems 10 | #---------------------------------------------- 11 | 12 | # rails, etc. 13 | gem "rack" 14 | gem "rails", "3.0.3" 15 | 16 | # other gems 17 | gem "michael_hintbuble", :path => File.join(File.dirname(__FILE__), "vendor/gems") 18 | gem "sqlite3" -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | MichaelHintbubleExample::Application.config.session_store :cookie_store, :key => '_michael_hintbuble_example_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 | # MichaelHintbubleExample::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------- 2 | # Requirements 3 | #---------------------------------------------------------- 4 | 5 | # rails stuff 6 | require "rubygems" 7 | require "active_support" 8 | require "active_support/test_case" 9 | require "action_controller" 10 | require "action_controller/test_case" 11 | require "action_view" 12 | require "action_view/test_case" 13 | require "test/unit" 14 | 15 | # the plugin itself 16 | require "#{File.dirname(__FILE__)}/../init" -------------------------------------------------------------------------------- /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 | MichaelHintbubleExample::Application.config.secret_token = 'c094c1d51c1db6a285b785a7bfa210f662d9ff147b6dc3d35842e08cf3ee5cc0d2f4858f017e47664584f62b68d56873c79f271530b4b6c30518fe56418d9dde' 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/generators/michael_hintbuble/michael_hintbuble_generator.rb: -------------------------------------------------------------------------------- 1 | class MichaelHintbubleGenerator < Rails::Generator::Base 2 | 3 | # This method copies images, javascript, and stylesheet files to the 4 | # corresponding public directories. 5 | # 6 | def manifest 7 | record do |m| 8 | m.file "help_bubble_pointer.png", "public/images/help_bubble_pointer.png" 9 | m.file "error_bubble_pointer.png", "public/images/error_bubble_pointer.png" 10 | m.file "michael_hintbuble.css", "public/stylesheets/michael_hintbuble.css" 11 | m.file "michael_hintbuble.js", "public/javascripts/michael_hintbuble.js" 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/lib/generators/michael_hintbuble/michael_hintbuble_generator.rb: -------------------------------------------------------------------------------- 1 | require "rails/generators" 2 | 3 | 4 | class MichaelHintbubleGenerator < Rails::Generators::Base 5 | 6 | # This call establishes the path to the templates directory. 7 | # 8 | def self.source_root 9 | File.join(File.dirname(__FILE__), "templates") 10 | end 11 | 12 | 13 | # This method copies images, javascript, and stylesheet files to the 14 | # corresponding public directories. 15 | # 16 | def generate_assets 17 | copy_file "help_bubble_pointer.png", "public/images/help_bubble_pointer.png" 18 | copy_file "error_bubble_pointer.png", "public/images/error_bubble_pointer.png" 19 | copy_file "michael_hintbuble.css", "public/stylesheets/michael_hintbuble.css" 20 | copy_file "michael_hintbuble.js", "public/javascripts/michael_hintbuble.js" 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | MichaelHintbubleExample::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 | # Only use best-standards-support built into browsers 24 | config.action_dispatch.best_standards_support = :builtin 25 | end 26 | 27 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Coroutine LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | MichaelHintbubleExample::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 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/Rakefile: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "rake/testtask" 3 | require "rake/rdoctask" 4 | require "jeweler" 5 | 6 | 7 | desc "Default: run tests." 8 | task :default => [:test] 9 | 10 | 11 | desc "Test the plugin." 12 | Rake::TestTask.new(:test) do |t| 13 | t.libs << "lib" 14 | t.pattern = "test/**/*_test.rb" 15 | t.verbose = true 16 | end 17 | 18 | 19 | desc "Generate documentation for the plugin." 20 | Rake::RDocTask.new(:rdoc) do |rdoc| 21 | rdoc.rdoc_dir = "rdoc" 22 | rdoc.title = "michael_hintbuble" 23 | rdoc.options << "--line-numbers --inline-source" 24 | rdoc.rdoc_files.include("README") 25 | rdoc.rdoc_files.include("lib/**/*.rb") 26 | end 27 | 28 | 29 | begin 30 | Jeweler::Tasks.new do |gemspec| 31 | gemspec.authors = ["Coroutine", "Tim Lowrimore", "John Dugan"] 32 | gemspec.description = "Michael HintBuble allows you to generate hint bubbles and tooltips in Rails applications using the same syntax used for rendering templates." 33 | gemspec.email = "gems@coroutine.com" 34 | gemspec.homepage = "http://github.com/coroutine/michael_hintbuble" 35 | gemspec.name = "michael_hintbuble" 36 | gemspec.summary = "Dead simple, beautiful hint bubbles for Rails." 37 | 38 | gemspec.add_dependency("actionpack", ">=2.3.4") 39 | gemspec.add_development_dependency("activesupport", ">=2.3.4") 40 | 41 | gemspec.files.include("generators/**/*", "lib/**/*") 42 | gemspec.files.include("test/**/*") 43 | end 44 | Jeweler::GemcutterTasks.new 45 | rescue LoadError 46 | puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" 47 | end 48 | 49 | 50 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | MichaelHintbubleExample::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # The production environment is meant for finished, "live" apps. 5 | # Code is not reloaded between requests 6 | config.cache_classes = true 7 | 8 | # Full error reports are disabled and caching is turned on 9 | config.consider_all_requests_local = false 10 | config.action_controller.perform_caching = true 11 | 12 | # Specifies the header that your server uses for sending files 13 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 14 | 15 | # For nginx: 16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 17 | 18 | # If you have no front-end server that supports something like X-Sendfile, 19 | # just comment this out and Rails will serve the files 20 | 21 | # See everything in the log (default is :info) 22 | # config.log_level = :debug 23 | 24 | # Use a different logger for distributed setups 25 | # config.logger = SyslogLogger.new 26 | 27 | # Use a different cache store in production 28 | # config.cache_store = :mem_cache_store 29 | 30 | # Disable Rails's static asset server 31 | # In production, Apache or nginx will already do this 32 | config.serve_static_assets = false 33 | 34 | # Enable serving of images, stylesheets, and javascripts from an asset server 35 | # config.action_controller.asset_host = "http://assets.example.com" 36 | 37 | # Disable delivery errors, bad email addresses will be ignored 38 | # config.action_mailer.raise_delivery_errors = false 39 | 40 | # Enable threaded mode 41 | # config.threadsafe! 42 | 43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 44 | # the I18n.default_locale when a translation can not be found) 45 | config.i18n.fallbacks = true 46 | 47 | # Send deprecation notices to registered listeners 48 | config.active_support.deprecation = :notify 49 | end 50 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: vendor/gems 3 | specs: 4 | michael_hintbuble (1.0.5) 5 | actionpack (>= 2.3.4) 6 | 7 | GEM 8 | remote: http://rubygems.org/ 9 | specs: 10 | abstract (1.0.0) 11 | actionmailer (3.0.3) 12 | actionpack (= 3.0.3) 13 | mail (~> 2.2.9) 14 | actionpack (3.0.3) 15 | activemodel (= 3.0.3) 16 | activesupport (= 3.0.3) 17 | builder (~> 2.1.2) 18 | erubis (~> 2.6.6) 19 | i18n (~> 0.4) 20 | rack (~> 1.2.1) 21 | rack-mount (~> 0.6.13) 22 | rack-test (~> 0.5.6) 23 | tzinfo (~> 0.3.23) 24 | activemodel (3.0.3) 25 | activesupport (= 3.0.3) 26 | builder (~> 2.1.2) 27 | i18n (~> 0.4) 28 | activerecord (3.0.3) 29 | activemodel (= 3.0.3) 30 | activesupport (= 3.0.3) 31 | arel (~> 2.0.2) 32 | tzinfo (~> 0.3.23) 33 | activeresource (3.0.3) 34 | activemodel (= 3.0.3) 35 | activesupport (= 3.0.3) 36 | activesupport (3.0.3) 37 | arel (2.0.7) 38 | builder (2.1.2) 39 | erubis (2.6.6) 40 | abstract (>= 1.0.0) 41 | i18n (0.5.0) 42 | mail (2.2.15) 43 | activesupport (>= 2.3.6) 44 | i18n (>= 0.4.0) 45 | mime-types (~> 1.16) 46 | treetop (~> 1.4.8) 47 | mime-types (1.16) 48 | polyglot (0.3.1) 49 | rack (1.2.1) 50 | rack-mount (0.6.13) 51 | rack (>= 1.0.0) 52 | rack-test (0.5.7) 53 | rack (>= 1.0) 54 | rails (3.0.3) 55 | actionmailer (= 3.0.3) 56 | actionpack (= 3.0.3) 57 | activerecord (= 3.0.3) 58 | activeresource (= 3.0.3) 59 | activesupport (= 3.0.3) 60 | bundler (~> 1.0) 61 | railties (= 3.0.3) 62 | railties (3.0.3) 63 | actionpack (= 3.0.3) 64 | activesupport (= 3.0.3) 65 | rake (>= 0.8.7) 66 | thor (~> 0.14.4) 67 | rake (0.8.7) 68 | sqlite3 (1.3.3) 69 | thor (0.14.6) 70 | treetop (1.4.9) 71 | polyglot (>= 0.3.1) 72 | tzinfo (0.3.24) 73 | 74 | PLATFORMS 75 | ruby 76 | 77 | DEPENDENCIES 78 | michael_hintbuble! 79 | rack 80 | rails (= 3.0.3) 81 | sqlite3 82 | -------------------------------------------------------------------------------- /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 MichaelHintbubleExample 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] 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/views/examples/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |

Console

7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 |
20 |
Top
21 | <%= render_bubble :target_with_top_position, :position => "top" do %> 22 |
This uses block notation instead of the :inline option.
23 | <% end %> 24 |
25 | 26 | 27 |
28 |
Left
29 | <%= render_bubble :target_with_left_position, :class => :help_bubble, :position => "left", :inline => "
This example uses inline markup.
" %> 30 |
31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 |
Bottom
46 | 50 | <%= render_bubble :target_with_bottom_position, :class => :error_bubble, :position => "bottom", :text => "This example uses simple text." %> 51 |
52 | 53 | 54 |
55 | 56 | <%= render_bubble :target_with_right_position, 57 | :class => :error_bubble, :position => "right", :event_names => ["focus"], 58 | :partial => "partial_example", :locals => { :name => "John" } 59 | %> 60 |
61 | 62 |
63 |
-------------------------------------------------------------------------------- /public/stylesheets/michael_hintbuble.css: -------------------------------------------------------------------------------- 1 | /* common styles */ 2 | .michael_hintbuble_bubble_frame { 3 | position: absolute; 4 | border: none; 5 | z-index: 1; 6 | filter: alpha(opacity=0); /* really only needed if ie6 suport is enabled */ 7 | } 8 | .michael_hintbuble_bubble { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | z-index: 2; 13 | filter: alpha(opacity=100); /* really only needed if ie6 suport is enabled */ 14 | width: 240px; 15 | } 16 | .michael_hintbuble_bubble .container { 17 | margin: 2px; 18 | padding: 8px; 19 | font-size: .85em; 20 | } 21 | .michael_hintbuble_bubble .container .content { 22 | padding: 8px; 23 | background: #333; 24 | color: #FFF; 25 | border-radius: 2px; 26 | -moz-border-radius: 2px; 27 | -webkit-border-radius: 2px; 28 | } 29 | .michael_hintbuble_bubble .bottom, 30 | .michael_hintbuble_bubble .left, 31 | .michael_hintbuble_bubble .right, 32 | .michael_hintbuble_bubble .top { 33 | background-position: top; 34 | background-repeat: no-repeat; 35 | } 36 | .michael_hintbuble_bubble .left { 37 | background-position: right; 38 | } 39 | .michael_hintbuble_bubble .right { 40 | background-position: left; 41 | } 42 | .michael_hintbuble_bubble .top { 43 | background-position: bottom; 44 | } 45 | 46 | 47 | /* error override styles */ 48 | .error_bubble { 49 | width: 250px; 50 | } 51 | .error_bubble .container .content { 52 | background: #d00; 53 | color: #fff; 54 | } 55 | .error_bubble .bottom, 56 | .error_bubble .left, 57 | .error_bubble .right, 58 | .error_bubble .top { 59 | background-image: url('../images/error_bubble_pointer.png'); 60 | } 61 | 62 | 63 | /* help override styles */ 64 | .help_bubble { 65 | width: 300px; 66 | } 67 | .help_bubble .container .content { 68 | background: #333; 69 | color: #fff; 70 | } 71 | .help_bubble .bottom, 72 | .help_bubble .left, 73 | .help_bubble .right, 74 | .help_bubble .top { 75 | background-image: url('../images/help_bubble_pointer.png'); 76 | } -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/generators/michael_hintbuble/templates/michael_hintbuble.css: -------------------------------------------------------------------------------- 1 | /* common styles */ 2 | .michael_hintbuble_bubble_frame { 3 | position: absolute; 4 | border: none; 5 | z-index: 1; 6 | filter: alpha(opacity=0); /* really only needed if ie6 suport is enabled */ 7 | } 8 | .michael_hintbuble_bubble { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | z-index: 2; 13 | filter: alpha(opacity=100); /* really only needed if ie6 suport is enabled */ 14 | width: 240px; 15 | } 16 | .michael_hintbuble_bubble .container { 17 | margin: 2px; 18 | padding: 8px; 19 | font-size: .85em; 20 | } 21 | .michael_hintbuble_bubble .container .content { 22 | padding: 8px; 23 | background: #333; 24 | color: #FFF; 25 | border-radius: 2px; 26 | -moz-border-radius: 2px; 27 | -webkit-border-radius: 2px; 28 | } 29 | .michael_hintbuble_bubble .bottom, 30 | .michael_hintbuble_bubble .left, 31 | .michael_hintbuble_bubble .right, 32 | .michael_hintbuble_bubble .top { 33 | background-position: top; 34 | background-repeat: no-repeat; 35 | } 36 | .michael_hintbuble_bubble .left { 37 | background-position: right; 38 | } 39 | .michael_hintbuble_bubble .right { 40 | background-position: left; 41 | } 42 | .michael_hintbuble_bubble .top { 43 | background-position: bottom; 44 | } 45 | 46 | 47 | /* error override styles */ 48 | .error_bubble { 49 | width: 250px; 50 | } 51 | .error_bubble .container .content { 52 | background: #d00; 53 | color: #fff; 54 | } 55 | .error_bubble .bottom, 56 | .error_bubble .left, 57 | .error_bubble .right, 58 | .error_bubble .top { 59 | background-image: url('../images/error_bubble_pointer.png'); 60 | } 61 | 62 | 63 | /* help override styles */ 64 | .help_bubble { 65 | width: 300px; 66 | } 67 | .help_bubble .container .content { 68 | background: #333; 69 | color: #fff; 70 | } 71 | .help_bubble .bottom, 72 | .help_bubble .left, 73 | .help_bubble .right, 74 | .help_bubble .top { 75 | background-image: url('../images/help_bubble_pointer.png'); 76 | } -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/lib/generators/michael_hintbuble/templates/michael_hintbuble.css: -------------------------------------------------------------------------------- 1 | /* common styles */ 2 | .michael_hintbuble_bubble_frame { 3 | position: absolute; 4 | border: none; 5 | z-index: 1; 6 | filter: alpha(opacity=0); /* really only needed if ie6 suport is enabled */ 7 | } 8 | .michael_hintbuble_bubble { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | z-index: 2; 13 | filter: alpha(opacity=100); /* really only needed if ie6 suport is enabled */ 14 | width: 240px; 15 | } 16 | .michael_hintbuble_bubble .container { 17 | margin: 2px; 18 | padding: 8px; 19 | font-size: .85em; 20 | } 21 | .michael_hintbuble_bubble .container .content { 22 | padding: 8px; 23 | background: #333; 24 | color: #FFF; 25 | border-radius: 2px; 26 | -moz-border-radius: 2px; 27 | -webkit-border-radius: 2px; 28 | } 29 | .michael_hintbuble_bubble .bottom, 30 | .michael_hintbuble_bubble .left, 31 | .michael_hintbuble_bubble .right, 32 | .michael_hintbuble_bubble .top { 33 | background-position: top; 34 | background-repeat: no-repeat; 35 | } 36 | .michael_hintbuble_bubble .left { 37 | background-position: right; 38 | } 39 | .michael_hintbuble_bubble .right { 40 | background-position: left; 41 | } 42 | .michael_hintbuble_bubble .top { 43 | background-position: bottom; 44 | } 45 | 46 | 47 | /* error override styles */ 48 | .error_bubble { 49 | width: 250px; 50 | } 51 | .error_bubble .container .content { 52 | background: #d00; 53 | color: #fff; 54 | } 55 | .error_bubble .bottom, 56 | .error_bubble .left, 57 | .error_bubble .right, 58 | .error_bubble .top { 59 | background-image: url('../images/error_bubble_pointer.png'); 60 | } 61 | 62 | 63 | /* help override styles */ 64 | .help_bubble { 65 | width: 300px; 66 | } 67 | .help_bubble .container .content { 68 | background: #333; 69 | color: #fff; 70 | } 71 | .help_bubble .bottom, 72 | .help_bubble .left, 73 | .help_bubble .right, 74 | .help_bubble .top { 75 | background-image: url('../images/help_bubble_pointer.png'); 76 | } -------------------------------------------------------------------------------- /public/stylesheets/screen.css: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------- 2 | reset styles 3 | -----------------------------------------------*/ 4 | 5 | body, div, ol, ul, li, h1, h2, h3, h4, h5, h6, form, fieldset, p, hr { 6 | padding: 0; 7 | margin: 0; 8 | font-weight: normal; 9 | text-align: left; 10 | border: none; 11 | } 12 | table { 13 | border-collapse: collapse; 14 | } 15 | img { 16 | border: none; 17 | margin: 0; 18 | padding: 0; 19 | } 20 | ol, ul { 21 | list-style: none; 22 | } 23 | 24 | 25 | /*--------------------------------------------- 26 | core styles 27 | -----------------------------------------------*/ 28 | 29 | body { 30 | width: 1600px; 31 | height: 1000px; 32 | font-size: 12px/14px; 33 | font-family: Helvetica, Arial, Sans-Serif; 34 | background: #fff; 35 | color: #333; 36 | } 37 | input { 38 | padding: 2px; 39 | font-size: 16px; 40 | border: 1px solid #999; 41 | } 42 | 43 | 44 | 45 | /*--------------------------------------------- 46 | example styles 47 | -----------------------------------------------*/ 48 | 49 | .main_container { 50 | float: left; 51 | margin: 20px 100px; 52 | padding: 10px 0; 53 | width: 500px; 54 | height: 500px; 55 | border: 1px solid #999; 56 | border-radius: 12px; 57 | -moz-border-radius: 12px; 58 | -webkit-border-radius: 12px; 59 | } 60 | .scrollable { 61 | overflow: auto; 62 | } 63 | .target { 64 | cursor: pointer; 65 | padding: 6px 12px; 66 | width: 80px; 67 | text-align: center; 68 | background: #00a0c6; 69 | color: #fff; 70 | border-radius: 6px; 71 | -moz-border-radius: 6px; 72 | -webkit-border-radius: 6px; 73 | } 74 | 75 | #scrolling_container { 76 | width: 600px; 77 | height: 1200px; 78 | } 79 | #top_container { 80 | margin: 50px 0 0 100px; 81 | } 82 | #bottom_container { 83 | margin: 50px 0 0 100px; 84 | } 85 | #left_container { 86 | margin: 250px 0 0 100px; 87 | } 88 | #right_container { 89 | margin: 200px 0 0 100px; 90 | } 91 | 92 | #selector_for_bottom_position { 93 | margin-top: 20px; 94 | } 95 | 96 | #console { 97 | position: absolute; 98 | top: 600px; 99 | left: 100px; 100 | padding-top: 50px; 101 | width: 1200px; 102 | } 103 | #console h2 { 104 | border-bottom: 1px solid #333; 105 | } 106 | #console .log_container div { 107 | margin: 6px 0; 108 | padding: 6px 0; 109 | font-size: 14px; 110 | border-bottom: 1px solid #ccc; 111 | } -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/michael_hintbuble.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{michael_hintbuble} 8 | s.version = "1.0.5" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Coroutine", "Tim Lowrimore", "John Dugan"] 12 | s.date = %q{2010-10-19} 13 | s.description = %q{Michael HintBuble allows you to generate hint bubbles and tooltips in Rails applications using the same syntax used for rendering templates.} 14 | s.email = %q{gems@coroutine.com} 15 | s.extra_rdoc_files = [ 16 | "README.rdoc" 17 | ] 18 | s.files = [ 19 | ".gitignore", 20 | ".specification", 21 | "MIT-LICENSE", 22 | "README.rdoc", 23 | "Rakefile", 24 | "VERSION", 25 | "generators/michael_hintbuble/michael_hintbuble_generator.rb", 26 | "generators/michael_hintbuble/templates/error_bubble_pointer.png", 27 | "generators/michael_hintbuble/templates/help_bubble_pointer.png", 28 | "generators/michael_hintbuble/templates/michael_hintbuble.css", 29 | "generators/michael_hintbuble/templates/michael_hintbuble.js", 30 | "init.rb", 31 | "lib/generators/michael_hintbuble/michael_hintbuble_generator.rb", 32 | "lib/generators/michael_hintbuble/templates/error_bubble_pointer.png", 33 | "lib/generators/michael_hintbuble/templates/help_bubble_pointer.png", 34 | "lib/generators/michael_hintbuble/templates/michael_hintbuble.css", 35 | "lib/generators/michael_hintbuble/templates/michael_hintbuble.js", 36 | "lib/michael_hintbuble.rb", 37 | "lib/michael_hintbuble/helpers.rb", 38 | "michael_hintbuble.gemspec", 39 | "rails/init.rb", 40 | "test/michael_hintbuble/helpers_test.rb", 41 | "test/test_helper.rb" 42 | ] 43 | s.homepage = %q{http://github.com/coroutine/michael_hintbuble} 44 | s.rdoc_options = ["--charset=UTF-8"] 45 | s.require_paths = ["lib"] 46 | s.rubygems_version = %q{1.3.7} 47 | s.summary = %q{Dead simple, beautiful hint bubbles for Rails.} 48 | s.test_files = [ 49 | "test/michael_hintbuble/helpers_test.rb", 50 | "test/test_helper.rb" 51 | ] 52 | 53 | if s.respond_to? :specification_version then 54 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 55 | s.specification_version = 3 56 | 57 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 58 | s.add_runtime_dependency(%q, [">= 2.3.4"]) 59 | s.add_development_dependency(%q, [">= 2.3.4"]) 60 | else 61 | s.add_dependency(%q, [">= 2.3.4"]) 62 | s.add_dependency(%q, [">= 2.3.4"]) 63 | end 64 | else 65 | s.add_dependency(%q, [">= 2.3.4"]) 66 | s.add_dependency(%q, [">= 2.3.4"]) 67 | end 68 | end 69 | 70 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/.specification: -------------------------------------------------------------------------------- 1 | --- !ruby/object:Gem::Specification 2 | name: michael_hintbuble 3 | version: !ruby/object:Gem::Version 4 | hash: 31 5 | prerelease: false 6 | segments: 7 | - 1 8 | - 0 9 | - 4 10 | version: 1.0.4 11 | platform: ruby 12 | authors: 13 | - Coroutine 14 | - Tim Lowrimore 15 | - John Dugan 16 | autorequire: 17 | bindir: bin 18 | cert_chain: [] 19 | 20 | date: 2010-10-19 00:00:00 -05:00 21 | default_executable: 22 | dependencies: 23 | - !ruby/object:Gem::Dependency 24 | name: actionpack 25 | prerelease: false 26 | requirement: &id001 !ruby/object:Gem::Requirement 27 | none: false 28 | requirements: 29 | - - ">=" 30 | - !ruby/object:Gem::Version 31 | hash: 11 32 | segments: 33 | - 2 34 | - 3 35 | - 4 36 | version: 2.3.4 37 | type: :runtime 38 | version_requirements: *id001 39 | - !ruby/object:Gem::Dependency 40 | name: activesupport 41 | prerelease: false 42 | requirement: &id002 !ruby/object:Gem::Requirement 43 | none: false 44 | requirements: 45 | - - ">=" 46 | - !ruby/object:Gem::Version 47 | hash: 11 48 | segments: 49 | - 2 50 | - 3 51 | - 4 52 | version: 2.3.4 53 | type: :development 54 | version_requirements: *id002 55 | description: Michael HintBuble allows you to generate hint bubbles and tooltips in Rails applications using the same syntax used for rendering templates. 56 | email: gems@coroutine.com 57 | executables: [] 58 | 59 | extensions: [] 60 | 61 | extra_rdoc_files: 62 | - README.rdoc 63 | files: 64 | - .gitignore 65 | - .specification 66 | - MIT-LICENSE 67 | - README.rdoc 68 | - Rakefile 69 | - VERSION 70 | - generators/michael_hintbuble/michael_hintbuble_generator.rb 71 | - generators/michael_hintbuble/templates/error_bubble_pointer.png 72 | - generators/michael_hintbuble/templates/help_bubble_pointer.png 73 | - generators/michael_hintbuble/templates/michael_hintbuble.css 74 | - generators/michael_hintbuble/templates/michael_hintbuble.js 75 | - init.rb 76 | - lib/generators/michael_hintbuble/michael_hintbuble_generator.rb 77 | - lib/generators/michael_hintbuble/templates/error_bubble_pointer.png 78 | - lib/generators/michael_hintbuble/templates/help_bubble_pointer.png 79 | - lib/generators/michael_hintbuble/templates/michael_hintbuble.css 80 | - lib/generators/michael_hintbuble/templates/michael_hintbuble.js 81 | - lib/michael_hintbuble.rb 82 | - lib/michael_hintbuble/helpers.rb 83 | - michael_hintbuble.gemspec 84 | - rails/init.rb 85 | - test/michael_hintbuble/helpers_test.rb 86 | - test/test_helper.rb 87 | has_rdoc: true 88 | homepage: http://github.com/coroutine/michael_hintbuble 89 | licenses: [] 90 | 91 | post_install_message: 92 | rdoc_options: 93 | - --charset=UTF-8 94 | require_paths: 95 | - lib 96 | required_ruby_version: !ruby/object:Gem::Requirement 97 | none: false 98 | requirements: 99 | - - ">=" 100 | - !ruby/object:Gem::Version 101 | hash: 3 102 | segments: 103 | - 0 104 | version: "0" 105 | required_rubygems_version: !ruby/object:Gem::Requirement 106 | none: false 107 | requirements: 108 | - - ">=" 109 | - !ruby/object:Gem::Version 110 | hash: 3 111 | segments: 112 | - 0 113 | version: "0" 114 | requirements: [] 115 | 116 | rubyforge_project: 117 | rubygems_version: 1.3.7 118 | signing_key: 119 | specification_version: 3 120 | summary: Dead simple, beautiful hint bubbles for Rails. 121 | test_files: 122 | - test/michael_hintbuble/helpers_test.rb 123 | - test/test_helper.rb 124 | 125 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/test/michael_hintbuble/helpers_test.rb: -------------------------------------------------------------------------------- 1 | #--------------------------------------------------------- 2 | # Requirements 3 | #--------------------------------------------------------- 4 | 5 | # all generic stuff required by test helper 6 | require "test/test_helper" 7 | 8 | 9 | 10 | #--------------------------------------------------------- 11 | # Class Definitions 12 | #--------------------------------------------------------- 13 | 14 | class TestView < ActionView::Base 15 | end 16 | 17 | 18 | 19 | #--------------------------------------------------------- 20 | # Tests 21 | #--------------------------------------------------------- 22 | 23 | class MichaelHintbubleHelpersTest < ActionView::TestCase 24 | 25 | def setup 26 | @view = TestView.new 27 | end 28 | 29 | 30 | def test_composition 31 | assert @view.respond_to?(:render_bubble, false) 32 | assert @view.respond_to?(:show_bubble, false) 33 | assert @view.respond_to?(:hide_bubble, false) 34 | assert @view.respond_to?(:bubble_javascript_option_keys, true) 35 | assert @view.respond_to?(:bubble_options_to_js, true) 36 | assert @view.respond_to?(:extract_bubble_javascript_options, true) 37 | assert @view.respond_to?(:extract_bubble_render_options, true) 38 | end 39 | 40 | 41 | #--------------------------------------------------------- 42 | # Public methods 43 | #--------------------------------------------------------- 44 | 45 | def test_render_bubble_with_simple_options 46 | text = "Who wants to play volleyball on a court with a four-foot net?" 47 | default_options = "{class:'michael_hintbuble_bubble',eventNames:['mouseover','resize','scroll'],position:'right'}" 48 | 49 | expected = "" 54 | actual = @view.render_bubble(:come_fly_with_me, :text => text) 55 | 56 | assert_equal expected, actual 57 | end 58 | 59 | 60 | def test_show_bubble 61 | expected = "MichaelHintbuble.Bubble.show('come_fly_with_me');" 62 | actual = @view.show_bubble(:come_fly_with_me) 63 | 64 | assert_equal expected, actual 65 | end 66 | 67 | 68 | def test_hide_dialog 69 | expected = "MichaelHintbuble.Bubble.hide('come_fly_with_me');" 70 | actual = @view.hide_bubble(:come_fly_with_me) 71 | 72 | assert_equal expected, actual 73 | end 74 | 75 | 76 | 77 | #--------------------------------------------------------- 78 | # Public methods 79 | #--------------------------------------------------------- 80 | 81 | def test_bubble_javascript_option_keys 82 | expected = [:class, :style, :position, :event_names, :before_show, :after_show, :before_hide, :after_hide] 83 | actual = @view.send(:bubble_javascript_option_keys) 84 | 85 | assert_equal expected, actual 86 | end 87 | 88 | 89 | def test_bubble_options_to_js 90 | options = { 91 | :class => "error_container", 92 | :position => "top", 93 | :event_names => ["mouseover","resize","scroll"], 94 | :before_show => "function{ alert('hello, world!'); }", 95 | :after_show => "function{ alert('goodbye, world!'); }" 96 | } 97 | 98 | expected = "{" + 99 | "afterShow:function{ alert('goodbye, world!'); }" + "," + 100 | "beforeShow:function{ alert('hello, world!'); }" + "," + 101 | "class:'error_container'" + "," + 102 | "eventNames:['mouseover','resize','scroll']" + "," + 103 | "position:'top'" + 104 | "}" 105 | actual = @view.send(:bubble_options_to_js, options) 106 | 107 | assert_equal expected, actual 108 | end 109 | 110 | 111 | def test_extract_bubble_javascript_options 112 | options = { :class => "my_class", :event_names => ["focus", "resize", "scroll"], :position => "right", :text => "Text" } 113 | js_options = @view.send(:extract_bubble_javascript_options, options) 114 | 115 | assert_equal true, js_options.has_key?(:class) 116 | assert_equal true, js_options.has_key?(:event_names) 117 | assert_equal true, js_options.has_key?(:position) 118 | assert_equal false, js_options.has_key?(:text) 119 | 120 | assert_equal "my_class", js_options[:class] 121 | assert_equal ["focus", "resize", "scroll"], js_options[:event_names] 122 | assert_equal "right", js_options[:position] 123 | end 124 | def test_extract_bubble_javascript_options_for_default_class 125 | options = { :event_names => ["focus", "resize", "scroll"], :position => "top right", :text => "Text" } 126 | js_options = @view.send(:extract_bubble_javascript_options, options) 127 | 128 | assert_equal "michael_hintbuble_bubble", js_options[:class] 129 | end 130 | def test_extract_bubble_javascript_options_for_default_event_names 131 | options = { :class => "my_class", :position => "top right", :text => "Text" } 132 | js_options = @view.send(:extract_bubble_javascript_options, options) 133 | 134 | assert_equal ["mouseover", "resize", "scroll"], js_options[:event_names] 135 | end 136 | def test_extract_bubble_javascript_options_for_default_position 137 | options = { :class => "my_class", :event_names => ["focus", "resize", "scroll"], :text => "Text" } 138 | js_options = @view.send(:extract_bubble_javascript_options, options) 139 | 140 | assert_equal "right", js_options[:position] 141 | end 142 | 143 | 144 | def test_extract_bubble_render_options 145 | options = { :class => :my_class, :event_names => [:focus, :resize, :scroll], :position => "top right", :text => "Text" } 146 | render_options = @view.send(:extract_bubble_render_options, options) 147 | 148 | assert_equal false, render_options.has_key?(:class) 149 | assert_equal false, render_options.has_key?(:event_names) 150 | assert_equal false, render_options.has_key?(:position) 151 | assert_equal true, render_options.has_key?(:text) 152 | 153 | assert_equal "Text", render_options[:text] 154 | end 155 | 156 | end -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/lib/michael_hintbuble/helpers.rb: -------------------------------------------------------------------------------- 1 | module Coroutine #:nodoc: 2 | module MichaelHintbuble #:nodoc: 3 | module Helpers #:nodoc: 4 | 5 | # This method returns a javascript tag containing the hint bubble initialization logic. The first argument 6 | # to this method is target_id, the id of the html element to which the hint bubble is anchored. 7 | # The target_id is required and should be unique (duh). Further options may be provided; those that 8 | # are specific to the hint bubble are: 9 | # 10 | # * :class - the css style to assign to the outermost div container (defaults to "michael_hintbuble_bubble") 11 | # * :position - css-style value that specifies the hint bubble's relative position, e.g., top, bottom, right, or left (defaults to right) 12 | # * :event_names - an array of strings specifying the events that should trigger the display of the hint bubble 13 | # * :before_show - a Javascript function that will be invoked before the hint bubble is shown 14 | # * :after_show - a Javascript function that will be invoked after the hint bubble has been shown 15 | # * :before_hide - a Javascript function that will be invoked before the hint bubble is hidden 16 | # * :after_hide - a Javascript function that will be invoked after the hint bubble has been hidden 17 | # * &block - HTML markup that will be automatically converted to render's inline option 18 | # 19 | # All remaining options are the same as the options available to ActionController::Base#render. Please 20 | # see the documentation for ActionController::Base#render for further details. 21 | # 22 | # ==== Events 23 | # 24 | # The library manages repositioning the hint bubble in response to window resizing and scrolling events automatically. 25 | # You do not need to specify resize or scroll in the optional event names array. 26 | # 27 | # The library defaults to trapping mouse gestures, but it is capable of trapping focus events in addition 28 | # to or in place of mouse events. When specifying events, you need only reference the positive action: the 29 | # library can infer the corresponding action to toggle off the tooltip. 30 | # 31 | # Valid entries for the events array are any combination of the following options: 32 | # 33 | # * "focus" 34 | # * "mouseover" 35 | # 36 | # 37 | # ==== Example 38 | # 39 | # # Generates: 40 | # # 41 | # # 46 | # <%= render_bubble :foo_target_id, :content => "What up, foo?", :position => "top center" %> 47 | # 48 | # In this case, a simple hint bubble is produced with the specified text content. The bubble responds to mouseover/mouseout 49 | # events and centers itself above the target element when shown. 50 | # 51 | # 52 | # ==== Example 53 | # 54 | # # Generates: 55 | # # 56 | # # 61 | # <%= render_bubble :bar_target_id, :event_names => ["focus"], :position => "center left" %> 62 | #
    63 | #
  • Item 1
  • 64 | #
  • Item 2
  • 65 | #
66 | # <% end %> 67 | # 68 | # In this case, a slightly more complex hint bubble is produced with the specified markup. The bubble responds to focus/blur 69 | # events and positions itself to the left of the target. 70 | # 71 | def render_bubble(target_id, options = {}, &block) 72 | options[:inline] = capture(&block) if block_given? 73 | render_options = extract_bubble_render_options(options) 74 | javascript_options = bubble_options_to_js(extract_bubble_javascript_options(options)) 75 | 76 | content = escape_javascript(render(render_options)) 77 | 78 | raise "You gotta specify a target id to register a hint bubble, baby." unless target_id 79 | raise "You gotta provide content to register a hint bubble, baby." unless content 80 | 81 | javascript_tag "Event.observe(window, 'load', function() { MichaelHintbuble.Bubble.instances['#{target_id}'] = new MichaelHintbuble.Bubble('#{target_id}', '#{content}', #{javascript_options}) });" 82 | end 83 | 84 | 85 | # This method returns a Javascript string that will show the bubble attached to the supplied 86 | # target id. 87 | # 88 | def show_bubble(target_id) 89 | "MichaelHintbuble.Bubble.show('#{target_id}');" 90 | end 91 | 92 | 93 | # This method returns a Javascript string that will hide the bubble attached to the supplied 94 | # target id. 95 | # 96 | def hide_bubble(target_id) 97 | "MichaelHintbuble.Bubble.hide('#{target_id}');" 98 | end 99 | 100 | 101 | 102 | private 103 | 104 | # This method returns an array of javascript option keys supported by the accompanying 105 | # javascript library. 106 | # 107 | def bubble_javascript_option_keys 108 | [:class, :position, :event_names, :before_show, :after_show, :before_hide, :after_hide] 109 | end 110 | 111 | 112 | # This method converts ruby hashes using underscore notation to js strings using camelcase 113 | # notation, which is more common in javascript. 114 | # 115 | def bubble_options_to_js(options={}) 116 | js_kv_pairs = [] 117 | sorted_keys = options.keys.map { |k| k.to_s }.sort.map { |s| s.to_sym } 118 | 119 | sorted_keys.each do |key| 120 | js_key = key.to_s.camelcase(:lower) 121 | js_value = "null" 122 | 123 | options[key] = options[key].to_s unless options[key].respond_to?(:empty?) 124 | 125 | unless options[key].empty? 126 | case key 127 | when :before_show, :after_show, :before_hide, :after_hide 128 | js_value = "#{options[key]}" 129 | when :event_names 130 | js_value = "['" + options[key].join("','") + "']" 131 | else 132 | js_value = "'#{options[key]}'" 133 | end 134 | end 135 | 136 | js_kv_pairs << "'#{js_key}':#{js_value}" 137 | end 138 | 139 | "{#{js_kv_pairs.join(',')}}" 140 | end 141 | 142 | 143 | # This method returns a hash with javascript options. It also inspects the supplied options 144 | # and adds defaults as necessary. 145 | # 146 | def extract_bubble_javascript_options(options) 147 | js_options = options.reject { |k,v| !bubble_javascript_option_keys.include?(k) } 148 | 149 | js_options[:position] = "right" if js_options[:position].blank? 150 | 151 | js_options[:event_names] = [] if js_options[:event_names].blank? 152 | js_options[:event_names] = js_options[:event_names].uniq.map { |en| en.to_s } 153 | js_options[:event_names] << "mouseover" if js_options[:event_names].empty? 154 | 155 | js_options 156 | end 157 | 158 | 159 | # This method returns a hash with rendering options. It also inspects the supplied options 160 | # and adds defaults as necessary. 161 | # 162 | def extract_bubble_render_options(options) 163 | options.reject { |k,v| bubble_javascript_option_keys.include?(k) } 164 | end 165 | 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/README.rdoc: -------------------------------------------------------------------------------- 1 | = Michael Hintbuble 2 | 3 | Hiya. 4 | 5 | I'm Michael Hintbuble. A lot of people confuse me with the singer who rose to fame singing jazz 6 | standards, but I'm a different guy altogether. 7 | 8 | Don't get me wrong, that Michael and I are into a lot of the same things, like dating Emily Blunt and 9 | opening fictitious restaurants with Jon Hamm. But while that Michael spent his career focused on smooth 10 | vocal stylings and making ladies of all nationalities swoon, I only care about one thing. 11 | 12 | Freakin' Hint Bubbles. 13 | 14 | If you're a Rails developer who wants to use hint bubbles but doesn't want to mess with Javascript, I 15 | suggest you get yourself a firmer grip on your knickers. 16 | 17 | Check it. If you want my hint bubble anchored to an element in one of your views, here's what you do: 18 | 19 | # Define the target 20 |
Hamm and Buble
21 | 22 | # Create and attach the bubble 23 | <%= render_bubble :restaurant_header, 24 | :position => :right, :event_names => [:mouseover], 25 | :partial => "map", :locals => { :address => @restaurant.address } %> 26 | 27 | That's pretty much it. Did you notice how render_bubble takes the same options as 28 | render? The content for your bubble can just be a regular old partial. 29 | 30 | Boom. 31 | 32 | What's that, you want a few more details? You got it, bro. 33 | 34 | render_bubble always needs the unique id for the target element as its first argument. After 35 | that, it will take the same options as ActionController::Base#render. It'll also take a few 36 | more, namely: 37 | 38 | * :class - an additional css style to assign to the outermost div container (facilitates multiple stylings) 39 | * :position - css-style value that specifies the hint bubble's relative position, e.g., top, bottom, right, or left (defaults to right) 40 | * :event_names - an array of strings specifying the events that should trigger the display of the hint bubble (accepts focus and/or mouseover) 41 | * :before_show - a Javascript function that will be invoked before the hint bubble is shown 42 | * :after_show - a Javascript function that will be invoked after the hint bubble has been shown 43 | * :before_hide - a Javascript function that will be invoked before the hint bubble is hidden 44 | * :after_hide - a Javascript function that will be invoked after the hint bubble has been hidden 45 | * &block - HTML markup that will be automatically converted to render's inline option 46 | 47 | Here's an example using the before_show option with block notation: 48 | 49 | # Define the target 50 |
I have a feeling that he's standing right behind me.
51 | 52 | # Create and attach the bubble 53 | <%= render_bubble :cry_for_help, :position => :bottom, :before_show => "function() { JonHamm.eyes.goBlack(); }" %> 54 |
You are on the thinnest of ice.
55 | <% end %> 56 | 57 | If you need more help than that, maybe you should just look at the source code. There are a ton of comments 58 | in there. 59 | 60 | 61 | Cheers, 62 | 63 | Michael Hintbuble 64 | 65 | 66 | 67 | == Multiple Hint Bubble Styles 68 | 69 | If you need to style more than one kind of hint bubble (e.g., one style for tooltips, one style for errors), just 70 | use the :class option to append a css class name on the outermost div. That'll give you a logical anchor 71 | around which you can restyle all the interior classes. 72 | 73 | Please note that the blocking iframe is automatically given an additional class name equal to the outermost div's class name plus 74 | the string "_frame". 75 | 76 | By default, the top-level class assignments are "michael_hintbuble_bubble" and "michael_hintbuble_bubble_frame". 77 | 78 | If you set the :class option to :error_bubble, the top-level class assignments will be 79 | "michael_hintbuble_bubble error_bubble" and "michael_hintbuble_bubble_frame error_bubble_frame". 80 | 81 | 82 | 83 | == Positioning Notes 84 | 85 | Windows get resized, documents and divs scroll, stuff happens. Sometimes the area in which you intended for a 86 | hint bubble to appear ends up off the viewport. Which kind of screws the whole hint bubble UI pattern. 87 | 88 | Good thing I'm so friendly. Here's what I can do to help. 89 | 90 | If you tell me to position the bubble to one side of the target and the bubble can't fit in the viewport over 91 | there, I'll just place it on the opposite side. If it doesn't fit over there either, I'll just give up and 92 | put it where you told me in the first place. I'm not a mindreader, you know. 93 | 94 | 95 | 96 | == IE6 Support 97 | 98 | I'm not what you'd call a huge fan of IE6, so I don't provide a blocking 99 | iframe for my hint bubbles by default. But I can. You just need to ask nicely. 100 | 101 | At the top of the generated javascript file, just change the obviously-named property 102 | hanging right off of the main namespace. Like this. 103 | 104 | MichaelHintbuble.SUPPORT_IE6_BULLSHIT = true; 105 | 106 | That's it. I do the rest. 107 | 108 | 109 | 110 | == Helpful Links 111 | 112 | * Repository: http://github.com/coroutine/michael_hintbuble 113 | * Gem: http://rubygems.org/gems/michael_hintbuble 114 | * Authors: http://coroutine.com 115 | 116 | 117 | 118 | == Prerequisites 119 | 120 | If you want to come fly with me, you'll need to invite the other members of my trio, Prototype and Scriptaculous. 121 | 122 | But since I was designed as a Rails extension, chances are you already have my bandmates 123 | in the mix. 124 | 125 | * Prototype: http://prototypejs.org 126 | * Scriptaculous: http://script.aculo.us 127 | 128 | 129 | 130 | == Installation & Generators (Rails 3) 131 | 132 | Install me from RubyGems.org by adding a gem dependency to your Gemfile. Bundler does 133 | the rest. 134 | 135 | gem "michael_hintbuble" 136 | 137 | $ bundle install 138 | 139 | Then generate the required javascript file and the starter stylesheet and image. 140 | 141 | $ rails g michael_hintbuble 142 | 143 | 144 | 145 | == Installation & Generators (Rails 2) 146 | 147 | Install me from RubyGems.org and add a gem dependency in the appropriate file. 148 | 149 | $ gem install michael_hintbuble 150 | 151 | Or install me as a plugin. 152 | 153 | $ script/plugin install git://github.com/coroutine/michael_hintbuble.git 154 | 155 | Either way, then generate the required javascript file and the starter 156 | stylesheet and image. 157 | 158 | $ script/generate michael_hintbuble 159 | 160 | 161 | 162 | == Gemroll 163 | 164 | If you think I'm awesome, you should check out my soulmate 165 | {Kenny Dialoggins}[http://github.com/coroutine/kenny_dialoggins]. 166 | 167 | Other gems by Coroutine include: 168 | 169 | * {acts_as_current}[http://github.com/coroutine/acts_as_current] 170 | * {acts_as_label}[http://github.com/coroutine/acts_as_label] 171 | * {acts_as_list_with_sti_support}[http://github.com/coroutine/acts_as_list_with_sti_support] 172 | * {delayed_form_observer}[http://github.com/coroutine/delayed_form_observer] 173 | * {tiny_navigation}[http://github.com/coroutine/tiny_navigation] 174 | 175 | 176 | 177 | == License 178 | 179 | Copyright (c) 2010 {Coroutine LLC}[http://coroutine.com]. 180 | 181 | Permission is hereby granted, free of charge, to any person obtaining 182 | a copy of this software and associated documentation files (the 183 | "Software"), to deal in the Software without restriction, including 184 | without limitation the rights to use, copy, modify, merge, publish, 185 | distribute, sublicense, and/or sell copies of the Software, and to 186 | permit persons to whom the Software is furnished to do so, subject to 187 | the following conditions: 188 | 189 | The above copyright notice and this permission notice shall be 190 | included in all copies or substantial portions of the Software. 191 | 192 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 193 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 194 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 195 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 196 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 197 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 198 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | == Welcome to Rails 2 | 3 | Rails is a web-application framework that includes everything needed to create 4 | database-backed web applications according to the Model-View-Control pattern. 5 | 6 | This pattern splits the view (also called the presentation) into "dumb" 7 | templates that are primarily responsible for inserting pre-built data in between 8 | HTML tags. The model contains the "smart" domain objects (such as Account, 9 | Product, Person, Post) that holds all the business logic and knows how to 10 | persist themselves to a database. The controller handles the incoming requests 11 | (such as Save New Account, Update Product, Show Post) by manipulating the model 12 | and directing data to the view. 13 | 14 | In Rails, the model is handled by what's called an object-relational mapping 15 | layer entitled Active Record. This layer allows you to present the data from 16 | database rows as objects and embellish these data objects with business logic 17 | methods. You can read more about Active Record in 18 | link:files/vendor/rails/activerecord/README.html. 19 | 20 | The controller and view are handled by the Action Pack, which handles both 21 | layers by its two parts: Action View and Action Controller. These two layers 22 | are bundled in a single package due to their heavy interdependence. This is 23 | unlike the relationship between the Active Record and Action Pack that is much 24 | more separate. Each of these packages can be used independently outside of 25 | Rails. You can read more about Action Pack in 26 | link:files/vendor/rails/actionpack/README.html. 27 | 28 | 29 | == Getting Started 30 | 31 | 1. At the command prompt, create a new Rails application: 32 | rails new myapp (where myapp is the application name) 33 | 34 | 2. Change directory to myapp and start the web server: 35 | cd myapp; rails server (run with --help for options) 36 | 37 | 3. Go to http://localhost:3000/ and you'll see: 38 | "Welcome aboard: You're riding Ruby on Rails!" 39 | 40 | 4. Follow the guidelines to start developing your application. You can find 41 | the following resources handy: 42 | 43 | * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html 44 | * Ruby on Rails Tutorial Book: http://www.railstutorial.org/ 45 | 46 | 47 | == Debugging Rails 48 | 49 | Sometimes your application goes wrong. Fortunately there are a lot of tools that 50 | will help you debug it and get it back on the rails. 51 | 52 | First area to check is the application log files. Have "tail -f" commands 53 | running on the server.log and development.log. Rails will automatically display 54 | debugging and runtime information to these files. Debugging info will also be 55 | shown in the browser on requests from 127.0.0.1. 56 | 57 | You can also log your own messages directly into the log file from your code 58 | using the Ruby logger class from inside your controllers. Example: 59 | 60 | class WeblogController < ActionController::Base 61 | def destroy 62 | @weblog = Weblog.find(params[:id]) 63 | @weblog.destroy 64 | logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") 65 | end 66 | end 67 | 68 | The result will be a message in your log file along the lines of: 69 | 70 | Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! 71 | 72 | More information on how to use the logger is at http://www.ruby-doc.org/core/ 73 | 74 | Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are 75 | several books available online as well: 76 | 77 | * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) 78 | * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) 79 | 80 | These two books will bring you up to speed on the Ruby language and also on 81 | programming in general. 82 | 83 | 84 | == Debugger 85 | 86 | Debugger support is available through the debugger command when you start your 87 | Mongrel or WEBrick server with --debugger. This means that you can break out of 88 | execution at any point in the code, investigate and change the model, and then, 89 | resume execution! You need to install ruby-debug to run the server in debugging 90 | mode. With gems, use sudo gem install ruby-debug. Example: 91 | 92 | class WeblogController < ActionController::Base 93 | def index 94 | @posts = Post.find(:all) 95 | debugger 96 | end 97 | end 98 | 99 | So the controller will accept the action, run the first line, then present you 100 | with a IRB prompt in the server window. Here you can do things like: 101 | 102 | >> @posts.inspect 103 | => "[#nil, "body"=>nil, "id"=>"1"}>, 105 | #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" 107 | >> @posts.first.title = "hello from a debugger" 108 | => "hello from a debugger" 109 | 110 | ...and even better, you can examine how your runtime objects actually work: 111 | 112 | >> f = @posts.first 113 | => #nil, "body"=>nil, "id"=>"1"}> 114 | >> f. 115 | Display all 152 possibilities? (y or n) 116 | 117 | Finally, when you're ready to resume execution, you can enter "cont". 118 | 119 | 120 | == Console 121 | 122 | The console is a Ruby shell, which allows you to interact with your 123 | application's domain model. Here you'll have all parts of the application 124 | configured, just like it is when the application is running. You can inspect 125 | domain models, change values, and save to the database. Starting the script 126 | without arguments will launch it in the development environment. 127 | 128 | To start the console, run rails console from the application 129 | directory. 130 | 131 | Options: 132 | 133 | * Passing the -s, --sandbox argument will rollback any modifications 134 | made to the database. 135 | * Passing an environment name as an argument will load the corresponding 136 | environment. Example: rails console production. 137 | 138 | To reload your controllers and models after launching the console run 139 | reload! 140 | 141 | More information about irb can be found at: 142 | link:http://www.rubycentral.com/pickaxe/irb.html 143 | 144 | 145 | == dbconsole 146 | 147 | You can go to the command line of your database directly through rails 148 | dbconsole. You would be connected to the database with the credentials 149 | defined in database.yml. Starting the script without arguments will connect you 150 | to the development database. Passing an argument will connect you to a different 151 | database, like rails dbconsole production. Currently works for MySQL, 152 | PostgreSQL and SQLite 3. 153 | 154 | == Description of Contents 155 | 156 | The default directory structure of a generated Ruby on Rails application: 157 | 158 | |-- app 159 | | |-- controllers 160 | | |-- helpers 161 | | |-- models 162 | | `-- views 163 | | `-- layouts 164 | |-- config 165 | | |-- environments 166 | | |-- initializers 167 | | `-- locales 168 | |-- db 169 | |-- doc 170 | |-- lib 171 | | `-- tasks 172 | |-- log 173 | |-- public 174 | | |-- images 175 | | |-- javascripts 176 | | `-- stylesheets 177 | |-- script 178 | | `-- performance 179 | |-- test 180 | | |-- fixtures 181 | | |-- functional 182 | | |-- integration 183 | | |-- performance 184 | | `-- unit 185 | |-- tmp 186 | | |-- cache 187 | | |-- pids 188 | | |-- sessions 189 | | `-- sockets 190 | `-- vendor 191 | `-- plugins 192 | 193 | app 194 | Holds all the code that's specific to this particular application. 195 | 196 | app/controllers 197 | Holds controllers that should be named like weblogs_controller.rb for 198 | automated URL mapping. All controllers should descend from 199 | ApplicationController which itself descends from ActionController::Base. 200 | 201 | app/models 202 | Holds models that should be named like post.rb. Models descend from 203 | ActiveRecord::Base by default. 204 | 205 | app/views 206 | Holds the template files for the view that should be named like 207 | weblogs/index.html.erb for the WeblogsController#index action. All views use 208 | eRuby syntax by default. 209 | 210 | app/views/layouts 211 | Holds the template files for layouts to be used with views. This models the 212 | common header/footer method of wrapping views. In your views, define a layout 213 | using the layout :default and create a file named default.html.erb. 214 | Inside default.html.erb, call <% yield %> to render the view using this 215 | layout. 216 | 217 | app/helpers 218 | Holds view helpers that should be named like weblogs_helper.rb. These are 219 | generated for you automatically when using generators for controllers. 220 | Helpers can be used to wrap functionality for your views into methods. 221 | 222 | config 223 | Configuration files for the Rails environment, the routing map, the database, 224 | and other dependencies. 225 | 226 | db 227 | Contains the database schema in schema.rb. db/migrate contains all the 228 | sequence of Migrations for your schema. 229 | 230 | doc 231 | This directory is where your application documentation will be stored when 232 | generated using rake doc:app 233 | 234 | lib 235 | Application specific libraries. Basically, any kind of custom code that 236 | doesn't belong under controllers, models, or helpers. This directory is in 237 | the load path. 238 | 239 | public 240 | The directory available for the web server. Contains subdirectories for 241 | images, stylesheets, and javascripts. Also contains the dispatchers and the 242 | default HTML files. This should be set as the DOCUMENT_ROOT of your web 243 | server. 244 | 245 | script 246 | Helper scripts for automation and generation. 247 | 248 | test 249 | Unit and functional tests along with fixtures. When using the rails generate 250 | command, template test files will be generated for you and placed in this 251 | directory. 252 | 253 | vendor 254 | External libraries that the application depends on. Also includes the plugins 255 | subdirectory. If the app has frozen rails, those gems also go here, under 256 | vendor/rails/. This directory is in the load path. 257 | -------------------------------------------------------------------------------- /public/javascripts/michael_hintbuble.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Michael Hintbuble creates pretty hint bubbles using prototype and 3 | * scriptaculous. These functions work with ActionView helpers 4 | * to provide hint bubble components using the syntax defined 5 | * for rendering rails templates. 6 | * 7 | * 8 | * Brought to you by the good folks at Coroutine. Hire us! 9 | * http://coroutine.com 10 | */ 11 | var MichaelHintbuble = {} 12 | 13 | 14 | /** 15 | * This property governs whether or not Michael bothers creating and 16 | * managing a blocking iframe to accommodate ie6. 17 | * 18 | * Defaults to false, but override if you must. 19 | */ 20 | MichaelHintbuble.SUPPORT_IE6_BULLSHIT = false; 21 | 22 | 23 | 24 | //----------------------------------------------------------------------------- 25 | // Bubble class 26 | //----------------------------------------------------------------------------- 27 | 28 | /** 29 | * This function lets you come fly with Michael by defining 30 | * the hint bubble class. 31 | */ 32 | MichaelHintbuble.Bubble = function(target_id, content, options) { 33 | this._target = $(target_id); 34 | this._element = null; 35 | this._positioner = null; 36 | this._isShowing = null; 37 | 38 | this._class = options["class"] || ""; 39 | this._eventNames = options["eventNames"] || ["mouseover"] 40 | this._position = options["position"] || "right"; 41 | this._beforeShow = options["beforeShow"] || Prototype.emptyFunction 42 | this._afterShow = options["afterShow"] || Prototype.emptyFunction 43 | this._beforeHide = options["beforeHide"] || Prototype.emptyFunction 44 | this._afterHide = options["afterHide"] || Prototype.emptyFunction 45 | 46 | this._makeBubble(); 47 | this._makePositioner(); 48 | this._attachObservers(); 49 | this.setContent(content); 50 | this.setPosition(); 51 | 52 | if (MichaelHintbuble.SUPPORT_IE6_BULLSHIT) { 53 | this._makeFrame(); 54 | } 55 | }; 56 | 57 | 58 | /** 59 | * This hash maps the bubble id to the bubble object itself. It allows the Rails 60 | * code a way to specify the js object it wishes to invoke. 61 | */ 62 | MichaelHintbuble.Bubble.instances = {}; 63 | 64 | 65 | /** 66 | * This method destroys the bubble with the corresponding target id. 67 | * 68 | * @param {String} id The target id value of the bubble element (also the key 69 | * in the instances hash.) 70 | */ 71 | MichaelHintbuble.Bubble.destroy = function(id) { 72 | var bubble = this.instances[id]; 73 | if (bubble) { 74 | bubble.finalize(); 75 | } 76 | this.instances[id] = null; 77 | }; 78 | 79 | 80 | /** 81 | * This method hides the bubble with the corresponding target id. 82 | * 83 | * @param {String} id The target id value of the bubble element (also the key 84 | * in the instances hash.) 85 | * 86 | * @return {Object} an instance of MichaelHintbuble.Bubble 87 | * 88 | */ 89 | MichaelHintbuble.Bubble.hide = function(id) { 90 | var bubble = this.instances[id]; 91 | if (bubble) { 92 | bubble.hide(); 93 | } 94 | return bubble; 95 | }; 96 | 97 | 98 | /** 99 | * This method returns a boolean indiciating whether or not the 100 | * bubble with the corresponding target id is showing. 101 | * 102 | * @param {String} id The target id value of the bubble element (also the key 103 | * in the instances hash.) 104 | * 105 | * @return {Boolean} Whether or not the bubble with the corresponding 106 | * id is showing. 107 | * 108 | */ 109 | MichaelHintbuble.Bubble.isShowing = function(id) { 110 | var bubble = this.instances[id]; 111 | if (!bubble) { 112 | throw "No bubble cound be found for the supplied id."; 113 | } 114 | return bubble.isShowing(); 115 | }; 116 | 117 | 118 | /** 119 | * This method shows the bubble with the corresponding target id. 120 | * 121 | * @param {String} id The target id value of the bubble element (also the key 122 | * in the instances hash.) 123 | * 124 | * @return {Object} an instance of MichaelHintbuble.Bubble 125 | * 126 | */ 127 | MichaelHintbuble.Bubble.show = function(id) { 128 | var bubble = this.instances[id]; 129 | if (bubble) { 130 | bubble.show(); 131 | } 132 | return bubble; 133 | }; 134 | 135 | 136 | /** 137 | * This function establishes all of the observations specified in the options. 138 | */ 139 | MichaelHintbuble.Bubble.prototype._attachObservers = function() { 140 | if (this._eventNames.indexOf("focus") > -1) { 141 | this._target.observe("focus", function() { 142 | this.show(); 143 | }.bind(this)); 144 | this._target.observe("blur", function() { 145 | this.hide(); 146 | }.bind(this)); 147 | } 148 | if (this._eventNames.indexOf("mouseover") > -1) { 149 | this._target.observe("mouseover", function() { 150 | this.show(); 151 | }.bind(this)); 152 | this._target.observe("mouseout", function() { 153 | this.hide(); 154 | }.bind(this)); 155 | } 156 | Event.observe(window, "resize", function() { 157 | if (this.isShowing()) { 158 | this.setPosition(); 159 | } 160 | }.bind(this)); 161 | Event.observe(window, "scroll", function() { 162 | if (this.isShowing()) { 163 | this.setPosition(); 164 | } 165 | }.bind(this)); 166 | }; 167 | 168 | 169 | /** 170 | * This function creates the bubble element and hides it by default. 171 | */ 172 | MichaelHintbuble.Bubble.prototype._makeBubble = function() { 173 | if (!this._element) { 174 | this._container = new Element("DIV"); 175 | this._container.addClassName("container"); 176 | 177 | this._element = new Element("DIV"); 178 | this._element.addClassName("michael_hintbuble_bubble"); 179 | this._element.addClassName(this._class); 180 | this._element.update(this._container); 181 | this._element.hide(); 182 | document.body.insert(this._element); 183 | } 184 | }; 185 | 186 | 187 | /** 188 | * This function creates the blocking frame element and hides it by default. 189 | */ 190 | MichaelHintbuble.Bubble.prototype._makeFrame = function() { 191 | if (!this._frame) { 192 | this._frame = new Element("IFRAME"); 193 | this._frame.addClassName("michael_hintbuble_bubble_frame"); 194 | this._frame.addClassName(this._class + "_frame"); 195 | this._frame.setAttribute("src", "about:blank"); 196 | this._frame.hide(); 197 | document.body.insert(this._frame); 198 | } 199 | }; 200 | 201 | 202 | /** 203 | * This function creates the bubble positioner object. 204 | */ 205 | MichaelHintbuble.Bubble.prototype._makePositioner = function() { 206 | if (!this._positioner) { 207 | this._positioner = new MichaelHintbuble.BubblePositioner(this._target, this._element, this._position); 208 | } 209 | }; 210 | 211 | 212 | /** 213 | * This method updates the container element by applying an additional style 214 | * class representing the relative position of the bubble to the target. 215 | */ 216 | MichaelHintbuble.Bubble.prototype._updateContainerClass = function() { 217 | this._container.removeClassName(); 218 | this._container.addClassName("container"); 219 | this._container.addClassName(this._positioner.styleClassForPosition()); 220 | }; 221 | 222 | 223 | /** 224 | * This function allows the bubble object to be destroyed without 225 | * creating memory leaks. 226 | */ 227 | MichaelHintbuble.Bubble.prototype.finalize = function() { 228 | this._positioner.finalize(); 229 | this._container.remove(); 230 | this._element.remove(); 231 | if (this._frame) { 232 | this._frame.remove(); 233 | } 234 | 235 | this._target = null; 236 | this._element = null; 237 | this._container = null; 238 | this._positioner = null; 239 | this._frame = null; 240 | }; 241 | 242 | 243 | /** 244 | * This function shows the hint bubble container (and the blocking frame, if 245 | * required). 246 | */ 247 | MichaelHintbuble.Bubble.prototype.hide = function() { 248 | new Effect.Fade(this._element, { 249 | duration: 0.2, 250 | beforeStart: this._beforeHide, 251 | afterFinish: function() { 252 | this._isShowing = false; 253 | this._afterHide(); 254 | }.bind(this) 255 | }); 256 | 257 | if (this._frame) { 258 | new Effect.Fade(this._frame, { 259 | duration: 0.2 260 | }); 261 | } 262 | }; 263 | 264 | 265 | /** 266 | * This function returns a boolean indicating whether or not the bubble is 267 | * showing. 268 | * 269 | * @returns {Boolean} Whether or not the bubble is showing. 270 | */ 271 | MichaelHintbuble.Bubble.prototype.isShowing = function() { 272 | return this._isShowing; 273 | }; 274 | 275 | 276 | /** 277 | * This function sets the content of the hint bubble container. 278 | * 279 | * @param {String} content A string representation of the content to be added 280 | * to the hint bubble container. 281 | */ 282 | MichaelHintbuble.Bubble.prototype.setContent = function(content) { 283 | var content_container = new Element("DIV"); 284 | content_container.className = "content"; 285 | content_container.update(content); 286 | 287 | this._container.update(content_container); 288 | }; 289 | 290 | 291 | /** 292 | * This method sets the position of the hint bubble. It should be noted that the 293 | * position simply states a preferred location for the bubble within the viewport. 294 | * If the supplied position results in the bubble overrunning the viewport, 295 | * the bubble will be repositioned to the opposite side to avoid viewport 296 | * overrun. 297 | * 298 | * @param {String} position A string representation of the preferred position of 299 | * the bubble element. 300 | */ 301 | MichaelHintbuble.Bubble.prototype.setPosition = function(position) { 302 | if (position) { 303 | this._position = position.toLowerCase(); 304 | } 305 | this._positioner.setPosition(this._position); 306 | this._updateContainerClass(); 307 | }; 308 | 309 | 310 | /** 311 | * This function shows the hint bubble container (and the blocking frame, if 312 | * required). 313 | */ 314 | MichaelHintbuble.Bubble.prototype.show = function() { 315 | this.setPosition(); 316 | 317 | if (this._frame) { 318 | var layout = new Element.Layout(this._element); 319 | this._frame.style.top = this._element.style.top; 320 | this._frame.style.left = this._element.style.left; 321 | this._frame.style.width = layout.get("padding-box-width") + "px"; 322 | this._frame.style.height = layout.get("padding-box-height") + "px"; 323 | 324 | new Effect.Appear(this._frame, { 325 | duration: 0.2 326 | }); 327 | } 328 | 329 | new Effect.Appear(this._element, { 330 | duration: 0.2, 331 | beforeStart: this._beforeShow, 332 | afterFinish: function() { 333 | this._isShowing = true; 334 | this._afterShow(); 335 | }.bind(this) 336 | }); 337 | }; 338 | 339 | 340 | 341 | 342 | //----------------------------------------------------------------------------- 343 | // BubblePositioner class 344 | //----------------------------------------------------------------------------- 345 | 346 | /** 347 | * This class encapsulates the positioning logic for bubble classes. 348 | * 349 | * @param {Element} target the dom element to which the bubble is anchored. 350 | * @param {Element} element the bubble element itself. 351 | */ 352 | MichaelHintbuble.BubblePositioner = function(target, element, position) { 353 | this._target = target; 354 | this._element = element; 355 | this._position = position; 356 | this._axis = null 357 | }; 358 | 359 | 360 | /** 361 | * These properties establish numeric values for the x and y axes. 362 | */ 363 | MichaelHintbuble.BubblePositioner.X_AXIS = 1; 364 | MichaelHintbuble.BubblePositioner.Y_AXIS = 2; 365 | 366 | 367 | /** 368 | * This property maps position values to one or the other axis. 369 | */ 370 | MichaelHintbuble.BubblePositioner.AXIS_MAP = { 371 | left: MichaelHintbuble.BubblePositioner.X_AXIS, 372 | right: MichaelHintbuble.BubblePositioner.X_AXIS, 373 | top: MichaelHintbuble.BubblePositioner.Y_AXIS, 374 | bottom: MichaelHintbuble.BubblePositioner.Y_AXIS 375 | }; 376 | 377 | 378 | /** 379 | * This property maps position values to their opposite value. 380 | */ 381 | MichaelHintbuble.BubblePositioner.COMPLEMENTS = { 382 | left: "right", 383 | right: "left", 384 | top: "bottom", 385 | bottom: "top" 386 | }; 387 | 388 | 389 | /** 390 | * This hash is a convenience that allows us to write slightly denser code when 391 | * calculating the bubble's position. 392 | */ 393 | MichaelHintbuble.BubblePositioner.POSITION_FN_MAP = { 394 | left: "getWidth", 395 | top: "getHeight" 396 | }; 397 | 398 | 399 | 400 | /** 401 | * This function positions the element below the target. 402 | */ 403 | MichaelHintbuble.BubblePositioner.prototype._bottom = function() { 404 | var to = this._targetAdjustedOffset(); 405 | var tl = new Element.Layout(this._target); 406 | 407 | this._element.style.top = (to.top + tl.get("border-box-height")) + "px"; 408 | }; 409 | 410 | 411 | /** 412 | * This function centers the positioning of the element for whichever 413 | * axis it is on. 414 | */ 415 | MichaelHintbuble.BubblePositioner.prototype._center = function() { 416 | var to = this._targetAdjustedOffset(); 417 | var tl = new Element.Layout(this._target); 418 | var el = new Element.Layout(this._element); 419 | 420 | if (this._axis === MichaelHintbuble.BubblePositioner.X_AXIS) { 421 | this._element.style.top = (to.top + Math.ceil(tl.get("border-box-height")/2) - Math.ceil(el.get("padding-box-height")/2)) + "px"; 422 | } 423 | else if (this._axis === MichaelHintbuble.BubblePositioner.Y_AXIS) { 424 | this._element.style.left = (to.left + Math.ceil(tl.get("border-box-width")/2) - Math.ceil(el.get("padding-box-width")/2)) + "px"; 425 | } 426 | }; 427 | 428 | 429 | /** 430 | * This function returns a boolean indicating whether or not the element is 431 | * contained within the viewport. 432 | * 433 | * @returns {Boolean} whether or not the element is contained within the viewport. 434 | */ 435 | MichaelHintbuble.BubblePositioner.prototype._isElementWithinViewport = function() { 436 | var isWithinViewport = true; 437 | var fnMap = MichaelHintbuble.BubblePositioner.POSITION_FN_MAP; 438 | var method = null; 439 | var viewPortMinEdge = null; 440 | var viewPortMaxEdge = null; 441 | var elementMinEdge = null; 442 | var elementMaxEdge = null; 443 | 444 | for (var prop in fnMap) { 445 | method = fnMap[prop]; 446 | viewportMinEdge = document.viewport.getScrollOffsets()[prop]; 447 | viewportMaxEdge = viewportMinEdge + document.viewport[method](); 448 | elementMinEdge = parseInt(this._element.style[prop] || 0); 449 | elementMaxEdge = elementMinEdge + this._element[method](); 450 | 451 | if ((elementMaxEdge > viewportMaxEdge) || (elementMinEdge < viewportMinEdge)) { 452 | isWithinViewport = false; 453 | break; 454 | } 455 | } 456 | 457 | return isWithinViewport; 458 | }; 459 | 460 | 461 | /** 462 | * This function positions the element to the left of the target. 463 | */ 464 | MichaelHintbuble.BubblePositioner.prototype._left = function() { 465 | var to = this._targetAdjustedOffset(); 466 | var el = new Element.Layout(this._element); 467 | 468 | this._element.style.left = (to.left - el.get("padding-box-width")) + "px"; 469 | }; 470 | 471 | 472 | /** 473 | * This function positions the element to the right of the target. 474 | */ 475 | MichaelHintbuble.BubblePositioner.prototype._right = function() { 476 | var to = this._targetAdjustedOffset(); 477 | var tl = new Element.Layout(this._target); 478 | 479 | this._element.style.left = (to.left + tl.get("border-box-width")) + "px"; 480 | }; 481 | 482 | 483 | /** 484 | * This function positions the element relative to the target according to the 485 | * position value supplied. Because this function is private, it assumes a 486 | * safe position value. 487 | * 488 | * @param {String} position the desired relative position of the element to the 489 | * target. 490 | */ 491 | MichaelHintbuble.BubblePositioner.prototype._setPosition = function(position) { 492 | this._axis = MichaelHintbuble.BubblePositioner.AXIS_MAP[position]; 493 | this._position = position; 494 | this["_" + position](); 495 | this._center(); 496 | }; 497 | 498 | 499 | /** 500 | * This function returns a hash with the adjusted offset positions for the target 501 | * element. 502 | */ 503 | MichaelHintbuble.BubblePositioner.prototype._targetAdjustedOffset = function() { 504 | var bs = $$("body").first().cumulativeScrollOffset(); 505 | var to = this._target.cumulativeOffset(); 506 | var ts = this._target.cumulativeScrollOffset(); 507 | 508 | return { 509 | "top": to.top - ts.top + bs.top, 510 | "left": to.left - ts.left + bs.left 511 | } 512 | }; 513 | 514 | 515 | /** 516 | * This function positions the element above the target. 517 | */ 518 | MichaelHintbuble.BubblePositioner.prototype._top = function() { 519 | var to = this._targetAdjustedOffset(); 520 | var el = new Element.Layout(this._element); 521 | 522 | this._element.style.top = (to.top - el.get("padding-box-height")) + "px"; 523 | }; 524 | 525 | 526 | /** 527 | * This function allows the bubble positioner object to be destroyed without 528 | * creating memory leaks. 529 | */ 530 | MichaelHintbuble.BubblePositioner.prototype.finalize = function() { 531 | this._target = null; 532 | this._element = null; 533 | this._axis = null; 534 | this._position = null; 535 | }; 536 | 537 | 538 | /** 539 | * This function positions the element relative to the target according to the 540 | * position value supplied. Invalid position values are ignored. If the new 541 | * position runs off the viewport, the complement is tried. If that fails too, 542 | * it gives up and does what was asked. 543 | * 544 | * @param {String} position the desired relative position of the element to the 545 | * target. 546 | */ 547 | MichaelHintbuble.BubblePositioner.prototype.setPosition = function(position) { 548 | var axis = MichaelHintbuble.BubblePositioner.AXIS_MAP[position]; 549 | if (axis) { 550 | this._setPosition(position); 551 | if (!this._isElementWithinViewport()) { 552 | this._setPosition(MichaelHintbuble.BubblePositioner.COMPLEMENTS[position]); 553 | if (!this._isElementWithinViewport()) { 554 | this._setPosition(position); 555 | } 556 | } 557 | } 558 | }; 559 | 560 | 561 | /** 562 | * This function returns a string representation of the current logical positioning that 563 | * can be used as a stylesheet class for physical positioning. 564 | * 565 | * @returns {String} a styleclass name appropriate for the current position. 566 | */ 567 | MichaelHintbuble.BubblePositioner.prototype.styleClassForPosition = function() { 568 | return this._position.toLowerCase(); 569 | }; -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/generators/michael_hintbuble/templates/michael_hintbuble.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Michael Hintbuble creates pretty hint bubbles using prototype and 3 | * scriptaculous. These functions work with ActionView helpers 4 | * to provide hint bubble components using the syntax defined 5 | * for rendering rails templates. 6 | * 7 | * 8 | * Brought to you by the good folks at Coroutine. Hire us! 9 | * http://coroutine.com 10 | */ 11 | var MichaelHintbuble = {} 12 | 13 | 14 | /** 15 | * This property governs whether or not Michael bothers creating and 16 | * managing a blocking iframe to accommodate ie6. 17 | * 18 | * Defaults to false, but override if you must. 19 | */ 20 | MichaelHintbuble.SUPPORT_IE6_BULLSHIT = false; 21 | 22 | 23 | 24 | //----------------------------------------------------------------------------- 25 | // Bubble class 26 | //----------------------------------------------------------------------------- 27 | 28 | /** 29 | * This function lets you come fly with Michael by defining 30 | * the hint bubble class. 31 | */ 32 | MichaelHintbuble.Bubble = function(target_id, content, options) { 33 | this._target = $(target_id); 34 | this._element = null; 35 | this._positioner = null; 36 | this._isShowing = null; 37 | 38 | this._class = options["class"] || ""; 39 | this._eventNames = options["eventNames"] || ["mouseover"] 40 | this._position = options["position"] || "right"; 41 | this._beforeShow = options["beforeShow"] || Prototype.emptyFunction 42 | this._afterShow = options["afterShow"] || Prototype.emptyFunction 43 | this._beforeHide = options["beforeHide"] || Prototype.emptyFunction 44 | this._afterHide = options["afterHide"] || Prototype.emptyFunction 45 | 46 | this._makeBubble(); 47 | this._makePositioner(); 48 | this._attachObservers(); 49 | this.setContent(content); 50 | this.setPosition(); 51 | 52 | if (MichaelHintbuble.SUPPORT_IE6_BULLSHIT) { 53 | this._makeFrame(); 54 | } 55 | }; 56 | 57 | 58 | /** 59 | * This hash maps the bubble id to the bubble object itself. It allows the Rails 60 | * code a way to specify the js object it wishes to invoke. 61 | */ 62 | MichaelHintbuble.Bubble.instances = {}; 63 | 64 | 65 | /** 66 | * This method destroys the bubble with the corresponding target id. 67 | * 68 | * @param {String} id The target id value of the bubble element (also the key 69 | * in the instances hash.) 70 | */ 71 | MichaelHintbuble.Bubble.destroy = function(id) { 72 | var bubble = this.instances[id]; 73 | if (bubble) { 74 | bubble.finalize(); 75 | } 76 | this.instances[id] = null; 77 | }; 78 | 79 | 80 | /** 81 | * This method hides the bubble with the corresponding target id. 82 | * 83 | * @param {String} id The target id value of the bubble element (also the key 84 | * in the instances hash.) 85 | * 86 | * @return {Object} an instance of MichaelHintbuble.Bubble 87 | * 88 | */ 89 | MichaelHintbuble.Bubble.hide = function(id) { 90 | var bubble = this.instances[id]; 91 | if (bubble) { 92 | bubble.hide(); 93 | } 94 | return bubble; 95 | }; 96 | 97 | 98 | /** 99 | * This method returns a boolean indiciating whether or not the 100 | * bubble with the corresponding target id is showing. 101 | * 102 | * @param {String} id The target id value of the bubble element (also the key 103 | * in the instances hash.) 104 | * 105 | * @return {Boolean} Whether or not the bubble with the corresponding 106 | * id is showing. 107 | * 108 | */ 109 | MichaelHintbuble.Bubble.isShowing = function(id) { 110 | var bubble = this.instances[id]; 111 | if (!bubble) { 112 | throw "No bubble cound be found for the supplied id."; 113 | } 114 | return bubble.isShowing(); 115 | }; 116 | 117 | 118 | /** 119 | * This method shows the bubble with the corresponding target id. 120 | * 121 | * @param {String} id The target id value of the bubble element (also the key 122 | * in the instances hash.) 123 | * 124 | * @return {Object} an instance of MichaelHintbuble.Bubble 125 | * 126 | */ 127 | MichaelHintbuble.Bubble.show = function(id) { 128 | var bubble = this.instances[id]; 129 | if (bubble) { 130 | bubble.show(); 131 | } 132 | return bubble; 133 | }; 134 | 135 | 136 | /** 137 | * This function establishes all of the observations specified in the options. 138 | */ 139 | MichaelHintbuble.Bubble.prototype._attachObservers = function() { 140 | if (this._eventNames.indexOf("focus") > -1) { 141 | this._target.observe("focus", function() { 142 | this.show(); 143 | }.bind(this)); 144 | this._target.observe("blur", function() { 145 | this.hide(); 146 | }.bind(this)); 147 | } 148 | if (this._eventNames.indexOf("mouseover") > -1) { 149 | this._target.observe("mouseover", function() { 150 | this.show(); 151 | }.bind(this)); 152 | this._target.observe("mouseout", function() { 153 | this.hide(); 154 | }.bind(this)); 155 | } 156 | Event.observe(window, "resize", function() { 157 | if (this.isShowing()) { 158 | this.setPosition(); 159 | } 160 | }.bind(this)); 161 | Event.observe(window, "scroll", function() { 162 | if (this.isShowing()) { 163 | this.setPosition(); 164 | } 165 | }.bind(this)); 166 | }; 167 | 168 | 169 | /** 170 | * This function creates the bubble element and hides it by default. 171 | */ 172 | MichaelHintbuble.Bubble.prototype._makeBubble = function() { 173 | if (!this._element) { 174 | this._container = new Element("DIV"); 175 | this._container.addClassName("container"); 176 | 177 | this._element = new Element("DIV"); 178 | this._element.addClassName("michael_hintbuble_bubble"); 179 | this._element.addClassName(this._class); 180 | this._element.update(this._container); 181 | this._element.hide(); 182 | document.body.insert(this._element); 183 | } 184 | }; 185 | 186 | 187 | /** 188 | * This function creates the blocking frame element and hides it by default. 189 | */ 190 | MichaelHintbuble.Bubble.prototype._makeFrame = function() { 191 | if (!this._frame) { 192 | this._frame = new Element("IFRAME"); 193 | this._frame.addClassName("michael_hintbuble_bubble_frame"); 194 | this._frame.addClassName(this._class + "_frame"); 195 | this._frame.setAttribute("src", "about:blank"); 196 | this._frame.hide(); 197 | document.body.insert(this._frame); 198 | } 199 | }; 200 | 201 | 202 | /** 203 | * This function creates the bubble positioner object. 204 | */ 205 | MichaelHintbuble.Bubble.prototype._makePositioner = function() { 206 | if (!this._positioner) { 207 | this._positioner = new MichaelHintbuble.BubblePositioner(this._target, this._element, this._position); 208 | } 209 | }; 210 | 211 | 212 | /** 213 | * This method updates the container element by applying an additional style 214 | * class representing the relative position of the bubble to the target. 215 | */ 216 | MichaelHintbuble.Bubble.prototype._updateContainerClass = function() { 217 | this._container.removeClassName(); 218 | this._container.addClassName("container"); 219 | this._container.addClassName(this._positioner.styleClassForPosition()); 220 | }; 221 | 222 | 223 | /** 224 | * This function allows the bubble object to be destroyed without 225 | * creating memory leaks. 226 | */ 227 | MichaelHintbuble.Bubble.prototype.finalize = function() { 228 | this._positioner.finalize(); 229 | this._container.remove(); 230 | this._element.remove(); 231 | if (this._frame) { 232 | this._frame.remove(); 233 | } 234 | 235 | this._target = null; 236 | this._element = null; 237 | this._container = null; 238 | this._positioner = null; 239 | this._frame = null; 240 | }; 241 | 242 | 243 | /** 244 | * This function shows the hint bubble container (and the blocking frame, if 245 | * required). 246 | */ 247 | MichaelHintbuble.Bubble.prototype.hide = function() { 248 | new Effect.Fade(this._element, { 249 | duration: 0.2, 250 | beforeStart: this._beforeHide, 251 | afterFinish: function() { 252 | this._isShowing = false; 253 | this._afterHide(); 254 | }.bind(this) 255 | }); 256 | 257 | if (this._frame) { 258 | new Effect.Fade(this._frame, { 259 | duration: 0.2 260 | }); 261 | } 262 | }; 263 | 264 | 265 | /** 266 | * This function returns a boolean indicating whether or not the bubble is 267 | * showing. 268 | * 269 | * @returns {Boolean} Whether or not the bubble is showing. 270 | */ 271 | MichaelHintbuble.Bubble.prototype.isShowing = function() { 272 | return this._isShowing; 273 | }; 274 | 275 | 276 | /** 277 | * This function sets the content of the hint bubble container. 278 | * 279 | * @param {String} content A string representation of the content to be added 280 | * to the hint bubble container. 281 | */ 282 | MichaelHintbuble.Bubble.prototype.setContent = function(content) { 283 | var content_container = new Element("DIV"); 284 | content_container.className = "content"; 285 | content_container.update(content); 286 | 287 | this._container.update(content_container); 288 | }; 289 | 290 | 291 | /** 292 | * This method sets the position of the hint bubble. It should be noted that the 293 | * position simply states a preferred location for the bubble within the viewport. 294 | * If the supplied position results in the bubble overrunning the viewport, 295 | * the bubble will be repositioned to the opposite side to avoid viewport 296 | * overrun. 297 | * 298 | * @param {String} position A string representation of the preferred position of 299 | * the bubble element. 300 | */ 301 | MichaelHintbuble.Bubble.prototype.setPosition = function(position) { 302 | if (position) { 303 | this._position = position.toLowerCase(); 304 | } 305 | this._positioner.setPosition(this._position); 306 | this._updateContainerClass(); 307 | }; 308 | 309 | 310 | /** 311 | * This function shows the hint bubble container (and the blocking frame, if 312 | * required). 313 | */ 314 | MichaelHintbuble.Bubble.prototype.show = function() { 315 | this.setPosition(); 316 | 317 | if (this._frame) { 318 | var layout = new Element.Layout(this._element); 319 | this._frame.style.top = this._element.style.top; 320 | this._frame.style.left = this._element.style.left; 321 | this._frame.style.width = layout.get("padding-box-width") + "px"; 322 | this._frame.style.height = layout.get("padding-box-height") + "px"; 323 | 324 | new Effect.Appear(this._frame, { 325 | duration: 0.2 326 | }); 327 | } 328 | 329 | new Effect.Appear(this._element, { 330 | duration: 0.2, 331 | beforeStart: this._beforeShow, 332 | afterFinish: function() { 333 | this._isShowing = true; 334 | this._afterShow(); 335 | }.bind(this) 336 | }); 337 | }; 338 | 339 | 340 | 341 | 342 | //----------------------------------------------------------------------------- 343 | // BubblePositioner class 344 | //----------------------------------------------------------------------------- 345 | 346 | /** 347 | * This class encapsulates the positioning logic for bubble classes. 348 | * 349 | * @param {Element} target the dom element to which the bubble is anchored. 350 | * @param {Element} element the bubble element itself. 351 | */ 352 | MichaelHintbuble.BubblePositioner = function(target, element, position) { 353 | this._target = target; 354 | this._element = element; 355 | this._position = position; 356 | this._axis = null 357 | }; 358 | 359 | 360 | /** 361 | * These properties establish numeric values for the x and y axes. 362 | */ 363 | MichaelHintbuble.BubblePositioner.X_AXIS = 1; 364 | MichaelHintbuble.BubblePositioner.Y_AXIS = 2; 365 | 366 | 367 | /** 368 | * This property maps position values to one or the other axis. 369 | */ 370 | MichaelHintbuble.BubblePositioner.AXIS_MAP = { 371 | left: MichaelHintbuble.BubblePositioner.X_AXIS, 372 | right: MichaelHintbuble.BubblePositioner.X_AXIS, 373 | top: MichaelHintbuble.BubblePositioner.Y_AXIS, 374 | bottom: MichaelHintbuble.BubblePositioner.Y_AXIS 375 | }; 376 | 377 | 378 | /** 379 | * This property maps position values to their opposite value. 380 | */ 381 | MichaelHintbuble.BubblePositioner.COMPLEMENTS = { 382 | left: "right", 383 | right: "left", 384 | top: "bottom", 385 | bottom: "top" 386 | }; 387 | 388 | 389 | /** 390 | * This hash is a convenience that allows us to write slightly denser code when 391 | * calculating the bubble's position. 392 | */ 393 | MichaelHintbuble.BubblePositioner.POSITION_FN_MAP = { 394 | left: "getWidth", 395 | top: "getHeight" 396 | }; 397 | 398 | 399 | 400 | /** 401 | * This function positions the element below the target. 402 | */ 403 | MichaelHintbuble.BubblePositioner.prototype._bottom = function() { 404 | var to = this._targetAdjustedOffset(); 405 | var tl = new Element.Layout(this._target); 406 | 407 | this._element.style.top = (to.top + tl.get("border-box-height")) + "px"; 408 | }; 409 | 410 | 411 | /** 412 | * This function centers the positioning of the element for whichever 413 | * axis it is on. 414 | */ 415 | MichaelHintbuble.BubblePositioner.prototype._center = function() { 416 | var to = this._targetAdjustedOffset(); 417 | var tl = new Element.Layout(this._target); 418 | var el = new Element.Layout(this._element); 419 | 420 | if (this._axis === MichaelHintbuble.BubblePositioner.X_AXIS) { 421 | this._element.style.top = (to.top + Math.ceil(tl.get("border-box-height")/2) - Math.ceil(el.get("padding-box-height")/2)) + "px"; 422 | } 423 | else if (this._axis === MichaelHintbuble.BubblePositioner.Y_AXIS) { 424 | this._element.style.left = (to.left + Math.ceil(tl.get("border-box-width")/2) - Math.ceil(el.get("padding-box-width")/2)) + "px"; 425 | } 426 | }; 427 | 428 | 429 | /** 430 | * This function returns a boolean indicating whether or not the element is 431 | * contained within the viewport. 432 | * 433 | * @returns {Boolean} whether or not the element is contained within the viewport. 434 | */ 435 | MichaelHintbuble.BubblePositioner.prototype._isElementWithinViewport = function() { 436 | var isWithinViewport = true; 437 | var fnMap = MichaelHintbuble.BubblePositioner.POSITION_FN_MAP; 438 | var method = null; 439 | var viewPortMinEdge = null; 440 | var viewPortMaxEdge = null; 441 | var elementMinEdge = null; 442 | var elementMaxEdge = null; 443 | 444 | for (var prop in fnMap) { 445 | method = fnMap[prop]; 446 | viewportMinEdge = document.viewport.getScrollOffsets()[prop]; 447 | viewportMaxEdge = viewportMinEdge + document.viewport[method](); 448 | elementMinEdge = parseInt(this._element.style[prop] || 0); 449 | elementMaxEdge = elementMinEdge + this._element[method](); 450 | 451 | if ((elementMaxEdge > viewportMaxEdge) || (elementMinEdge < viewportMinEdge)) { 452 | isWithinViewport = false; 453 | break; 454 | } 455 | } 456 | 457 | return isWithinViewport; 458 | }; 459 | 460 | 461 | /** 462 | * This function positions the element to the left of the target. 463 | */ 464 | MichaelHintbuble.BubblePositioner.prototype._left = function() { 465 | var to = this._targetAdjustedOffset(); 466 | var el = new Element.Layout(this._element); 467 | 468 | this._element.style.left = (to.left - el.get("padding-box-width")) + "px"; 469 | }; 470 | 471 | 472 | /** 473 | * This function positions the element to the right of the target. 474 | */ 475 | MichaelHintbuble.BubblePositioner.prototype._right = function() { 476 | var to = this._targetAdjustedOffset(); 477 | var tl = new Element.Layout(this._target); 478 | 479 | this._element.style.left = (to.left + tl.get("border-box-width")) + "px"; 480 | }; 481 | 482 | 483 | /** 484 | * This function positions the element relative to the target according to the 485 | * position value supplied. Because this function is private, it assumes a 486 | * safe position value. 487 | * 488 | * @param {String} position the desired relative position of the element to the 489 | * target. 490 | */ 491 | MichaelHintbuble.BubblePositioner.prototype._setPosition = function(position) { 492 | this._axis = MichaelHintbuble.BubblePositioner.AXIS_MAP[position]; 493 | this._position = position; 494 | this["_" + position](); 495 | this._center(); 496 | }; 497 | 498 | 499 | /** 500 | * This function returns a hash with the adjusted offset positions for the target 501 | * element. 502 | */ 503 | MichaelHintbuble.BubblePositioner.prototype._targetAdjustedOffset = function() { 504 | var bs = $$("body").first().cumulativeScrollOffset(); 505 | var to = this._target.cumulativeOffset(); 506 | var ts = this._target.cumulativeScrollOffset(); 507 | 508 | return { 509 | "top": to.top - ts.top + bs.top, 510 | "left": to.left - ts.left + bs.left 511 | } 512 | }; 513 | 514 | 515 | /** 516 | * This function positions the element above the target. 517 | */ 518 | MichaelHintbuble.BubblePositioner.prototype._top = function() { 519 | var to = this._targetAdjustedOffset(); 520 | var el = new Element.Layout(this._element); 521 | 522 | this._element.style.top = (to.top - el.get("padding-box-height")) + "px"; 523 | }; 524 | 525 | 526 | /** 527 | * This function allows the bubble positioner object to be destroyed without 528 | * creating memory leaks. 529 | */ 530 | MichaelHintbuble.BubblePositioner.prototype.finalize = function() { 531 | this._target = null; 532 | this._element = null; 533 | this._axis = null; 534 | this._position = null; 535 | }; 536 | 537 | 538 | /** 539 | * This function positions the element relative to the target according to the 540 | * position value supplied. Invalid position values are ignored. If the new 541 | * position runs off the viewport, the complement is tried. If that fails too, 542 | * it gives up and does what was asked. 543 | * 544 | * @param {String} position the desired relative position of the element to the 545 | * target. 546 | */ 547 | MichaelHintbuble.BubblePositioner.prototype.setPosition = function(position) { 548 | var axis = MichaelHintbuble.BubblePositioner.AXIS_MAP[position]; 549 | if (axis) { 550 | this._setPosition(position); 551 | if (!this._isElementWithinViewport()) { 552 | this._setPosition(MichaelHintbuble.BubblePositioner.COMPLEMENTS[position]); 553 | if (!this._isElementWithinViewport()) { 554 | this._setPosition(position); 555 | } 556 | } 557 | } 558 | }; 559 | 560 | 561 | /** 562 | * This function returns a string representation of the current logical positioning that 563 | * can be used as a stylesheet class for physical positioning. 564 | * 565 | * @returns {String} a styleclass name appropriate for the current position. 566 | */ 567 | MichaelHintbuble.BubblePositioner.prototype.styleClassForPosition = function() { 568 | return this._position.toLowerCase(); 569 | }; -------------------------------------------------------------------------------- /vendor/gems/michael_hintbuble-1.0.5/lib/generators/michael_hintbuble/templates/michael_hintbuble.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Michael Hintbuble creates pretty hint bubbles using prototype and 3 | * scriptaculous. These functions work with ActionView helpers 4 | * to provide hint bubble components using the syntax defined 5 | * for rendering rails templates. 6 | * 7 | * 8 | * Brought to you by the good folks at Coroutine. Hire us! 9 | * http://coroutine.com 10 | */ 11 | var MichaelHintbuble = {} 12 | 13 | 14 | /** 15 | * This property governs whether or not Michael bothers creating and 16 | * managing a blocking iframe to accommodate ie6. 17 | * 18 | * Defaults to false, but override if you must. 19 | */ 20 | MichaelHintbuble.SUPPORT_IE6_BULLSHIT = false; 21 | 22 | 23 | 24 | //----------------------------------------------------------------------------- 25 | // Bubble class 26 | //----------------------------------------------------------------------------- 27 | 28 | /** 29 | * This function lets you come fly with Michael by defining 30 | * the hint bubble class. 31 | */ 32 | MichaelHintbuble.Bubble = function(target_id, content, options) { 33 | this._target = $(target_id); 34 | this._element = null; 35 | this._positioner = null; 36 | this._isShowing = null; 37 | 38 | this._class = options["class"] || ""; 39 | this._eventNames = options["eventNames"] || ["mouseover"] 40 | this._position = options["position"] || "right"; 41 | this._beforeShow = options["beforeShow"] || Prototype.emptyFunction 42 | this._afterShow = options["afterShow"] || Prototype.emptyFunction 43 | this._beforeHide = options["beforeHide"] || Prototype.emptyFunction 44 | this._afterHide = options["afterHide"] || Prototype.emptyFunction 45 | 46 | this._makeBubble(); 47 | this._makePositioner(); 48 | this._attachObservers(); 49 | this.setContent(content); 50 | this.setPosition(); 51 | 52 | if (MichaelHintbuble.SUPPORT_IE6_BULLSHIT) { 53 | this._makeFrame(); 54 | } 55 | }; 56 | 57 | 58 | /** 59 | * This hash maps the bubble id to the bubble object itself. It allows the Rails 60 | * code a way to specify the js object it wishes to invoke. 61 | */ 62 | MichaelHintbuble.Bubble.instances = {}; 63 | 64 | 65 | /** 66 | * This method destroys the bubble with the corresponding target id. 67 | * 68 | * @param {String} id The target id value of the bubble element (also the key 69 | * in the instances hash.) 70 | */ 71 | MichaelHintbuble.Bubble.destroy = function(id) { 72 | var bubble = this.instances[id]; 73 | if (bubble) { 74 | bubble.finalize(); 75 | } 76 | this.instances[id] = null; 77 | }; 78 | 79 | 80 | /** 81 | * This method hides the bubble with the corresponding target id. 82 | * 83 | * @param {String} id The target id value of the bubble element (also the key 84 | * in the instances hash.) 85 | * 86 | * @return {Object} an instance of MichaelHintbuble.Bubble 87 | * 88 | */ 89 | MichaelHintbuble.Bubble.hide = function(id) { 90 | var bubble = this.instances[id]; 91 | if (bubble) { 92 | bubble.hide(); 93 | } 94 | return bubble; 95 | }; 96 | 97 | 98 | /** 99 | * This method returns a boolean indiciating whether or not the 100 | * bubble with the corresponding target id is showing. 101 | * 102 | * @param {String} id The target id value of the bubble element (also the key 103 | * in the instances hash.) 104 | * 105 | * @return {Boolean} Whether or not the bubble with the corresponding 106 | * id is showing. 107 | * 108 | */ 109 | MichaelHintbuble.Bubble.isShowing = function(id) { 110 | var bubble = this.instances[id]; 111 | if (!bubble) { 112 | throw "No bubble cound be found for the supplied id."; 113 | } 114 | return bubble.isShowing(); 115 | }; 116 | 117 | 118 | /** 119 | * This method shows the bubble with the corresponding target id. 120 | * 121 | * @param {String} id The target id value of the bubble element (also the key 122 | * in the instances hash.) 123 | * 124 | * @return {Object} an instance of MichaelHintbuble.Bubble 125 | * 126 | */ 127 | MichaelHintbuble.Bubble.show = function(id) { 128 | var bubble = this.instances[id]; 129 | if (bubble) { 130 | bubble.show(); 131 | } 132 | return bubble; 133 | }; 134 | 135 | 136 | /** 137 | * This function establishes all of the observations specified in the options. 138 | */ 139 | MichaelHintbuble.Bubble.prototype._attachObservers = function() { 140 | if (this._eventNames.indexOf("focus") > -1) { 141 | this._target.observe("focus", function() { 142 | this.show(); 143 | }.bind(this)); 144 | this._target.observe("blur", function() { 145 | this.hide(); 146 | }.bind(this)); 147 | } 148 | if (this._eventNames.indexOf("mouseover") > -1) { 149 | this._target.observe("mouseover", function() { 150 | this.show(); 151 | }.bind(this)); 152 | this._target.observe("mouseout", function() { 153 | this.hide(); 154 | }.bind(this)); 155 | } 156 | Event.observe(window, "resize", function() { 157 | if (this.isShowing()) { 158 | this.setPosition(); 159 | } 160 | }.bind(this)); 161 | Event.observe(window, "scroll", function() { 162 | if (this.isShowing()) { 163 | this.setPosition(); 164 | } 165 | }.bind(this)); 166 | }; 167 | 168 | 169 | /** 170 | * This function creates the bubble element and hides it by default. 171 | */ 172 | MichaelHintbuble.Bubble.prototype._makeBubble = function() { 173 | if (!this._element) { 174 | this._container = new Element("DIV"); 175 | this._container.addClassName("container"); 176 | 177 | this._element = new Element("DIV"); 178 | this._element.addClassName("michael_hintbuble_bubble"); 179 | this._element.addClassName(this._class); 180 | this._element.update(this._container); 181 | this._element.hide(); 182 | document.body.insert(this._element); 183 | } 184 | }; 185 | 186 | 187 | /** 188 | * This function creates the blocking frame element and hides it by default. 189 | */ 190 | MichaelHintbuble.Bubble.prototype._makeFrame = function() { 191 | if (!this._frame) { 192 | this._frame = new Element("IFRAME"); 193 | this._frame.addClassName("michael_hintbuble_bubble_frame"); 194 | this._frame.addClassName(this._class + "_frame"); 195 | this._frame.setAttribute("src", "about:blank"); 196 | this._frame.hide(); 197 | document.body.insert(this._frame); 198 | } 199 | }; 200 | 201 | 202 | /** 203 | * This function creates the bubble positioner object. 204 | */ 205 | MichaelHintbuble.Bubble.prototype._makePositioner = function() { 206 | if (!this._positioner) { 207 | this._positioner = new MichaelHintbuble.BubblePositioner(this._target, this._element, this._position); 208 | } 209 | }; 210 | 211 | 212 | /** 213 | * This method updates the container element by applying an additional style 214 | * class representing the relative position of the bubble to the target. 215 | */ 216 | MichaelHintbuble.Bubble.prototype._updateContainerClass = function() { 217 | this._container.removeClassName(); 218 | this._container.addClassName("container"); 219 | this._container.addClassName(this._positioner.styleClassForPosition()); 220 | }; 221 | 222 | 223 | /** 224 | * This function allows the bubble object to be destroyed without 225 | * creating memory leaks. 226 | */ 227 | MichaelHintbuble.Bubble.prototype.finalize = function() { 228 | this._positioner.finalize(); 229 | this._container.remove(); 230 | this._element.remove(); 231 | if (this._frame) { 232 | this._frame.remove(); 233 | } 234 | 235 | this._target = null; 236 | this._element = null; 237 | this._container = null; 238 | this._positioner = null; 239 | this._frame = null; 240 | }; 241 | 242 | 243 | /** 244 | * This function shows the hint bubble container (and the blocking frame, if 245 | * required). 246 | */ 247 | MichaelHintbuble.Bubble.prototype.hide = function() { 248 | new Effect.Fade(this._element, { 249 | duration: 0.2, 250 | beforeStart: this._beforeHide, 251 | afterFinish: function() { 252 | this._isShowing = false; 253 | this._afterHide(); 254 | }.bind(this) 255 | }); 256 | 257 | if (this._frame) { 258 | new Effect.Fade(this._frame, { 259 | duration: 0.2 260 | }); 261 | } 262 | }; 263 | 264 | 265 | /** 266 | * This function returns a boolean indicating whether or not the bubble is 267 | * showing. 268 | * 269 | * @returns {Boolean} Whether or not the bubble is showing. 270 | */ 271 | MichaelHintbuble.Bubble.prototype.isShowing = function() { 272 | return this._isShowing; 273 | }; 274 | 275 | 276 | /** 277 | * This function sets the content of the hint bubble container. 278 | * 279 | * @param {String} content A string representation of the content to be added 280 | * to the hint bubble container. 281 | */ 282 | MichaelHintbuble.Bubble.prototype.setContent = function(content) { 283 | var content_container = new Element("DIV"); 284 | content_container.className = "content"; 285 | content_container.update(content); 286 | 287 | this._container.update(content_container); 288 | }; 289 | 290 | 291 | /** 292 | * This method sets the position of the hint bubble. It should be noted that the 293 | * position simply states a preferred location for the bubble within the viewport. 294 | * If the supplied position results in the bubble overrunning the viewport, 295 | * the bubble will be repositioned to the opposite side to avoid viewport 296 | * overrun. 297 | * 298 | * @param {String} position A string representation of the preferred position of 299 | * the bubble element. 300 | */ 301 | MichaelHintbuble.Bubble.prototype.setPosition = function(position) { 302 | if (position) { 303 | this._position = position.toLowerCase(); 304 | } 305 | this._positioner.setPosition(this._position); 306 | this._updateContainerClass(); 307 | }; 308 | 309 | 310 | /** 311 | * This function shows the hint bubble container (and the blocking frame, if 312 | * required). 313 | */ 314 | MichaelHintbuble.Bubble.prototype.show = function() { 315 | this.setPosition(); 316 | 317 | if (this._frame) { 318 | var layout = new Element.Layout(this._element); 319 | this._frame.style.top = this._element.style.top; 320 | this._frame.style.left = this._element.style.left; 321 | this._frame.style.width = layout.get("padding-box-width") + "px"; 322 | this._frame.style.height = layout.get("padding-box-height") + "px"; 323 | 324 | new Effect.Appear(this._frame, { 325 | duration: 0.2 326 | }); 327 | } 328 | 329 | new Effect.Appear(this._element, { 330 | duration: 0.2, 331 | beforeStart: this._beforeShow, 332 | afterFinish: function() { 333 | this._isShowing = true; 334 | this._afterShow(); 335 | }.bind(this) 336 | }); 337 | }; 338 | 339 | 340 | 341 | 342 | //----------------------------------------------------------------------------- 343 | // BubblePositioner class 344 | //----------------------------------------------------------------------------- 345 | 346 | /** 347 | * This class encapsulates the positioning logic for bubble classes. 348 | * 349 | * @param {Element} target the dom element to which the bubble is anchored. 350 | * @param {Element} element the bubble element itself. 351 | */ 352 | MichaelHintbuble.BubblePositioner = function(target, element, position) { 353 | this._target = target; 354 | this._element = element; 355 | this._position = position; 356 | this._axis = null 357 | }; 358 | 359 | 360 | /** 361 | * These properties establish numeric values for the x and y axes. 362 | */ 363 | MichaelHintbuble.BubblePositioner.X_AXIS = 1; 364 | MichaelHintbuble.BubblePositioner.Y_AXIS = 2; 365 | 366 | 367 | /** 368 | * This property maps position values to one or the other axis. 369 | */ 370 | MichaelHintbuble.BubblePositioner.AXIS_MAP = { 371 | left: MichaelHintbuble.BubblePositioner.X_AXIS, 372 | right: MichaelHintbuble.BubblePositioner.X_AXIS, 373 | top: MichaelHintbuble.BubblePositioner.Y_AXIS, 374 | bottom: MichaelHintbuble.BubblePositioner.Y_AXIS 375 | }; 376 | 377 | 378 | /** 379 | * This property maps position values to their opposite value. 380 | */ 381 | MichaelHintbuble.BubblePositioner.COMPLEMENTS = { 382 | left: "right", 383 | right: "left", 384 | top: "bottom", 385 | bottom: "top" 386 | }; 387 | 388 | 389 | /** 390 | * This hash is a convenience that allows us to write slightly denser code when 391 | * calculating the bubble's position. 392 | */ 393 | MichaelHintbuble.BubblePositioner.POSITION_FN_MAP = { 394 | left: "getWidth", 395 | top: "getHeight" 396 | }; 397 | 398 | 399 | 400 | /** 401 | * This function positions the element below the target. 402 | */ 403 | MichaelHintbuble.BubblePositioner.prototype._bottom = function() { 404 | var to = this._targetAdjustedOffset(); 405 | var tl = new Element.Layout(this._target); 406 | 407 | this._element.style.top = (to.top + tl.get("border-box-height")) + "px"; 408 | }; 409 | 410 | 411 | /** 412 | * This function centers the positioning of the element for whichever 413 | * axis it is on. 414 | */ 415 | MichaelHintbuble.BubblePositioner.prototype._center = function() { 416 | var to = this._targetAdjustedOffset(); 417 | var tl = new Element.Layout(this._target); 418 | var el = new Element.Layout(this._element); 419 | 420 | if (this._axis === MichaelHintbuble.BubblePositioner.X_AXIS) { 421 | this._element.style.top = (to.top + Math.ceil(tl.get("border-box-height")/2) - Math.ceil(el.get("padding-box-height")/2)) + "px"; 422 | } 423 | else if (this._axis === MichaelHintbuble.BubblePositioner.Y_AXIS) { 424 | this._element.style.left = (to.left + Math.ceil(tl.get("border-box-width")/2) - Math.ceil(el.get("padding-box-width")/2)) + "px"; 425 | } 426 | }; 427 | 428 | 429 | /** 430 | * This function returns a boolean indicating whether or not the element is 431 | * contained within the viewport. 432 | * 433 | * @returns {Boolean} whether or not the element is contained within the viewport. 434 | */ 435 | MichaelHintbuble.BubblePositioner.prototype._isElementWithinViewport = function() { 436 | var isWithinViewport = true; 437 | var fnMap = MichaelHintbuble.BubblePositioner.POSITION_FN_MAP; 438 | var method = null; 439 | var viewPortMinEdge = null; 440 | var viewPortMaxEdge = null; 441 | var elementMinEdge = null; 442 | var elementMaxEdge = null; 443 | 444 | for (var prop in fnMap) { 445 | method = fnMap[prop]; 446 | viewportMinEdge = document.viewport.getScrollOffsets()[prop]; 447 | viewportMaxEdge = viewportMinEdge + document.viewport[method](); 448 | elementMinEdge = parseInt(this._element.style[prop] || 0); 449 | elementMaxEdge = elementMinEdge + this._element[method](); 450 | 451 | if ((elementMaxEdge > viewportMaxEdge) || (elementMinEdge < viewportMinEdge)) { 452 | isWithinViewport = false; 453 | break; 454 | } 455 | } 456 | 457 | return isWithinViewport; 458 | }; 459 | 460 | 461 | /** 462 | * This function positions the element to the left of the target. 463 | */ 464 | MichaelHintbuble.BubblePositioner.prototype._left = function() { 465 | var to = this._targetAdjustedOffset(); 466 | var el = new Element.Layout(this._element); 467 | 468 | this._element.style.left = (to.left - el.get("padding-box-width")) + "px"; 469 | }; 470 | 471 | 472 | /** 473 | * This function positions the element to the right of the target. 474 | */ 475 | MichaelHintbuble.BubblePositioner.prototype._right = function() { 476 | var to = this._targetAdjustedOffset(); 477 | var tl = new Element.Layout(this._target); 478 | 479 | this._element.style.left = (to.left + tl.get("border-box-width")) + "px"; 480 | }; 481 | 482 | 483 | /** 484 | * This function positions the element relative to the target according to the 485 | * position value supplied. Because this function is private, it assumes a 486 | * safe position value. 487 | * 488 | * @param {String} position the desired relative position of the element to the 489 | * target. 490 | */ 491 | MichaelHintbuble.BubblePositioner.prototype._setPosition = function(position) { 492 | this._axis = MichaelHintbuble.BubblePositioner.AXIS_MAP[position]; 493 | this._position = position; 494 | this["_" + position](); 495 | this._center(); 496 | }; 497 | 498 | 499 | /** 500 | * This function returns a hash with the adjusted offset positions for the target 501 | * element. 502 | */ 503 | MichaelHintbuble.BubblePositioner.prototype._targetAdjustedOffset = function() { 504 | var bs = $$("body").first().cumulativeScrollOffset(); 505 | var to = this._target.cumulativeOffset(); 506 | var ts = this._target.cumulativeScrollOffset(); 507 | 508 | return { 509 | "top": to.top - ts.top + bs.top, 510 | "left": to.left - ts.left + bs.left 511 | } 512 | }; 513 | 514 | 515 | /** 516 | * This function positions the element above the target. 517 | */ 518 | MichaelHintbuble.BubblePositioner.prototype._top = function() { 519 | var to = this._targetAdjustedOffset(); 520 | var el = new Element.Layout(this._element); 521 | 522 | this._element.style.top = (to.top - el.get("padding-box-height")) + "px"; 523 | }; 524 | 525 | 526 | /** 527 | * This function allows the bubble positioner object to be destroyed without 528 | * creating memory leaks. 529 | */ 530 | MichaelHintbuble.BubblePositioner.prototype.finalize = function() { 531 | this._target = null; 532 | this._element = null; 533 | this._axis = null; 534 | this._position = null; 535 | }; 536 | 537 | 538 | /** 539 | * This function positions the element relative to the target according to the 540 | * position value supplied. Invalid position values are ignored. If the new 541 | * position runs off the viewport, the complement is tried. If that fails too, 542 | * it gives up and does what was asked. 543 | * 544 | * @param {String} position the desired relative position of the element to the 545 | * target. 546 | */ 547 | MichaelHintbuble.BubblePositioner.prototype.setPosition = function(position) { 548 | var axis = MichaelHintbuble.BubblePositioner.AXIS_MAP[position]; 549 | if (axis) { 550 | this._setPosition(position); 551 | if (!this._isElementWithinViewport()) { 552 | this._setPosition(MichaelHintbuble.BubblePositioner.COMPLEMENTS[position]); 553 | if (!this._isElementWithinViewport()) { 554 | this._setPosition(position); 555 | } 556 | } 557 | } 558 | }; 559 | 560 | 561 | /** 562 | * This function returns a string representation of the current logical positioning that 563 | * can be used as a stylesheet class for physical positioning. 564 | * 565 | * @returns {String} a styleclass name appropriate for the current position. 566 | */ 567 | MichaelHintbuble.BubblePositioner.prototype.styleClassForPosition = function() { 568 | return this._position.toLowerCase(); 569 | }; -------------------------------------------------------------------------------- /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/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 "
      " + ret.join('') + "
    "; 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 | }); --------------------------------------------------------------------------------