├── 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 /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.5 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.gem -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | require "michael_hintbuble" -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/rails/init.rb" -------------------------------------------------------------------------------- /generators/michael_hintbuble/templates/error_bubble_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/michael_hintbuble/master/generators/michael_hintbuble/templates/error_bubble_pointer.png -------------------------------------------------------------------------------- /generators/michael_hintbuble/templates/help_bubble_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/michael_hintbuble/master/generators/michael_hintbuble/templates/help_bubble_pointer.png -------------------------------------------------------------------------------- /lib/generators/michael_hintbuble/templates/help_bubble_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/michael_hintbuble/master/lib/generators/michael_hintbuble/templates/help_bubble_pointer.png -------------------------------------------------------------------------------- /lib/generators/michael_hintbuble/templates/error_bubble_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/michael_hintbuble/master/lib/generators/michael_hintbuble/templates/error_bubble_pointer.png -------------------------------------------------------------------------------- /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 } -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.specification: -------------------------------------------------------------------------------- 1 | --- !ruby/object:Gem::Specification 2 | name: michael_hintbuble 3 | version: !ruby/object:Gem::Version 4 | hash: 29 5 | prerelease: false 6 | segments: 7 | - 1 8 | - 0 9 | - 5 10 | version: 1.0.5 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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /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 | }; --------------------------------------------------------------------------------