├── VERSION ├── .gitignore ├── spec ├── spec.opts ├── unit │ ├── unit_spec_helper.rb │ ├── config_spec.rb │ ├── initializer_spec.rb │ ├── cache_manager_spec.rb │ └── controller_spec.rb ├── assets │ └── locales │ │ └── en.yml ├── functional │ ├── routes_spec.rb │ ├── cache_spec.rb │ ├── controller_spec.rb │ └── functional_spec_helper.rb └── spec_helper.rb ├── uninstall.rb ├── init.rb ├── assets ├── images │ ├── close.png │ ├── boxy-ne.png │ ├── boxy-nw.png │ ├── boxy-se.png │ ├── boxy-sw.png │ └── magnify.png ├── stylesheets │ ├── super_finder.css │ └── boxy.css └── javascripts │ ├── super_finder.js │ └── boxy.js ├── tasks └── super_finder_tasks.rake ├── lib ├── super_finder │ ├── routes.rb │ ├── helper.rb │ ├── filters.rb │ ├── initializer.rb │ ├── cache_manager.rb │ ├── config.rb │ ├── cache_sweeper.rb │ └── generator_controller.rb └── super_finder.rb ├── install.rb ├── MIT-LICENSE ├── Rakefile ├── super_finder.gemspec └── README.textile /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /uninstall.rb: -------------------------------------------------------------------------------- 1 | # Uninstall hook code here 2 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | $:.unshift "#{File.dirname(__FILE__)}/lib" 2 | require 'super_finder' 3 | -------------------------------------------------------------------------------- /spec/unit/unit_spec_helper.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', 'spec_helper') -------------------------------------------------------------------------------- /spec/assets/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | activerecord: 3 | models: 4 | project: Fun project -------------------------------------------------------------------------------- /assets/images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/super_finder/master/assets/images/close.png -------------------------------------------------------------------------------- /assets/images/boxy-ne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/super_finder/master/assets/images/boxy-ne.png -------------------------------------------------------------------------------- /assets/images/boxy-nw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/super_finder/master/assets/images/boxy-nw.png -------------------------------------------------------------------------------- /assets/images/boxy-se.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/super_finder/master/assets/images/boxy-se.png -------------------------------------------------------------------------------- /assets/images/boxy-sw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/super_finder/master/assets/images/boxy-sw.png -------------------------------------------------------------------------------- /assets/images/magnify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/did/super_finder/master/assets/images/magnify.png -------------------------------------------------------------------------------- /tasks/super_finder_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :super_finder do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /lib/super_finder/routes.rb: -------------------------------------------------------------------------------- 1 | module SuperFinder 2 | 3 | module Routes 4 | 5 | def self.draw(map) 6 | map.super_finder_resources '/super_finder_resources.js', :controller => 'super_finder/generator', :action => 'index' 7 | end 8 | end 9 | 10 | end -------------------------------------------------------------------------------- /lib/super_finder/helper.rb: -------------------------------------------------------------------------------- 1 | module SuperFinder 2 | 3 | module Helper 4 | 5 | def super_finder_tag 6 | %{ 7 | 11 | 12 | 13 | } 14 | end 15 | 16 | end 17 | 18 | end -------------------------------------------------------------------------------- /lib/super_finder.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path(File.dirname(__FILE__)) 2 | 3 | require 'super_finder/routes' 4 | require 'super_finder/config' 5 | require 'super_finder/initializer' 6 | require 'super_finder/cache_manager' 7 | require 'super_finder/cache_sweeper' 8 | require 'super_finder/helper' 9 | require 'super_finder/filters' 10 | 11 | ActionView::Base.send :include, SuperFinder::Helper 12 | 13 | # ActionController::Base.class_eval { include SuperFinder::Filters } # dirty hack for dev env 14 | 15 | require 'super_finder/generator_controller' 16 | 17 | 18 | -------------------------------------------------------------------------------- /install.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | unless defined?(RAILS_ROOT) 4 | RAILS_ROOT = File.expand_path( File.join(File.dirname(__FILE__), '../../../') ) 5 | end 6 | 7 | ['images', 'stylesheets', 'javascripts'].each do |folder| 8 | if folder == 'images' 9 | unless FileTest.exist? File.join(RAILS_ROOT, 'public', 'images', 'super_finder') 10 | FileUtils.mkdir( File.join(RAILS_ROOT, 'public', 'images', 'super_finder') ) 11 | end 12 | end 13 | 14 | FileUtils.cp( 15 | Dir[File.join(File.dirname(__FILE__), 'assets', folder, '*')], 16 | File.join([RAILS_ROOT, 'public', folder, folder == 'images' ? 'super_finder' : nil].compact) 17 | ) 18 | end 19 | -------------------------------------------------------------------------------- /spec/functional/routes_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe 'Routes' do 4 | 5 | before do 6 | ActionController::Routing::Routes.draw do |map| 7 | map.resources :foos do |fu| 8 | fu.resources :bars 9 | end 10 | 11 | SuperFinder::Routes.draw(map) 12 | 13 | map.connect '/:controller/:action/:id' 14 | end 15 | end 16 | 17 | it 'should add a custom route' do 18 | params_from(:get, '/super_finder_resources.js').should == { 19 | :controller => 'super_finder/generator', 20 | :action => 'index' 21 | } 22 | end 23 | 24 | # from rspec-rails 25 | def params_from(method, path) 26 | ActionController::Routing::Routes.recognize_path(path, :method => method) 27 | end 28 | 29 | end -------------------------------------------------------------------------------- /lib/super_finder/filters.rb: -------------------------------------------------------------------------------- 1 | # Monkey patch to enable filters even in development environment. 2 | module SuperFinder 3 | 4 | module Filters 5 | 6 | def self.included(base) 7 | base.extend(ClassMethods) 8 | end 9 | 10 | module ClassMethods 11 | 12 | def enable_super_finder_filters 13 | class_eval <<-EOV 14 | include SuperFinder::Filters::InstanceMethods 15 | before_filter :apply_superfinder_filters 16 | EOV 17 | end 18 | 19 | end 20 | 21 | module InstanceMethods 22 | 23 | def apply_superfinder_filters 24 | (SuperFinder::Config.instance.before_filters || []).each do |filter| 25 | return false if self.send(filter.to_sym) == false 26 | end 27 | end 28 | 29 | end 30 | 31 | end 32 | 33 | end -------------------------------------------------------------------------------- /lib/super_finder/initializer.rb: -------------------------------------------------------------------------------- 1 | module SuperFinder 2 | 3 | class Initializer 4 | 5 | unloadable 6 | 7 | def self.run(&block) 8 | block.call(SuperFinder::Config.instance) if block_given? 9 | 10 | raise 'SuperFinder needs one or many models' if (SuperFinder::Config.instance.models || []).empty? 11 | 12 | self.apply 13 | end 14 | 15 | protected 16 | 17 | def self.apply 18 | # Cache Sweeper 19 | SuperFinder::CacheSweeper.instance # register the observer 20 | 21 | # Before filters 22 | unless (filters = SuperFinder::Config.instance.before_filters).empty? 23 | SuperFinder::GeneratorController.class_eval do 24 | before_filter SuperFinder::Config.instance.before_filters 25 | end 26 | end 27 | end 28 | 29 | end 30 | 31 | end -------------------------------------------------------------------------------- /lib/super_finder/cache_manager.rb: -------------------------------------------------------------------------------- 1 | module SuperFinder 2 | 3 | class CacheManager 4 | 5 | include Singleton 6 | 7 | @@caching_keys = {} 8 | 9 | def fetch(klass, scoper = nil, &block) 10 | ::Rails.cache.fetch(self.key(klass, scoper), &block) 11 | end 12 | 13 | def key(klass, scoper = nil) 14 | key = internal_key(klass, scoper) 15 | 16 | @@caching_keys[key] ||= Time.now 17 | 18 | "superfinder_#{key}_#{@@caching_keys[key].to_i}" 19 | end 20 | 21 | def refresh!(klass, scoper = nil) 22 | @@caching_keys[internal_key(klass, scoper)] = nil 23 | end 24 | 25 | protected 26 | 27 | def internal_key(klass, scoper = nil) 28 | scope_id = (if scoper 29 | scoper.is_a?(Integer) ? scoper : scoper.id 30 | else 31 | :all 32 | end) 33 | "#{scope_id}_#{klass.name.tableize}" 34 | end 35 | 36 | end 37 | 38 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | 4 | require 'rubygems' 5 | require 'mocha' 6 | 7 | gem 'actionpack' 8 | gem 'activerecord', '>= 1.15.4.7794' 9 | require 'action_controller' 10 | require 'action_controller/test_process.rb' 11 | require 'active_support' 12 | require 'action_pack' 13 | require 'action_view' 14 | require 'active_record' 15 | require 'active_record/observer' 16 | 17 | require 'spec' 18 | require 'spec/mocks' 19 | require 'spec/mocks/mock.rb' 20 | require 'spec/rails/mocks.rb' 21 | require 'spec/autorun' 22 | 23 | I18n.load_path << Dir[File.join(File.dirname(__FILE__), 'assets', 'locales', '*.{rb,yml}') ] 24 | I18n.default_locale = :en 25 | 26 | class ApplicationController < ActionController::Base 27 | end 28 | 29 | require 'super_finder' 30 | 31 | 32 | Spec::Runner.configure do |config| 33 | config.mock_with :mocha 34 | end 35 | 36 | include ActionController::UrlWriter 37 | 38 | -------------------------------------------------------------------------------- /lib/super_finder/config.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module SuperFinder 4 | 5 | class Config 6 | 7 | include Singleton 8 | 9 | @@default_options = { 10 | :url => { 11 | :name_prefix => nil, 12 | :action => :show 13 | }, 14 | :models => [], 15 | :scoper => { 16 | :column => nil, 17 | :getter => nil 18 | }, 19 | :before_filters => [] 20 | } 21 | 22 | def attributes 23 | @attributes ||= @@default_options.clone # only called once 24 | end 25 | 26 | def reset_attributes 27 | @attributes = nil 28 | end 29 | 30 | def method_missing(method, *args) 31 | if method.to_s.ends_with?('=') 32 | attributes[method.to_s[0..-2].to_sym] = args.first 33 | else 34 | attributes[method] 35 | end 36 | end 37 | 38 | def to_s 39 | attributes.inspect 40 | end 41 | 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /spec/unit/config_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/unit_spec_helper') 2 | 3 | describe SuperFinder::Config do 4 | 5 | before(:each) do 6 | SuperFinder::Config.instance.reset_attributes 7 | end 8 | 9 | it 'should have default values' do 10 | SuperFinder::Config.instance.url.should == { :name_prefix => nil, :action => :show } 11 | SuperFinder::Config.instance.models.should be_empty 12 | SuperFinder::Config.instance.scoper.should == { :column => nil, :getter => nil } 13 | SuperFinder::Config.instance.before_filters.should be_empty 14 | end 15 | 16 | it 'should set custom values' do 17 | SuperFinder::Config.instance.url = { :name_prefix => 'admin', :action => :edit } 18 | SuperFinder::Config.instance.url.should == { :name_prefix => 'admin', :action => :edit } 19 | 20 | SuperFinder::Config.instance.before_filters = ['require_account', 'set_scoper'] 21 | SuperFinder::Config.instance.before_filters.should == ['require_account', 'set_scoper'] 22 | end 23 | 24 | end -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 [name of plugin creator] 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 | -------------------------------------------------------------------------------- /lib/super_finder/cache_sweeper.rb: -------------------------------------------------------------------------------- 1 | module SuperFinder 2 | 3 | class CacheSweeper < ActiveRecord::Observer 4 | 5 | unloadable 6 | 7 | def after_create(record) 8 | refresh_cache!(record) 9 | end 10 | 11 | def after_update(record) 12 | refresh_cache!(record) 13 | end 14 | 15 | def after_destroy(record) 16 | refresh_cache!(record) 17 | end 18 | 19 | def refresh_cache!(record) 20 | models, scoper = SuperFinder::Config.instance.models, SuperFinder::Config.instance.scoper 21 | 22 | options = models.find { |m| m[:klass].name == record.class.name } # do not rely on class in dev mode 23 | 24 | scope_id = nil 25 | 26 | if options[:scope].nil? 27 | if scoper && scoper[:column] # looking for a global scope 28 | scope_id = record.attributes[scoper[:column].to_s] 29 | end 30 | end 31 | 32 | SuperFinder::CacheManager.instance.refresh!(record.class, scope_id) 33 | end 34 | 35 | def observed_classes 36 | SuperFinder::Config.instance.models.map { |m| eval(m[:klass].name) } 37 | end 38 | 39 | end 40 | 41 | end -------------------------------------------------------------------------------- /assets/stylesheets/super_finder.css: -------------------------------------------------------------------------------- 1 | @import url(boxy.css); 2 | 3 | div#superfinder { width: 300px; min-height: 200px; } 4 | 5 | div#superfinder p { 6 | margin: 0px; 7 | background: transparent url(/images/super_finder/magnify.png) no-repeat right 3px; 8 | } 9 | 10 | div#superfinder p input { 11 | width: 260px; 12 | border: 1px solid #bbb; 13 | font-size: 16px; 14 | color: #222; 15 | padding: 5px; 16 | background: white; 17 | } 18 | 19 | div#superfinder ul { list-style: none; margin: 10px 0 0 0px; padding: 0px; } 20 | 21 | div#superfinder li { 22 | background: #dff5f8; 23 | padding: 5px; 24 | margin-bottom: 2px; 25 | overflow: hidden; 26 | } 27 | 28 | div#superfinder li.on { 29 | background-color: #ccc; 30 | } 31 | 32 | div#superfinder li label { 33 | position: relative; 34 | top: -2px; 35 | background: #333; 36 | padding: 2px 6px; 37 | color: white; 38 | font-weight: normal; 39 | font-size: 12px; 40 | 41 | -moz-border-radius: 5px; 42 | -webkit-border-radius: 5px; 43 | } 44 | 45 | div#superfinder li a { 46 | margin-left: 15px; 47 | color: #333; 48 | text-decoration: none; 49 | font-size: 16px; 50 | } 51 | 52 | div#superfinder li a em { 53 | font-style: normal; 54 | font-weight: bold; 55 | } 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /spec/unit/initializer_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/unit_spec_helper') 2 | 3 | describe SuperFinder::Initializer do 4 | 5 | before(:each) do 6 | SuperFinder::Config.instance.reset_attributes 7 | end 8 | 9 | it 'should update config settings' do 10 | SuperFinder::Initializer.stubs(:apply).returns(true) 11 | 12 | SuperFinder::Initializer.run do |config| 13 | config.url = { :name_prefix => 'admin', :action => :edit } 14 | config.models = ['foo', 'bar'] 15 | end 16 | 17 | SuperFinder::Config.instance.url[:name_prefix].should == 'admin' 18 | SuperFinder::Config.instance.url[:action].should == :edit 19 | end 20 | 21 | it 'should need models' do 22 | lambda { 23 | SuperFinder::Initializer.run 24 | }.should raise_error 25 | end 26 | 27 | it 'should apply default config' do 28 | SuperFinder::Initializer.expects(:apply).returns(true) 29 | SuperFinder::Initializer.run do |config| 30 | config.models = ['foo', 'bar'] 31 | end 32 | end 33 | 34 | it 'should apply new config once they all have been set' do 35 | SuperFinder::Initializer.expects(:apply).returns(true) 36 | 37 | SuperFinder::Initializer.run do |config| 38 | config.name_prefix = 'admin' 39 | config.default_action = :edit 40 | config.models = ['foo', 'bar'] 41 | end 42 | end 43 | 44 | end -------------------------------------------------------------------------------- /spec/unit/cache_manager_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/unit_spec_helper') 2 | 3 | describe SuperFinder::CacheManager do 4 | 5 | before(:each) do 6 | class Project; end 7 | end 8 | 9 | it 'should generate a key' do 10 | key = SuperFinder::CacheManager.instance.key(Project) 11 | key.should match /superfinder_all_projects_[0-9]*/ 12 | 13 | SuperFinder::CacheManager.instance.key(Project).should == key 14 | end 15 | 16 | it 'should generate a key depending on a scoper' do 17 | @instance = Object.new 18 | @instance.stubs(:id).returns(42) 19 | 20 | key = SuperFinder::CacheManager.instance.key(Project, @instance) 21 | key.should match /superfinder_42_projects_[0-9]*/ 22 | 23 | SuperFinder::CacheManager.instance.key(Project, @instance).should == key 24 | end 25 | 26 | it 'should generate a key depending on a scoper by passing its id' do 27 | key = SuperFinder::CacheManager.instance.key(Project, 7) 28 | key.should match /superfinder_7_projects_[0-9]*/ 29 | 30 | SuperFinder::CacheManager.instance.key(Project, 7).should == key 31 | end 32 | 33 | it 'should refresh a key' do 34 | key = SuperFinder::CacheManager.instance.key(Project) 35 | 36 | SuperFinder::CacheManager.instance.refresh!(Project) 37 | 38 | SuperFinder::CacheManager.instance.key(Project, @instance).should != key 39 | end 40 | 41 | end -------------------------------------------------------------------------------- /spec/functional/cache_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/functional_spec_helper') 2 | 3 | describe SuperFinder::CacheSweeper do 4 | 5 | before(:all) do 6 | add_project_and_task_records 7 | 8 | SuperFinder::Initializer.run do |config| 9 | config.name_prefix = 'admin' 10 | config.default_action = :edit 11 | config.scoper = { 12 | :column => :account_id 13 | } 14 | config.models = [ 15 | { :klass => Project }, 16 | { :klass => Task, :scope => false } 17 | ] 18 | end 19 | end 20 | 21 | it 'should refresh cache if a record is added' do 22 | SuperFinder::CacheSweeper.instance.expects(:refresh_cache!).returns(true) 23 | 24 | Project.create :title => 'Hello world !' 25 | end 26 | 27 | it 'should refresh cache if a record is updated' do 28 | SuperFinder::CacheSweeper.instance.expects(:refresh_cache!).returns(true) 29 | 30 | Project.first.update_attribute :title, 'new title' 31 | end 32 | 33 | it 'should refresh cache if a record is destroyed' do 34 | SuperFinder::CacheSweeper.instance.expects(:refresh_cache!).returns(true) 35 | 36 | Project.first.destroy 37 | end 38 | 39 | it 'should refresh cache based on a scope id' do 40 | SuperFinder::CacheManager.instance.expects(:refresh!).with(Project, 42).returns(true) 41 | 42 | Project.create :title => 'Hello world !', :account_id => 42 43 | end 44 | 45 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rubygems' 3 | require 'spec/rake/spectask' 4 | require 'rake/rdoctask' 5 | 6 | begin 7 | require 'jeweler' 8 | Jeweler::Tasks.new do |gem| 9 | gem.name = "super_finder" 10 | gem.summary = %Q{TextMate's "cmd-T" functionality in a web app} 11 | gem.description = %Q{TextMate's "cmd-T" functionality in a web app} 12 | gem.email = "didier@nocoffee.fr" 13 | gem.homepage = "http://github.com/did/super_finder" 14 | gem.authors = ["Didier Lafforgue"] 15 | gem.add_development_dependency "rspec", ">= 1.2.9" 16 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 17 | end 18 | Jeweler::GemcutterTasks.new 19 | rescue LoadError 20 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 21 | end 22 | 23 | desc 'Test the super_finder plugin.' 24 | Spec::Rake::SpecTask.new('spec:unit') do |spec| 25 | spec.libs << 'lib' << 'spec' 26 | spec.spec_files = FileList['spec/unit/**/*_spec.rb'] 27 | end 28 | 29 | Spec::Rake::SpecTask.new('spec:functionals') do |spec| 30 | spec.libs << 'lib' << 'spec' 31 | spec.spec_files = FileList['spec/functional/**/*_spec.rb'] 32 | end 33 | 34 | task :spec => ['spec:unit', 'spec:functionals'] 35 | 36 | desc 'Default: run rspec tests.' 37 | task :default => :spec 38 | 39 | desc 'Generate documentation for the super_finder plugin.' 40 | Rake::RDocTask.new(:rdoc) do |rdoc| 41 | rdoc.rdoc_dir = 'rdoc' 42 | rdoc.title = 'SuperFinder' 43 | rdoc.options << '--line-numbers' << '--inline-source' 44 | rdoc.rdoc_files.include('README') 45 | rdoc.rdoc_files.include('lib/**/*.rb') 46 | end 47 | -------------------------------------------------------------------------------- /spec/functional/controller_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/functional_spec_helper') 2 | 3 | describe SuperFinder::GeneratorController do 4 | 5 | before(:all) do 6 | add_project_and_task_records 7 | 8 | SuperFinder::Initializer.run do |config| 9 | config.url = { 10 | :name_prefix => 'admin', 11 | :action => :edit 12 | } 13 | config.scoper = { 14 | :column => :account_id, 15 | :getter => :current_account 16 | } 17 | config.models = [ 18 | { :klass => Project, :column => :title }, 19 | { :klass => Task, :column => :name, :label => 'My tasks', :scope => false }, 20 | { 21 | :klass => Person, 22 | :column => :nickname, 23 | :label => Proc.new { |c| "My people" }, 24 | :finder => Proc.new { |c| Person.all(:conditions => { :account_id => c.send(:current_account).id }) } 25 | } 26 | ] 27 | end 28 | 29 | # disable caching 30 | SuperFinder::CacheManager.instance.instance_eval do 31 | def fetch(klass, scoper = nil, &block) 32 | block.call 33 | end 34 | end 35 | 36 | @controller = SuperFinder::GeneratorController.new 37 | @controller.stubs(:url_for).returns('') 38 | end 39 | 40 | it 'should return of records with an url, a label and a name for each entry' do 41 | map = @controller.send(:generate) 42 | 43 | map.should_not be_empty 44 | map.size.should == 3 45 | 46 | map["My people"].count.should == 1 47 | map["Fun project"].count.should == 2 48 | map["My tasks"].count.should == 3 49 | end 50 | 51 | it 'should generate a json output' do 52 | @controller.stubs(:url_for).returns('') 53 | @controller.stubs(:view_paths).returns(nil) 54 | @controller.stubs(:default_template).returns(nil) 55 | @controller.response = ActionController::TestResponse.new 56 | 57 | output = @controller.send(:index) 58 | output.should match /^SuperFinderResources = \{(.*)\}$/ 59 | end 60 | 61 | end -------------------------------------------------------------------------------- /spec/functional/functional_spec_helper.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', 'spec_helper') 2 | 3 | ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:") 4 | 5 | ActiveRecord::Migration.verbose = false 6 | ActiveRecord::Schema.define(:version => 1) do 7 | create_table :projects do |t| 8 | t.column :title, :string 9 | t.column :account_id, :integer 10 | t.column :created_at, :datetime 11 | t.column :updated_at, :datetime 12 | end 13 | 14 | create_table :tasks do |t| 15 | t.column :name, :string 16 | t.column :created_at, :datetime 17 | t.column :updated_at, :datetime 18 | end 19 | 20 | create_table :people do |t| 21 | t.column :nickname, :string 22 | t.column :account_id, :integer 23 | t.column :created_at, :datetime 24 | t.column :updated_at, :datetime 25 | end 26 | end 27 | 28 | class Project < ActiveRecord::Base 29 | end 30 | 31 | class Task < ActiveRecord::Base 32 | end 33 | 34 | class Person < ActiveRecord::Base 35 | end 36 | 37 | class Account < ActiveRecord::Base 38 | end 39 | 40 | ApplicationController.class_eval do 41 | 42 | include Spec 43 | include Spec::Mocks 44 | include Spec::Mocks::Methods 45 | include Spec::Mocks::ExampleMethods 46 | include Spec::Rails::Mocks 47 | 48 | protected 49 | 50 | def current_account 51 | mock_model(Account, :id => 42) #("Account", :id => 42) 52 | end 53 | 54 | end 55 | 56 | 57 | def add_project_and_task_records 58 | clean_records 59 | 60 | Project.create :title => 'Ruby on Rails', :account_id => 42 61 | Project.create :title => 'Liquid', :account_id => 42 62 | Project.create :title => 'CMS', :account_id => 43 63 | 64 | Task.create :name => 'build engines' 65 | Task.create :name => 'drink some beer' 66 | Task.create :name => 'sleep' 67 | 68 | Person.create :nickname => 'Bart', :account_id => 42 69 | Person.create :nickname => 'Homer', :account_id => 43 70 | end 71 | 72 | def clean_records 73 | Project.destroy_all 74 | Task.destroy_all 75 | Person.destroy_all 76 | end 77 | 78 | # def teardown_db 79 | # ActiveRecord::Base.connection.tables.each do |table| 80 | # ActiveRecord::Base.connection.drop_table(table) 81 | # end 82 | # end -------------------------------------------------------------------------------- /assets/stylesheets/boxy.css: -------------------------------------------------------------------------------- 1 | .boxy-wrapper { position: absolute; } 2 | .boxy-wrapper.fixed { position: fixed; } 3 | 4 | /* Modal */ 5 | 6 | .boxy-modal-blackout { position: absolute; background-color: black; left: 0; top: 0; } 7 | 8 | /* Border */ 9 | 10 | .boxy-wrapper { empty-cells: show; width: auto; } 11 | .boxy-wrapper .top-left, 12 | .boxy-wrapper .top-right, 13 | .boxy-wrapper .bottom-right, 14 | .boxy-wrapper .bottom-left { width: 10px; height: 10px; padding: 0 } 15 | 16 | .boxy-wrapper .top-left { background: url('/images/super_finder/boxy-nw.png'); } 17 | .boxy-wrapper .top-right { background: url('/images/super_finder/boxy-ne.png'); } 18 | .boxy-wrapper .bottom-right { background: url('/images/super_finder/boxy-se.png'); } 19 | .boxy-wrapper .bottom-left { background: url('/images/super_finder/boxy-sw.png'); } 20 | 21 | /* IE6+7 hacks for the border. IE7 should support this natively but fails in conjuction with modal blackout bg. */ 22 | /* NB: these must be absolute paths or URLs to your images */ 23 | .boxy-wrapper .top-left { #background: none; #filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/images/super_finder/boxy-nw.png'); } 24 | .boxy-wrapper .top-right { #background: none; #filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/images/super_finder/boxy-ne.png'); } 25 | .boxy-wrapper .bottom-right { #background: none; #filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/images/super_finder/boxy-se.png'); } 26 | .boxy-wrapper .bottom-left { #background: none; #filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/images/super_finder/boxy-sw.png'); } 27 | 28 | .boxy-wrapper .top, 29 | .boxy-wrapper .bottom { height: 10px; background-color: black; opacity: 0.6; filter: alpha(opacity=60); padding: 0 } 30 | 31 | .boxy-wrapper .left, 32 | .boxy-wrapper .right { width: 10px; background-color: black; opacity: 0.6; filter: alpha(opacity=60); padding: 0 } 33 | 34 | /* Title bar */ 35 | 36 | .boxy-wrapper .title-bar { background-color: black; padding: 6px; position: relative; } 37 | .boxy-wrapper .title-bar.dragging { cursor: move; } 38 | .boxy-wrapper .title-bar h2 { font-size: 12px; color: white; line-height: 1; margin: 0; padding: 0; font-weight: normal; } 39 | .boxy-wrapper .title-bar .close { color: white; position: absolute; top: 6px; right: 6px; font-size: 90%; line-height: 1; } 40 | 41 | /* Content Region */ 42 | 43 | .boxy-inner { background-color: white; padding: 0 } 44 | .boxy-content { padding: 15px; } 45 | 46 | /* Question Boxes */ 47 | 48 | .boxy-wrapper .question { width: 350px; min-height: 80px; } 49 | .boxy-wrapper .answers { text-align: right; } 50 | -------------------------------------------------------------------------------- /lib/super_finder/generator_controller.rb: -------------------------------------------------------------------------------- 1 | class SuperFinder::GeneratorController < ApplicationController 2 | 3 | unloadable 4 | 5 | # enable_super_finder_filters # hack for dev env 6 | 7 | def index 8 | render :text => "SuperFinderResources = #{self.generate.to_json}" 9 | end 10 | 11 | protected 12 | 13 | def reloaded_entry_klass(klass) 14 | Rails.env.development? ? eval(klass.name) : klass 15 | rescue 16 | klass 17 | end 18 | 19 | def collect_entries_for(options) 20 | SuperFinder::CacheManager.instance.fetch(options[:klass], get_scoper(options)) do 21 | entries = (case options[:finder] 22 | when Proc 23 | options[:finder].call(self) 24 | else 25 | scoper = SuperFinder::Config.instance.scoper 26 | 27 | conditions = {} 28 | 29 | if options[:scope].nil? 30 | if scoper && scoper[:column] # looking for a global scope 31 | conditions = { scoper[:column] => self.send(scoper[:getter]).id } 32 | end 33 | end 34 | 35 | reloaded_entry_klass(options[:klass]).all(:conditions => conditions) 36 | end) 37 | 38 | entries.map do |entry| 39 | { 40 | :value => column_value(entry, options), 41 | :url => entry_url(entry, options) 42 | } 43 | end 44 | end 45 | end 46 | 47 | def get_scoper(options) 48 | self.send(SuperFinder::Config.instance.scoper[:getter].to_sym) rescue nil 49 | end 50 | 51 | def label_name(options) 52 | case options[:label] 53 | when String, Symbol then options[:label].to_s 54 | when Proc then options[:label].call(self) 55 | else 56 | name = options[:klass].name 57 | I18n.t(name.demodulize.underscore, :scope => [:activerecord, :models], :default => name.humanize) 58 | end 59 | end 60 | 61 | def column_value(entry, options) 62 | case options[:column] 63 | when String, Symbol then entry.attributes[options[:column].to_s] 64 | when Proc then options[:column].call(entry).to_s 65 | else 66 | '[Superfinder] Column is missing' 67 | end 68 | end 69 | 70 | def entry_url(entry, options) 71 | url = options[:url] 72 | case url 73 | when Hash, nil 74 | url = (SuperFinder::Config.instance.url || {}).merge(url || {}) 75 | resource_name = entry.class.name.pluralize.underscore 76 | 77 | url_for({ 78 | :controller => File.join(['/', url[:name_prefix], resource_name].compact), 79 | :action => url[:action].to_sym , 80 | :id => entry.id 81 | }) 82 | when Proc then url.call(self, entry) 83 | end 84 | end 85 | 86 | def generate 87 | map = {} 88 | SuperFinder::Config.instance.models.each do |options| 89 | map[label_name(options)] = self.collect_entries_for(options) 90 | end 91 | map 92 | end 93 | 94 | end 95 | -------------------------------------------------------------------------------- /spec/unit/controller_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/unit_spec_helper') 2 | 3 | describe SuperFinder::GeneratorController do 4 | 5 | before(:all) do 6 | class Project; end 7 | class Task; end 8 | class HelloWorld; end 9 | 10 | SuperFinder::CacheSweeper.stubs(:instance).returns(nil) 11 | SuperFinder::Initializer.run do |config| 12 | config.url = { 13 | :name_prefix => 'admin', 14 | :action => :edit 15 | } 16 | config.before_filters = [:hello_world, :foo] 17 | config.models = [:foo, :bar] 18 | end 19 | end 20 | 21 | before(:each) do 22 | @controller = SuperFinder::GeneratorController.new 23 | @controller.stubs(:url_for).returns('') 24 | 25 | @entry = Project.new 26 | @entry.stubs(:id).returns(42) 27 | @entry.stubs(:attributes).returns({ 'title' => 'Simple one' }) 28 | 29 | end 30 | 31 | it 'should generate a label from a String or a Symbol' do 32 | @controller.send(:label_name, { :label => "test" }).should == 'test' 33 | @controller.send(:label_name, { :label => :hello }).should == 'hello' 34 | end 35 | 36 | it 'should generate a label from a Proc' do 37 | @controller.send(:label_name, { :label => Proc.new { |c| 'Test' } }).should == 'Test' 38 | end 39 | 40 | it 'should generate a label even without an option' do 41 | @controller.send(:label_name, { :klass => Project }).should == 'Fun project' 42 | @controller.send(:label_name, { :klass => Task }).should == 'Task' 43 | @controller.send(:label_name, { :klass => HelloWorld }).should == 'Helloworld' 44 | end 45 | 46 | it 'should return the value of an entry from the String or Symbol column option' do 47 | @controller.send(:column_value, @entry, { :klass => Project }).should_not == 'Simple one' 48 | @controller.send(:column_value, @entry, { :klass => Project, :column => :title }).should == 'Simple one' 49 | @controller.send(:column_value, @entry, { :klass => Project, :column => 'title' }).should == 'Simple one' 50 | end 51 | 52 | it 'should return the value of an entry from an Proc column option' do 53 | @entry.stubs(:foo).returns('Foo') 54 | @controller.send(:column_value, @entry, { :klass => Project, :column => Proc.new { |p| p.foo } }).should == 'Foo' 55 | end 56 | 57 | it 'should generate an url' do 58 | @controller.expects(:url_for).with({ :controller => '/admin/projects', :action => :edit, :id => 42 }) 59 | @controller.send(:entry_url, @entry, { :klass => Project }) 60 | end 61 | 62 | it 'should generate an url from an Hash option' do 63 | @controller.expects(:url_for).with({ :controller => '/projects', :action => :show, :id => 42 }) 64 | @controller.send(:entry_url, @entry, { :klass => Project, :url => { :name_prefix => nil, :action => 'show' } }) 65 | end 66 | 67 | it 'should generate an url from an Proc option' do 68 | @controller.send(:entry_url, @entry, { :klass => Project, :url => Proc.new { |c, p| '/index.html' } }).should == '/index.html' 69 | end 70 | 71 | it 'should have filters defined in the config instance' do 72 | @controller.class.send(:filter_chain).count.should == 2 73 | @controller.class.send(:filter_chain).collect(&:method).should == [:hello_world, :foo] 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /super_finder.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{super_finder} 8 | s.version = "0.0.1" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Didier Lafforgue"] 12 | s.date = %q{2010-04-07} 13 | s.description = %q{TextMate's "cmd-T" functionality in a web app} 14 | s.email = %q{didier@nocoffee.fr} 15 | s.extra_rdoc_files = [ 16 | "README.textile" 17 | ] 18 | s.files = [ 19 | ".gitignore", 20 | "MIT-LICENSE", 21 | "README.textile", 22 | "Rakefile", 23 | "VERSION", 24 | "assets/images/boxy-ne.png", 25 | "assets/images/boxy-nw.png", 26 | "assets/images/boxy-se.png", 27 | "assets/images/boxy-sw.png", 28 | "assets/images/close.png", 29 | "assets/images/magnify.png", 30 | "assets/javascripts/boxy.js", 31 | "assets/javascripts/super_finder.js", 32 | "assets/stylesheets/boxy.css", 33 | "assets/stylesheets/super_finder.css", 34 | "init.rb", 35 | "install.rb", 36 | "lib/super_finder.rb", 37 | "lib/super_finder/cache_manager.rb", 38 | "lib/super_finder/cache_sweeper.rb", 39 | "lib/super_finder/config.rb", 40 | "lib/super_finder/filters.rb", 41 | "lib/super_finder/generator_controller.rb", 42 | "lib/super_finder/helper.rb", 43 | "lib/super_finder/initializer.rb", 44 | "lib/super_finder/routes.rb", 45 | "spec/assets/locales/en.yml", 46 | "spec/functional/cache_spec.rb", 47 | "spec/functional/controller_spec.rb", 48 | "spec/functional/functional_spec_helper.rb", 49 | "spec/functional/routes_spec.rb", 50 | "spec/spec.opts", 51 | "spec/spec_helper.rb", 52 | "spec/unit/cache_manager_spec.rb", 53 | "spec/unit/config_spec.rb", 54 | "spec/unit/controller_spec.rb", 55 | "spec/unit/initializer_spec.rb", 56 | "spec/unit/unit_spec_helper.rb", 57 | "super_finder.gemspec", 58 | "tasks/super_finder_tasks.rake", 59 | "uninstall.rb" 60 | ] 61 | s.homepage = %q{http://github.com/did/super_finder} 62 | s.rdoc_options = ["--charset=UTF-8"] 63 | s.require_paths = ["lib"] 64 | s.rubygems_version = %q{1.3.6} 65 | s.summary = %q{TextMate's "cmd-T" functionality in a web app} 66 | s.test_files = [ 67 | "spec/functional/cache_spec.rb", 68 | "spec/functional/controller_spec.rb", 69 | "spec/functional/functional_spec_helper.rb", 70 | "spec/functional/routes_spec.rb", 71 | "spec/spec_helper.rb", 72 | "spec/unit/cache_manager_spec.rb", 73 | "spec/unit/config_spec.rb", 74 | "spec/unit/controller_spec.rb", 75 | "spec/unit/initializer_spec.rb", 76 | "spec/unit/unit_spec_helper.rb" 77 | ] 78 | 79 | if s.respond_to? :specification_version then 80 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 81 | s.specification_version = 3 82 | 83 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 84 | s.add_development_dependency(%q, [">= 1.2.9"]) 85 | else 86 | s.add_dependency(%q, [">= 1.2.9"]) 87 | end 88 | else 89 | s.add_dependency(%q, [">= 1.2.9"]) 90 | end 91 | end 92 | 93 | -------------------------------------------------------------------------------- /assets/javascripts/super_finder.js: -------------------------------------------------------------------------------- 1 | // Some portions of the code (related to keypress events) are inspired 2 | // one of the jquery autocomplete plugin 3 | // (http://www.pengoworks.com/workshop/jquery/autocomplete.htm) 4 | 5 | function SuperFinder(options) { 6 | var self = this; 7 | 8 | this.options = jQuery.extend({}, SuperFinder.DEFAULTS, options || {}); 9 | 10 | this.input = jQuery('#superfinder input[name=q]'); 11 | this.list = jQuery("#superfinder ul"); 12 | 13 | // Find -> Command T 14 | jQuery(window).keypress(function(event) { 15 | // console.log("keypressed " + event.which + ", " + event.altKey); 16 | if (!(event.which == 8224 && event.altKey)) return true; 17 | self.open(); 18 | event.preventDefault(); 19 | return false; 20 | }); 21 | 22 | jQuery(this.options.button).click(function(event) { 23 | self.open(); 24 | event.preventDefault(); 25 | return false; 26 | }); 27 | 28 | this.input.attr("autocomplete", "off") 29 | .keydown(function(e) { 30 | // track last key pressed 31 | self.lastKeyPressCode = e.keyCode; 32 | switch(e.keyCode) { 33 | case 27: // ESC 34 | e.preventDefault(); 35 | self.input.blur(); 36 | self.close(); 37 | case 38: // up 38 | e.preventDefault(); 39 | self.moveSelect(-1); 40 | break; 41 | case 40: // down 42 | e.preventDefault(); 43 | self.moveSelect(1); 44 | break; 45 | case 9: // tab 46 | e.preventDefault(); 47 | break; 48 | case 13: // return 49 | self.input.blur(); 50 | self.selectCurrent(); 51 | e.preventDefault(); 52 | break; 53 | default: 54 | self.active = -1; 55 | if (self.timeout) clearTimeout(self.timeout); 56 | self.timeout = setTimeout(function(){ self.onChange(); }, 400); 57 | break; 58 | } 59 | }); 60 | }; 61 | 62 | jQuery.extend(SuperFinder, { 63 | 64 | DEFAULTS: { 65 | title: 'Finder', 66 | limit: 6, 67 | maxWordSize: 15, 68 | button: [] 69 | } 70 | 71 | }); 72 | 73 | SuperFinder.prototype = { 74 | 75 | open: function() { 76 | if (this.boxy == null) { 77 | this.boxy = new Boxy(jQuery('#superfinder'), { 78 | title: this.options.title, 79 | closeText: "\"[close]\"" 80 | }); 81 | this.input.focus(); 82 | } else { 83 | this.boxy.show(); 84 | this.reset(); 85 | } 86 | }, 87 | 88 | close: function() { 89 | this.boxy.hide(); 90 | }, 91 | 92 | search: function(val) { 93 | // console.log("looking for '" + val + "'"); 94 | var matches = []; 95 | 96 | if (val.length != 0) { 97 | var regexp = this.buildRegExp(val); 98 | for (var key in SuperFinderResources) { 99 | var tmp = jQuery.grep(SuperFinderResources[key], function(resource) { 100 | var matched = regexp.test(resource.value.toLowerCase()); 101 | // console.log("resource.value=" + resource.value + ", label = " + key + ", matched =" + matched); 102 | if (matched) resource.label = key; 103 | return matched; 104 | }); 105 | jQuery.merge(matches, tmp); 106 | } 107 | } 108 | 109 | this.update(matches.slice(0, this.options.limit), val); 110 | }, 111 | 112 | selectCurrent: function() { 113 | var current = jQuery('li.on', this.list); 114 | 115 | if (current.size() != 0) 116 | window.location.href = current.find('a').attr('href'); 117 | }, 118 | 119 | update: function(matches, val) { 120 | var self = this; 121 | this.list.empty(); 122 | 123 | jQuery.each(matches, function() { 124 | var className = this.label.toLowerCase().replace(/ /g, "-").replace(/_/g, "-"); 125 | var name = self.highlight(val, this.value); 126 | 127 | var li = "
  • " + name + "
  • "; 128 | self.list.append(li); 129 | }); 130 | 131 | if (matches.length > 0) 132 | this.moveSelect(1); 133 | }, 134 | 135 | moveSelect: function(step) { 136 | var children = this.list.children(); 137 | 138 | if (children.length == 0) return ; 139 | 140 | if (this.active >= 0) 141 | jQuery(children[this.active]).find('label').animate({ marginLeft: '0px' }, 'fast'); 142 | 143 | this.active += step; 144 | 145 | if (this.active < 0) { 146 | this.active = 0; 147 | } else if (this.active >= children.size()) { 148 | this.active = children.size() - 1; 149 | } 150 | 151 | children.removeClass('on'); 152 | jQuery(children[this.active]).addClass('on').find('label').animate({ marginLeft: '10px' }, 'fast'); 153 | }, 154 | 155 | onChange: function() { 156 | if (this.lastKeyPressCode == 46 || (this.lastKeyPressCode > 8 && this.lastKeyPressCode < 32)) return ; 157 | var val = this.input.val(); 158 | if (val == this.prev) return ; 159 | this.prev = val; 160 | this.search(val); 161 | }, 162 | 163 | reset: function() { 164 | this.list.empty(); 165 | this.input.val('').focus(); 166 | this.active = -1; 167 | this.prev = null; 168 | }, 169 | 170 | highlight: function(val, word) { 171 | var text = ""; 172 | for (var i = 0; i < val.length; i++) { 173 | var pos = word.toLowerCase().indexOf(val[i]); 174 | if (pos != -1) { 175 | text += word.slice(0, pos) + "" + word[pos] + ""; 176 | if (i == val.length - 1) 177 | text += word.slice(pos + 1); 178 | else 179 | word = word.slice(pos + 1); 180 | } 181 | } 182 | return text; 183 | }, 184 | 185 | buildRegExp: function(val) { 186 | var exp = ""; 187 | for (var i = 0; i < val.length; i++) { 188 | exp += val[i]; 189 | if (i < val.length - 1) exp += ".*"; 190 | } 191 | return new RegExp(exp); 192 | } 193 | }; 194 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. SuperFinder 2 | 3 | TextMate’s "cmd-T" functionality right in your ruby on rails application. Results in the popup come from models you choose. Almost everything can be re-defined to match your needs. 4 | 5 | I strongly suggest you to check our demo application to see how it looks. 6 | 7 | h2. Pre-requisities 8 | 9 | ActiveRecord as ORM, jQuery and jQuery boxy plugin. 10 | 11 | h2. Installation 12 | 13 | For now, only installation as a plugin is available. 14 | Even if there are multiple steps to pass before using it, the installation is pretty simple. 15 | 16 |
     17 | script/plugin install git://github.com/did/super_finder.git
     18 | 
    19 | 20 | Then, in your config/routes.rb, just add at the end of your file the following statement: 21 | 22 |
     23 | SuperFinder::Routes.draw(map)
     24 | 
    25 | 26 | Register an new observer in config/environment.rb (or add it to your existing observers list). This observer is responsible for sweeping the cache. 27 | 28 |
     29 | config.active_record.observers = 'super_finder/cache_sweeper'
     30 | 
    31 | 32 | Define the behavior of the plugin by creating an initializer file (initializers/super_finder.rb for instance) 33 | 34 |
     35 | SuperFinder::Initializer.run do |config|
     36 |   config.models = [
     37 |     { :klass => Project, :column => :title },
     38 |     { :klass => Person, :column => :nickname }
     39 |   ]
     40 | end
     41 | 
    42 | 43 | Next, inside your application layout, just before the closing BODY tag, add this: 44 | 45 |
     46 | <%= super_finder_tag %>
     47 | 
    48 | 49 | Include the javascript and stylesheets files (do not forget to include jQuery as well): 50 | 51 |
     52 | <%= stylesheet_link_tag 'super_finder' %>
     53 | <%= javascript_include_tag 'boxy', 'super_finder' %>
     54 | 
    55 | 56 | Finally, put into your application.js 57 | 58 |
    	
     59 | $(document).ready(function() {
     60 | 	new SuperFinder({ button: $('#your-button a') })	
     61 | });
     62 | 
    63 | 64 | _Note: button is optional since we set up a shortcut *(ALT-T)* 65 | 66 | That's it !!! 67 | 68 | h2. Usage 69 | 70 | There are 2 ways to display the popup and begin to search for something 71 | 72 | * Press ALT and t in your browser. 73 | * if you provide a css selector for *button* (see previous section), just click the button 74 | 75 | Then, it's pretty straigth forward, type some letters and use your keyboard arrows to select your item. Once you're done, press ENTER. 76 | 77 | You may want to change the color assigned to a label. Just add the following lines in one of your stylesheet files 78 | 79 |
     80 | div#superfinder li.project label { background-color: #64992c; }
     81 | div#superfinder li.task label { background-color: #5a6986; }
     82 | div#superfinder li.person label { background-color: #ec7000; }
     83 | div#superfinder li.account label { background-color: #5229a3; }
     84 | 
    85 | 86 | h2. How it works ? 87 | 88 | Thru its own controller, SuperFinder generates on the fly a js file storing by model all the records it found. 89 | It means some important things: 90 | 91 | * It best fits for small collections of records. Not to fetch all the customers of an important e-commerce back-office. 92 | * Authentication and before_filter constraints can be applied. It prevents not authorized people to sneak at data. 93 | 94 | The javascript output is just a single statement; it assigns the previous collection to a global javascript variable. 95 | Then, a jquery uses this variable to emulate the same behaviour of the TextMate’s "cmd-T" functionality 96 | 97 | 98 | h2. More settings 99 | 100 | Instead of writing hundred or thousands lines, I prefer to show you some examples: 101 | 102 | h3. Simple one 103 | 104 |
    105 | SuperFinder::Initializer.run do |config|
    106 |   config.models = [
    107 |     { :klass => Project, :column => :title },
    108 |     { :klass => Person, :column => :nickname }
    109 |   ]
    110 | end
    111 | 
    112 | 113 | Let's say you have a project in db whose title is "Ruby on Rails" and id is 42. If you type, "ru" in the popup, "Ruby on Rails" will appear and if you press ENTER, you will be redirected to *project_url(42)*. 114 | Simple, isn't it ? 115 | 116 | 117 | h3. I18n 118 | 119 | For each entry displayed in the popup, a little label precising the model type is also displayed as well. 120 | By default, SuperFinder will find the translation of *activerecord.models.* and if it's missing humanize the model class name. But you can also define it ! 121 | 122 |
    123 | SuperFinder::Initializer.run do |config|
    124 |   config.models = [
    125 |     { :klass => Project, :label => 'My fancy projects', :column => :title },
    126 |     { :klass => Task, :label => Proc.new { |controller| "blablabla" }, :column => :title },
    127 |     { :klass => Person, :column => Proc.new { |p| p.full_name } }
    128 |   ]
    129 | end
    130 | 
    131 | 132 | h3. Urls 133 | 134 | When pressing enter, you are redirected to the url of the selected entry. For each model, you can define your own url strategy. 135 | 136 |
    137 | SuperFinder::Initializer.run do |config|
    138 |   config.url = {
    139 |     :name_prefix  => 'admin',
    140 |     :action       => :edit # by default, :show
    141 |   }
    142 |   config.models = [
    143 |     { :klass => Project, :column => :title, :url => { :action => :show, :name_prefix =>nil } },
    144 |     { :klass => Task, :column => :title },
    145 |     { :klass => Person, :column => :nickname, :url => Proc.new { |controller, person| controller.my_url(person) } }
    146 |   ]
    147 | end
    148 | 
    149 | 150 | Generated urls will be: 151 | 152 | * *project_url()* 153 | * *edit_admin_task_url()* 154 | * *edit_admin_task_url()* 155 | * *my_url()* 156 | 157 | h3. Scoping / finder 158 | 159 | Sometimes, one of your model is scoped and bound to an account for instance. The scoper object is generally retrieved from the controller based on subdomain. 160 | SuperFinder allows you to use the scoper to find entries. 161 | 162 |
    163 | SuperFinder::Initializer.run do |config|
    164 |   config.scoper = {
    165 |     :column_id => :account_id,
    166 |     :getter => :current_account # name of controller method returning the scoper instance
    167 |   }
    168 |   config.models = [
    169 |     { :klass => Project, :column => :title },
    170 |     { :klass => Person, :column => :nickname, :scoper => false }
    171 |     { :klass => Task, :column => :title, :finder => Proc.new { |controller| Task.active.all } }
    172 |   ]
    173 | end
    174 | 
    175 | 176 | h3. Controller filtering 177 | 178 | Of course, you do not want bad people sneak at your data. No problems, here is the solution 179 | 180 |
    181 | SuperFinder::Initializer.run do |config|
    182 |   config.before_filters = [:must_be_authenticated]
    183 |   config.models = [
    184 |      { :klass => Project, :column => :title },
    185 |      { :klass => Person, :column => :nickname }
    186 |   ]
    187 | end
    188 | 
    189 | 190 | _Note: before_filters does not work in development mode for some class reloading issues. 191 | 192 | h2. Demo 193 | 194 | A big thank you at Heroku's people for their awesome service. 195 | 196 | "http://superfinder.heroku.com":http://superfinder.heroku.com 197 | 198 | h2. Tests / Bugs / Evolutions 199 | 200 | The plugin is fully tests with rspec (unit / functional tests). Into the plugin folder, type 201 | 202 |
    203 | rake
    204 | 
    205 | 206 | You may find bugs, sure you will actually. If you have time to investigate and solve them, just apply the classic procedure (fork, fix, test and submit). 207 | 208 | For evolutions, you're welcome to suggest your ideas. Contact me at didier at nocoffee dot fr. 209 | 210 | 211 | Copyright (c) 2010 NoCoffee, released under the MIT license 212 | -------------------------------------------------------------------------------- /assets/javascripts/boxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Boxy 0.1.4 - Facebook-style dialog, with frills 3 | * 4 | * (c) 2008 Jason Frame 5 | * Licensed under the MIT License (LICENSE) 6 | */ 7 | 8 | /* 9 | * jQuery plugin 10 | * 11 | * Options: 12 | * message: confirmation message for form submit hook (default: "Please confirm:") 13 | * 14 | * Any other options - e.g. 'clone' - will be passed onto the boxy constructor (or 15 | * Boxy.load for AJAX operations) 16 | */ 17 | jQuery.fn.boxy = function(options) { 18 | options = options || {}; 19 | return this.each(function() { 20 | var node = this.nodeName.toLowerCase(), self = this; 21 | if (node == 'a') { 22 | jQuery(this).click(function() { 23 | var active = Boxy.linkedTo(this), 24 | href = this.getAttribute('href'), 25 | localOptions = jQuery.extend({actuator: this, title: this.title}, options); 26 | 27 | if (active) { 28 | active.show(); 29 | } else if (href.indexOf('#') >= 0) { 30 | var content = jQuery(href.substr(href.indexOf('#'))), 31 | newContent = content.clone(true); 32 | content.remove(); 33 | localOptions.unloadOnHide = false; 34 | new Boxy(newContent, localOptions); 35 | } else { // fall back to AJAX; could do with a same-origin check 36 | if (!localOptions.cache) localOptions.unloadOnHide = true; 37 | Boxy.load(this.href, localOptions); 38 | } 39 | 40 | return false; 41 | }); 42 | } else if (node == 'form') { 43 | jQuery(this).bind('submit.boxy', function() { 44 | Boxy.confirm(options.message || 'Please confirm:', function() { 45 | jQuery(self).unbind('submit.boxy').submit(); 46 | }); 47 | return false; 48 | }); 49 | } 50 | }); 51 | }; 52 | 53 | // 54 | // Boxy Class 55 | 56 | function Boxy(element, options) { 57 | 58 | this.boxy = jQuery(Boxy.WRAPPER); 59 | jQuery.data(this.boxy[0], 'boxy', this); 60 | 61 | this.visible = false; 62 | this.options = jQuery.extend({}, Boxy.DEFAULTS, options || {}); 63 | 64 | if (this.options.modal) { 65 | this.options = jQuery.extend(this.options, {center: true, draggable: false}); 66 | } 67 | 68 | // options.actuator == DOM element that opened this boxy 69 | // association will be automatically deleted when this boxy is remove()d 70 | if (this.options.actuator) { 71 | jQuery.data(this.options.actuator, 'active.boxy', this); 72 | } 73 | 74 | this.setContent(element || "
    "); 75 | this._setupTitleBar(); 76 | 77 | this.boxy.css('display', 'none').appendTo(document.body); 78 | this.toTop(); 79 | 80 | if (this.options.fixed) { 81 | if (jQuery.browser.msie && jQuery.browser.version < 7) { 82 | this.options.fixed = false; // IE6 doesn't support fixed positioning 83 | } else { 84 | this.boxy.addClass('fixed'); 85 | } 86 | } 87 | 88 | if (this.options.center && Boxy._u(this.options.x, this.options.y)) { 89 | this.center(); 90 | } else { 91 | this.moveTo( 92 | Boxy._u(this.options.x) ? this.options.x : Boxy.DEFAULT_X, 93 | Boxy._u(this.options.y) ? this.options.y : Boxy.DEFAULT_Y 94 | ); 95 | } 96 | 97 | if (this.options.show) this.show(); 98 | 99 | }; 100 | 101 | Boxy.EF = function() {}; 102 | 103 | jQuery.extend(Boxy, { 104 | 105 | WRAPPER: "" + 106 | "" + 107 | "" + 108 | "" + 109 | "
    ", 110 | 111 | DEFAULTS: { 112 | title: null, // titlebar text. titlebar will not be visible if not set. 113 | closeable: true, // display close link in titlebar? 114 | draggable: true, // can this dialog be dragged? 115 | clone: false, // clone content prior to insertion into dialog? 116 | actuator: null, // element which opened this dialog 117 | center: true, // center dialog in viewport? 118 | show: true, // show dialog immediately? 119 | modal: false, // make dialog modal? 120 | fixed: true, // use fixed positioning, if supported? absolute positioning used otherwise 121 | closeText: '[close]', // text to use for default close link 122 | unloadOnHide: false, // should this dialog be removed from the DOM after being hidden? 123 | clickToFront: false, // bring dialog to foreground on any click (not just titlebar)? 124 | behaviours: Boxy.EF, // function used to apply behaviours to all content embedded in dialog. 125 | afterDrop: Boxy.EF, // callback fired after dialog is dropped. executes in context of Boxy instance. 126 | afterShow: Boxy.EF, // callback fired after dialog becomes visible. executes in context of Boxy instance. 127 | afterHide: Boxy.EF, // callback fired after dialog is hidden. executed in context of Boxy instance. 128 | beforeUnload: Boxy.EF // callback fired after dialog is unloaded. executed in context of Boxy instance. 129 | }, 130 | 131 | DEFAULT_X: 50, 132 | DEFAULT_Y: 50, 133 | zIndex: 1337, 134 | dragConfigured: false, // only set up one drag handler for all boxys 135 | resizeConfigured: false, 136 | dragging: null, 137 | 138 | // load a URL and display in boxy 139 | // url - url to load 140 | // options keys (any not listed below are passed to boxy constructor) 141 | // type: HTTP method, default: GET 142 | // cache: cache retrieved content? default: false 143 | // filter: jQuery selector used to filter remote content 144 | load: function(url, options) { 145 | 146 | options = options || {}; 147 | 148 | var ajax = { 149 | url: url, type: 'GET', dataType: 'html', cache: false, success: function(html) { 150 | html = jQuery(html); 151 | if (options.filter) html = jQuery(options.filter, html); 152 | new Boxy(html, options); 153 | } 154 | }; 155 | 156 | jQuery.each(['type', 'cache'], function() { 157 | if (this in options) { 158 | ajax[this] = options[this]; 159 | delete options[this]; 160 | } 161 | }); 162 | 163 | jQuery.ajax(ajax); 164 | 165 | }, 166 | 167 | // allows you to get a handle to the containing boxy instance of any element 168 | // e.g. inspect!. 169 | // this returns the actual instance of the boxy 'class', not just a DOM element. 170 | // Boxy.get(this).hide() would be valid, for instance. 171 | get: function(ele) { 172 | var p = jQuery(ele).parents('.boxy-wrapper'); 173 | return p.length ? jQuery.data(p[0], 'boxy') : null; 174 | }, 175 | 176 | // returns the boxy instance which has been linked to a given element via the 177 | // 'actuator' constructor option. 178 | linkedTo: function(ele) { 179 | return jQuery.data(ele, 'active.boxy'); 180 | }, 181 | 182 | // displays an alert box with a given message, calling optional callback 183 | // after dismissal. 184 | alert: function(message, callback, options) { 185 | return Boxy.ask(message, ['OK'], callback, options); 186 | }, 187 | 188 | // displays an alert box with a given message, calling after callback iff 189 | // user selects OK. 190 | confirm: function(message, after, options) { 191 | return Boxy.ask(message, ['OK', 'Cancel'], function(response) { 192 | if (response == 'OK') after(); 193 | }, options); 194 | }, 195 | 196 | // asks a question with multiple responses presented as buttons 197 | // selected item is returned to a callback method. 198 | // answers may be either an array or a hash. if it's an array, the 199 | // the callback will received the selected value. if it's a hash, 200 | // you'll get the corresponding key. 201 | ask: function(question, answers, callback, options) { 202 | 203 | options = jQuery.extend({modal: true, closeable: false}, 204 | options || {}, 205 | {show: true, unloadOnHide: true}); 206 | 207 | var body = jQuery('
    ').append(jQuery('
    ').html(question)); 208 | 209 | // ick 210 | var map = {}, answerStrings = []; 211 | if (answers instanceof Array) { 212 | for (var i = 0; i < answers.length; i++) { 213 | map[answers[i]] = answers[i]; 214 | answerStrings.push(answers[i]); 215 | } 216 | } else { 217 | for (var k in answers) { 218 | map[answers[k]] = k; 219 | answerStrings.push(answers[k]); 220 | } 221 | } 222 | 223 | var buttons = jQuery('
    '); 224 | buttons.html(jQuery.map(answerStrings, function(v) { 225 | return ""; 226 | }).join(' ')); 227 | 228 | jQuery('input[type=button]', buttons).click(function() { 229 | var clicked = this; 230 | Boxy.get(this).hide(function() { 231 | if (callback) callback(map[clicked.value]); 232 | }); 233 | }); 234 | 235 | body.append(buttons); 236 | 237 | new Boxy(body, options); 238 | 239 | }, 240 | 241 | // returns true if a modal boxy is visible, false otherwise 242 | isModalVisible: function() { 243 | return jQuery('.boxy-modal-blackout').length > 0; 244 | }, 245 | 246 | _u: function() { 247 | for (var i = 0; i < arguments.length; i++) 248 | if (typeof arguments[i] != 'undefined') return false; 249 | return true; 250 | }, 251 | 252 | _handleResize: function(evt) { 253 | var d = jQuery(document); 254 | jQuery('.boxy-modal-blackout').css('display', 'none').css({ 255 | width: d.width(), height: d.height() 256 | }).css('display', 'block'); 257 | }, 258 | 259 | _handleDrag: function(evt) { 260 | var d; 261 | if (d = Boxy.dragging) { 262 | d[0].boxy.css({left: evt.pageX - d[1], top: evt.pageY - d[2]}); 263 | } 264 | }, 265 | 266 | _nextZ: function() { 267 | return Boxy.zIndex++; 268 | }, 269 | 270 | _viewport: function() { 271 | var d = document.documentElement, b = document.body, w = window; 272 | return jQuery.extend( 273 | jQuery.browser.msie ? 274 | { left: b.scrollLeft || d.scrollLeft, top: b.scrollTop || d.scrollTop } : 275 | { left: w.pageXOffset, top: w.pageYOffset }, 276 | !Boxy._u(w.innerWidth) ? 277 | { width: w.innerWidth, height: w.innerHeight } : 278 | (!Boxy._u(d) && !Boxy._u(d.clientWidth) && d.clientWidth != 0 ? 279 | { width: d.clientWidth, height: d.clientHeight } : 280 | { width: b.clientWidth, height: b.clientHeight }) ); 281 | } 282 | 283 | }); 284 | 285 | Boxy.prototype = { 286 | 287 | // Returns the size of this boxy instance without displaying it. 288 | // Do not use this method if boxy is already visible, use getSize() instead. 289 | estimateSize: function() { 290 | this.boxy.css({visibility: 'hidden', display: 'block'}); 291 | var dims = this.getSize(); 292 | this.boxy.css('display', 'none').css('visibility', 'visible'); 293 | return dims; 294 | }, 295 | 296 | // Returns the dimensions of the entire boxy dialog as [width,height] 297 | getSize: function() { 298 | return [this.boxy.width(), this.boxy.height()]; 299 | }, 300 | 301 | // Returns the dimensions of the content region as [width,height] 302 | getContentSize: function() { 303 | var c = this.getContent(); 304 | return [c.width(), c.height()]; 305 | }, 306 | 307 | // Returns the position of this dialog as [x,y] 308 | getPosition: function() { 309 | var b = this.boxy[0]; 310 | return [b.offsetLeft, b.offsetTop]; 311 | }, 312 | 313 | // Returns the center point of this dialog as [x,y] 314 | getCenter: function() { 315 | var p = this.getPosition(); 316 | var s = this.getSize(); 317 | return [Math.floor(p[0] + s[0] / 2), Math.floor(p[1] + s[1] / 2)]; 318 | }, 319 | 320 | // Returns a jQuery object wrapping the inner boxy region. 321 | // Not much reason to use this, you're probably more interested in getContent() 322 | getInner: function() { 323 | return jQuery('.boxy-inner', this.boxy); 324 | }, 325 | 326 | // Returns a jQuery object wrapping the boxy content region. 327 | // This is the user-editable content area (i.e. excludes titlebar) 328 | getContent: function() { 329 | return jQuery('.boxy-content', this.boxy); 330 | }, 331 | 332 | // Replace dialog content 333 | setContent: function(newContent) { 334 | newContent = jQuery(newContent).css({display: 'block'}).addClass('boxy-content'); 335 | if (this.options.clone) newContent = newContent.clone(true); 336 | this.getContent().remove(); 337 | this.getInner().append(newContent); 338 | this._setupDefaultBehaviours(newContent); 339 | this.options.behaviours.call(this, newContent); 340 | return this; 341 | }, 342 | 343 | // Move this dialog to some position, funnily enough 344 | moveTo: function(x, y) { 345 | this.moveToX(x).moveToY(y); 346 | return this; 347 | }, 348 | 349 | // Move this dialog (x-coord only) 350 | moveToX: function(x) { 351 | if (typeof x == 'number') this.boxy.css({left: x}); 352 | else this.centerX(); 353 | return this; 354 | }, 355 | 356 | // Move this dialog (y-coord only) 357 | moveToY: function(y) { 358 | if (typeof y == 'number') this.boxy.css({top: y}); 359 | else this.centerY(); 360 | return this; 361 | }, 362 | 363 | // Move this dialog so that it is centered at (x,y) 364 | centerAt: function(x, y) { 365 | var s = this[this.visible ? 'getSize' : 'estimateSize'](); 366 | if (typeof x == 'number') this.moveToX(x - s[0] / 2); 367 | if (typeof y == 'number') this.moveToY(y - s[1] / 2); 368 | return this; 369 | }, 370 | 371 | centerAtX: function(x) { 372 | return this.centerAt(x, null); 373 | }, 374 | 375 | centerAtY: function(y) { 376 | return this.centerAt(null, y); 377 | }, 378 | 379 | // Center this dialog in the viewport 380 | // axis is optional, can be 'x', 'y'. 381 | center: function(axis) { 382 | var v = Boxy._viewport(); 383 | var o = this.options.fixed ? [0, 0] : [v.left, v.top]; 384 | if (!axis || axis == 'x') this.centerAt(o[0] + v.width / 2, null); 385 | if (!axis || axis == 'y') this.centerAt(null, o[1] + v.height / 2); 386 | return this; 387 | }, 388 | 389 | // Center this dialog in the viewport (x-coord only) 390 | centerX: function() { 391 | return this.center('x'); 392 | }, 393 | 394 | // Center this dialog in the viewport (y-coord only) 395 | centerY: function() { 396 | return this.center('y'); 397 | }, 398 | 399 | // Resize the content region to a specific size 400 | resize: function(width, height, after) { 401 | if (!this.visible) return; 402 | var bounds = this._getBoundsForResize(width, height); 403 | this.boxy.css({left: bounds[0], top: bounds[1]}); 404 | this.getContent().css({width: bounds[2], height: bounds[3]}); 405 | if (after) after(this); 406 | return this; 407 | }, 408 | 409 | // Tween the content region to a specific size 410 | tween: function(width, height, after) { 411 | if (!this.visible) return; 412 | var bounds = this._getBoundsForResize(width, height); 413 | var self = this; 414 | this.boxy.stop().animate({left: bounds[0], top: bounds[1]}); 415 | this.getContent().stop().animate({width: bounds[2], height: bounds[3]}, function() { 416 | if (after) after(self); 417 | }); 418 | return this; 419 | }, 420 | 421 | // Returns true if this dialog is visible, false otherwise 422 | isVisible: function() { 423 | return this.visible; 424 | }, 425 | 426 | // Make this boxy instance visible 427 | show: function() { 428 | if (this.visible) return; 429 | if (this.options.modal) { 430 | var self = this; 431 | if (!Boxy.resizeConfigured) { 432 | Boxy.resizeConfigured = true; 433 | jQuery(window).resize(function() { Boxy._handleResize(); }); 434 | } 435 | this.modalBlackout = jQuery('
    ') 436 | .css({zIndex: Boxy._nextZ(), 437 | opacity: 0.7, 438 | width: jQuery(document).width(), 439 | height: jQuery(document).height()}) 440 | .appendTo(document.body); 441 | this.toTop(); 442 | if (this.options.closeable) { 443 | jQuery(document.body).bind('keypress.boxy', function(evt) { 444 | var key = evt.which || evt.keyCode; 445 | if (key == 27) { 446 | self.hide(); 447 | jQuery(document.body).unbind('keypress.boxy'); 448 | } 449 | }); 450 | } 451 | } 452 | this.boxy.stop().css({opacity: 1}).show(); 453 | this.visible = true; 454 | this._fire('afterShow'); 455 | return this; 456 | }, 457 | 458 | // Hide this boxy instance 459 | hide: function(after) { 460 | if (!this.visible) return; 461 | var self = this; 462 | if (this.options.modal) { 463 | jQuery(document.body).unbind('keypress.boxy'); 464 | this.modalBlackout.animate({opacity: 0}, function() { 465 | jQuery(this).remove(); 466 | }); 467 | } 468 | this.boxy.stop().animate({opacity: 0}, 300, function() { 469 | self.boxy.css({display: 'none'}); 470 | self.visible = false; 471 | self._fire('afterHide'); 472 | if (after) after(self); 473 | if (self.options.unloadOnHide) self.unload(); 474 | }); 475 | return this; 476 | }, 477 | 478 | toggle: function() { 479 | this[this.visible ? 'hide' : 'show'](); 480 | return this; 481 | }, 482 | 483 | hideAndUnload: function(after) { 484 | this.options.unloadOnHide = true; 485 | this.hide(after); 486 | return this; 487 | }, 488 | 489 | unload: function() { 490 | this._fire('beforeUnload'); 491 | this.boxy.remove(); 492 | if (this.options.actuator) { 493 | jQuery.data(this.options.actuator, 'active.boxy', false); 494 | } 495 | }, 496 | 497 | // Move this dialog box above all other boxy instances 498 | toTop: function() { 499 | this.boxy.css({zIndex: Boxy._nextZ()}); 500 | return this; 501 | }, 502 | 503 | // Returns the title of this dialog 504 | getTitle: function() { 505 | return jQuery('> .title-bar h2', this.getInner()).html(); 506 | }, 507 | 508 | // Sets the title of this dialog 509 | setTitle: function(t) { 510 | jQuery('> .title-bar h2', this.getInner()).html(t); 511 | return this; 512 | }, 513 | 514 | // 515 | // Don't touch these privates 516 | 517 | _getBoundsForResize: function(width, height) { 518 | var csize = this.getContentSize(); 519 | var delta = [width - csize[0], height - csize[1]]; 520 | var p = this.getPosition(); 521 | return [Math.max(p[0] - delta[0] / 2, 0), 522 | Math.max(p[1] - delta[1] / 2, 0), width, height]; 523 | }, 524 | 525 | _setupTitleBar: function() { 526 | if (this.options.title) { 527 | var self = this; 528 | var tb = jQuery("
    ").html("

    " + this.options.title + "

    "); 529 | if (this.options.closeable) { 530 | tb.append(jQuery("").html(this.options.closeText)); 531 | } 532 | if (this.options.draggable) { 533 | tb[0].onselectstart = function() { return false; } 534 | tb[0].unselectable = 'on'; 535 | tb[0].style.MozUserSelect = 'none'; 536 | if (!Boxy.dragConfigured) { 537 | jQuery(document).mousemove(Boxy._handleDrag); 538 | Boxy.dragConfigured = true; 539 | } 540 | tb.mousedown(function(evt) { 541 | self.toTop(); 542 | Boxy.dragging = [self, evt.pageX - self.boxy[0].offsetLeft, evt.pageY - self.boxy[0].offsetTop]; 543 | jQuery(this).addClass('dragging'); 544 | }).mouseup(function() { 545 | jQuery(this).removeClass('dragging'); 546 | Boxy.dragging = null; 547 | self._fire('afterDrop'); 548 | }); 549 | } 550 | this.getInner().prepend(tb); 551 | this._setupDefaultBehaviours(tb); 552 | } 553 | }, 554 | 555 | _setupDefaultBehaviours: function(root) { 556 | var self = this; 557 | if (this.options.clickToFront) { 558 | root.click(function() { self.toTop(); }); 559 | } 560 | jQuery('.close', root).click(function() { 561 | self.hide(); 562 | return false; 563 | }).mousedown(function(evt) { evt.stopPropagation(); }); 564 | }, 565 | 566 | _fire: function(event) { 567 | this.options[event].call(this); 568 | } 569 | 570 | }; 571 | --------------------------------------------------------------------------------