radiant-page_factory-extension 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/EXAMPLES.md +136 -0
- data/README.md +59 -0
- data/Rakefile +136 -0
- data/VERSION +1 -0
- data/app/controllers/admin/page_factories_controller.rb +42 -0
- data/app/helpers/admin/part_description_helper.rb +9 -0
- data/app/views/admin/page_factories/_page_factory.html.haml +4 -0
- data/app/views/admin/page_factories/index.haml +0 -0
- data/app/views/admin/page_parts/_part_description.html.haml +2 -0
- data/app/views/admin/pages/_add_child_column.html.haml +3 -0
- data/app/views/admin/pages/_edit_header.html.haml +1 -0
- data/app/views/admin/pages/_page_factories.html.haml +4 -0
- data/app/views/admin/pages/_page_factory_field.html.haml +1 -0
- data/config/locales/en.yml +3 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20100321222140_add_page_factory.rb +9 -0
- data/lib/page_factory.rb +12 -0
- data/lib/page_factory/base.rb +81 -0
- data/lib/page_factory/manager.rb +120 -0
- data/lib/page_factory/page_extensions.rb +25 -0
- data/lib/page_factory/page_part_extensions.rb +9 -0
- data/lib/page_factory/pages_controller_extensions.rb +32 -0
- data/lib/tasks/page_factory_extension_tasks.rake +95 -0
- data/page_factory_extension.rb +25 -0
- data/public/javascripts/admin/dropdown.js +153 -0
- data/public/javascripts/admin/pagefactory.js +30 -0
- data/public/stylesheets/admin/page_factory.css +14 -0
- data/public/stylesheets/sass/admin/dropdown.sass +32 -0
- data/public/stylesheets/sass/modules/_gradient.sass +47 -0
- data/public/stylesheets/sass/modules/_rounded.sass +41 -0
- data/public/stylesheets/sass/modules/_shadow.sass +9 -0
- data/spec/controllers/admin/page_factories_controller_spec.rb +47 -0
- data/spec/controllers/admin/pages_controller_spec.rb +63 -0
- data/spec/helpers/admin/part_description_helper_spec.rb +42 -0
- data/spec/lib/manager_spec.rb +218 -0
- data/spec/lib/page_extensions_spec.rb +15 -0
- data/spec/models/page_factory_spec.rb +95 -0
- data/spec/models/page_spec.rb +27 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +36 -0
- metadata +128 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
class PageFactory::Base
|
2
|
+
include Annotatable
|
3
|
+
annotate :template_name, :layout, :page_class, :description
|
4
|
+
template_name 'Page'
|
5
|
+
description 'A basic Radiant page.'
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :parts
|
9
|
+
|
10
|
+
def inherited(subclass)
|
11
|
+
subclass.parts = @parts.dup
|
12
|
+
subclass.layout = layout
|
13
|
+
subclass.page_class = page_class
|
14
|
+
subclass.template_name = subclass.name.to_name('Factory')
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# Add a part to this PageFactory
|
19
|
+
#
|
20
|
+
# @param [String] name The name of the page part
|
21
|
+
# @param [Hash] attrs A hash of attributes used to construct this part.
|
22
|
+
# @option attrs [String] :description Some additional text that will be
|
23
|
+
# shown in the part's tab on the page editing screen. This is used to
|
24
|
+
# display a part description or helper text to editors.
|
25
|
+
#
|
26
|
+
# @example Add a part with default content and some help text
|
27
|
+
# part 'Sidebar', :content => "Lorem ipsum dolor",
|
28
|
+
# :description => "This appears in the right-hand sidebar."
|
29
|
+
def part(name, attrs={})
|
30
|
+
remove name
|
31
|
+
@parts << PagePart.new(attrs.merge(:name => name))
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Remove a part from this PageFactory
|
36
|
+
#
|
37
|
+
# @param [<String>] names Any number of part names to remove.
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# remove 'body'
|
41
|
+
# remove 'body', 'extended'
|
42
|
+
def remove(*names)
|
43
|
+
names = names.map(&:downcase)
|
44
|
+
@parts.delete_if { |p| names.include? p.name.downcase }
|
45
|
+
end
|
46
|
+
|
47
|
+
def descendants
|
48
|
+
load_descendants
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def default_page_parts(config = Radiant::Config)
|
54
|
+
default_parts = config['defaults.page.parts'].to_s.strip.split(/\s*,\s*/)
|
55
|
+
default_parts.map do |name|
|
56
|
+
PagePart.new(:name => name, :filter_id => config['defaults.page.filter'])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def load_descendants
|
61
|
+
unless @_descendants_loaded
|
62
|
+
factory_paths = Radiant::Extension.descendants.inject [Rails.root.to_s + '/lib'] do |paths, ext|
|
63
|
+
paths << ext.root + '/app/models'
|
64
|
+
paths << ext.root + '/lib'
|
65
|
+
end
|
66
|
+
factory_paths.each do |path|
|
67
|
+
Dir["#{path}/*_page_factory.rb"].each do |page_factory|
|
68
|
+
if page_factory =~ %r{/([^/]+)\.rb}
|
69
|
+
require_dependency page_factory
|
70
|
+
ActiveSupport::Dependencies.explicitly_unloadable_constants << $1.camelize
|
71
|
+
end
|
72
|
+
end
|
73
|
+
@_descendants_loaded = true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
@parts = default_page_parts
|
81
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module PageFactory
|
2
|
+
##
|
3
|
+
# PageFactory::Manager is used to update your existing content with changes
|
4
|
+
# subsequently made to your PageFactories. All of these methods take a single
|
5
|
+
# optional argument, which should be the name of a PageFactory class.
|
6
|
+
#
|
7
|
+
# If no argument is given, the method is run for all PageFactories.
|
8
|
+
# Plain old pages not created with a specific factory are never affected in
|
9
|
+
# this case. If the name of a PageFactory is given, the method is only run
|
10
|
+
# on pages that were initially created by the specified PageFactory.
|
11
|
+
#
|
12
|
+
# Note that it is possible to pass 'page' as an argument, if you really
|
13
|
+
# need to update pages that were created without a specific factory.
|
14
|
+
class Manager
|
15
|
+
class << self
|
16
|
+
|
17
|
+
##
|
18
|
+
# Remove parts not specified in a PageFactory from all pages initially
|
19
|
+
# created by that PageFactory. This is useful if you decide to remove
|
20
|
+
# a part from a PageFactory and you want your existing content to
|
21
|
+
# reflect that change.
|
22
|
+
#
|
23
|
+
# @param [nil, String, Symbol, #to_s] page_factory The PageFactory to
|
24
|
+
# restrict this operation to, or nil to run it on all PageFactories.
|
25
|
+
def prune_parts!(page_factory=nil)
|
26
|
+
select_factories(page_factory).each do |factory|
|
27
|
+
parts = PagePart.scoped(:include => :page).
|
28
|
+
scoped(:conditions => {'pages.page_factory' => name_for(factory)}).
|
29
|
+
scoped(:conditions => ['page_parts.name NOT IN (?)', factory.parts.map(&:name)])
|
30
|
+
PagePart.destroy parts
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Add any parts defined in a PageFactory to all pages initially created
|
36
|
+
# by that factory, if those pages are missing any parts. This can be
|
37
|
+
# used when you've added a part to a factory and you want your existing
|
38
|
+
# content to reflect that change.
|
39
|
+
#
|
40
|
+
# @param [nil, String, Symbol, #to_s] page_factory The PageFactory to
|
41
|
+
# restrict this operation to, or nil to run it on all PageFactories.
|
42
|
+
def update_parts(page_factory=nil)
|
43
|
+
select_factories(page_factory).each do |factory|
|
44
|
+
Page.find(:all, :include => :parts, :conditions => {:page_factory => name_for(factory)}).each do |page|
|
45
|
+
existing = lambda { |f| page.parts.detect { |p| f.name.downcase == p.name.downcase } }
|
46
|
+
page.parts.create factory.parts.reject(&existing).map(&:attributes)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Replace any parts on a page that share a _name_ but not a _class_ with
|
53
|
+
# the parts defined in its PageFactory. Mismatched parts will be
|
54
|
+
# replaced with wholly new parts of the proper class -- this method
|
55
|
+
# _will_ discard content. Unless you're using an extension that
|
56
|
+
# subclasses PagePart (this is rare) you won't need this method.
|
57
|
+
#
|
58
|
+
# @param [nil, String, Symbol, #to_s] page_factory The PageFactory to
|
59
|
+
# restrict this operation to, or nil to run it on all PageFactories.
|
60
|
+
def sync_parts!(page_factory=nil)
|
61
|
+
select_factories(page_factory).each do |factory|
|
62
|
+
Page.find(:all, :include => :parts, :conditions => {:page_factory => name_for(factory)}).each do |page|
|
63
|
+
unsynced = lambda { |p| factory.parts.detect { |f| f.name.downcase == p.name.downcase and f.class != p.class } }
|
64
|
+
unsynced_parts = page.parts.select(&unsynced)
|
65
|
+
page.parts.destroy unsynced_parts
|
66
|
+
needs_update = lambda { |f| unsynced_parts.map(&:name).include? f.name }
|
67
|
+
page.parts.create factory.parts.select(&needs_update).map &:attributes
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Update the layout of all pages initially created by a PageFactory to
|
74
|
+
# match the layout currently specified on that PageFactory. Used when
|
75
|
+
# you decide to use a new layout in a PageFactory and you want your
|
76
|
+
# existing content to reflect that change.
|
77
|
+
#
|
78
|
+
# @param [nil, String, Symbol, #to_s] page_factory The PageFactory to
|
79
|
+
# restrict this operation to, or nil to run it on all PageFactories.
|
80
|
+
def sync_layouts!(page_factory=nil)
|
81
|
+
select_factories(page_factory).each do |factory|
|
82
|
+
Page.update_all({:layout_id => Layout.find_by_name(factory.layout, :select => :id).try(:id)}, {:page_factory => name_for(factory)})
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Update the Page class of all pages initially created by a PageFactory
|
88
|
+
# to match the class currently specified on that PageFactory. Useful
|
89
|
+
# when you assign a new page class to a PageFactory and you want your
|
90
|
+
# existing content to reflect that change.
|
91
|
+
#
|
92
|
+
# @param [nil, String, Symbol, #to_s] page_factory The PageFactory to
|
93
|
+
# restrict this operation to, or nil to run it on all PageFactories.
|
94
|
+
def sync_classes!(page_factory=nil)
|
95
|
+
select_factories(page_factory).each do |factory|
|
96
|
+
Page.update_all({:class_name => factory.page_class}, {:page_factory => name_for(factory)})
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def select_factories(page_factory)
|
103
|
+
[PageFactory::Base, *PageFactory::Base.descendants].select do |klass|
|
104
|
+
case page_factory
|
105
|
+
when '', nil
|
106
|
+
klass.name != 'PageFactory::Base'
|
107
|
+
when 'PageFactory', :PageFactory
|
108
|
+
klass.name == 'PageFactory::Base'
|
109
|
+
else
|
110
|
+
klass.name == page_factory.to_s.camelcase
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def name_for(factory)
|
116
|
+
factory == PageFactory::Base ? nil : factory.name
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module PageFactory
|
2
|
+
module PageExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.instance_eval do
|
5
|
+
def default_page_parts(config=Radiant::Config)
|
6
|
+
PageFactory.current_factory.parts
|
7
|
+
end
|
8
|
+
private_class_method :default_page_parts
|
9
|
+
end
|
10
|
+
base.class_eval do
|
11
|
+
##
|
12
|
+
# The PageFactory that was used to create this page. Note that Plain
|
13
|
+
# Old Pages do not have an assigned factory.
|
14
|
+
#
|
15
|
+
# @return [PageFactory, nil] This Page's initial PageFactory
|
16
|
+
def page_factory
|
17
|
+
(factory = read_attribute(:page_factory)).blank? ? nil : factory.constantize
|
18
|
+
rescue NameError => e # @page.page_factory is not a constant. class was removed?
|
19
|
+
logger.warn "Couldn't find page factory: #{e.message}"
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PageFactory
|
2
|
+
module PagesControllerExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
around_filter :set_page_factory, :only => :new
|
6
|
+
before_filter { |c| c.include_stylesheet 'admin/dropdown' }
|
7
|
+
before_filter { |c| c.include_javascript 'admin/dropdown' }
|
8
|
+
before_filter { |c| c.include_javascript 'admin/pagefactory' }
|
9
|
+
responses do |r|
|
10
|
+
r.singular.default { set_page_defaults if 'new' == action_name }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_page_factory
|
16
|
+
begin
|
17
|
+
PageFactory.current_factory = params[:factory]
|
18
|
+
rescue NameError => e # bad factory name passed
|
19
|
+
logger.error "Tried to create page with invalid factory: #{e.message}"
|
20
|
+
ensure
|
21
|
+
yield
|
22
|
+
PageFactory.current_factory = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_page_defaults
|
27
|
+
model.class_name = PageFactory.current_factory.page_class
|
28
|
+
model.layout = Layout.find_by_name(PageFactory.current_factory.layout)
|
29
|
+
model.page_factory = PageFactory.current_factory.name unless PageFactory::Base == PageFactory.current_factory
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
namespace :radiant do
|
2
|
+
namespace :extensions do
|
3
|
+
namespace :page_factory do
|
4
|
+
|
5
|
+
namespace :refresh do
|
6
|
+
def factory_class(factory)
|
7
|
+
factory_class = case factory
|
8
|
+
when '', nil : nil
|
9
|
+
when 'page', 'Page' : 'PageFactory'
|
10
|
+
else factory.capitalize + 'PageFactory'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
task :update_parts, :factory, :needs => :environment do |task, args|
|
14
|
+
updated = PageFactory::Manager.update_parts factory_class(args[:factory])
|
15
|
+
puts "Added missing parts from #{updated.join(', ')}"
|
16
|
+
end
|
17
|
+
task :prune_parts, :factory, :needs => :environment do |task, args|
|
18
|
+
updated = PageFactory::Manager.prune_parts! factory_class(args[:factory])
|
19
|
+
puts "Removed extra parts from #{updated.join(', ')}"
|
20
|
+
end
|
21
|
+
task :sync_parts, :factory, :needs => :environment do |task, args|
|
22
|
+
updated = PageFactory::Manager.sync_parts! factory_class(args[:factory])
|
23
|
+
puts "Synchronized part classes on #{updated.join(', ')}"
|
24
|
+
end
|
25
|
+
task :sync_layouts, :factory, :needs => :environment do |task, args|
|
26
|
+
updated = PageFactory::Manager.sync_layouts! factory_class(args[:factory])
|
27
|
+
puts "Synchronized layouts on #{updated.join(', ')}"
|
28
|
+
end
|
29
|
+
task :sync_classes, :factory, :needs => :environment do |task, args| factory_class(args[:factory])
|
30
|
+
updated = PageFactory::Manager.sync_classes! factory_class(args[:factory])
|
31
|
+
puts "Synchronized page classes on #{updated.join(', ')}"
|
32
|
+
end
|
33
|
+
desc "Add missing page parts, but don't change or remove any data."
|
34
|
+
task :soft, :factory, :needs => :environment do |task, args|
|
35
|
+
Rake::Task['radiant:extensions:page_factory:refresh:update_parts'].invoke args[:factory]
|
36
|
+
end
|
37
|
+
desc "Make pages look exactly like their factory definitions, including layout and page class."
|
38
|
+
task :hard, :factory, :needs => :environment do |task, args|
|
39
|
+
Rake::Task['radiant:extensions:page_factory:refresh:prune_parts'].invoke args[:factory]
|
40
|
+
Rake::Task['radiant:extensions:page_factory:refresh:sync_parts'].invoke args[:factory]
|
41
|
+
Rake::Task['radiant:extensions:page_factory:refresh:update_parts'].invoke args[:factory]
|
42
|
+
Rake::Task['radiant:extensions:page_factory:refresh:sync_layouts'].invoke args[:factory]
|
43
|
+
Rake::Task['radiant:extensions:page_factory:refresh:sync_classes'].invoke args[:factory]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "Runs the migration of the Page Factory extension"
|
48
|
+
task :migrate => :environment do
|
49
|
+
require 'radiant/extension_migrator'
|
50
|
+
if ENV["VERSION"]
|
51
|
+
PageFactoryExtension.migrator.migrate(ENV["VERSION"].to_i)
|
52
|
+
else
|
53
|
+
PageFactoryExtension.migrator.migrate
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "Copies public assets of the Page Factory to the instance public/ directory."
|
58
|
+
task :update => :environment do
|
59
|
+
is_svn_or_dir = proc {|path| path =~ /\.svn/ || File.directory?(path) }
|
60
|
+
puts "Copying assets from PageFactoryExtension"
|
61
|
+
Dir[PageFactoryExtension.root + "/public/**/*"].reject(&is_svn_or_dir).each do |file|
|
62
|
+
path = file.sub(PageFactoryExtension.root, '')
|
63
|
+
directory = File.dirname(path)
|
64
|
+
mkdir_p RAILS_ROOT + directory, :verbose => false
|
65
|
+
cp file, RAILS_ROOT + path, :verbose => false
|
66
|
+
end
|
67
|
+
unless PageFactoryExtension.root.starts_with? RAILS_ROOT # don't need to copy vendored tasks
|
68
|
+
puts "Copying rake tasks from PageFactoryExtension"
|
69
|
+
local_tasks_path = File.join(RAILS_ROOT, %w(lib tasks))
|
70
|
+
mkdir_p local_tasks_path, :verbose => false
|
71
|
+
Dir[File.join PageFactoryExtension.root, %w(lib tasks *.rake)].each do |file|
|
72
|
+
cp file, local_tasks_path, :verbose => false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
desc "Syncs all available translations for this ext to the English ext master"
|
78
|
+
task :sync => :environment do
|
79
|
+
# The main translation root, basically where English is kept
|
80
|
+
language_root = PageFactoryExtension.root + "/config/locales"
|
81
|
+
words = TranslationSupport.get_translation_keys(language_root)
|
82
|
+
|
83
|
+
Dir["#{language_root}/*.yml"].each do |filename|
|
84
|
+
next if filename.match('_available_tags')
|
85
|
+
basename = File.basename(filename, '.yml')
|
86
|
+
puts "Syncing #{basename}"
|
87
|
+
(comments, other) = TranslationSupport.read_file(filename, basename)
|
88
|
+
words.each { |k,v| other[k] ||= words[k] } # Initializing hash variable as empty if it does not exist
|
89
|
+
other.delete_if { |k,v| !words[k] } # Remove if not defined in en.yml
|
90
|
+
TranslationSupport.write_file(filename, basename, comments, other)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_dependency 'application_controller'
|
2
|
+
|
3
|
+
class PageFactoryExtension < Radiant::Extension
|
4
|
+
version "0.1"
|
5
|
+
description "A small DSL for intelligently defining content types."
|
6
|
+
url "http://github.com/joshfrench/radiant-page_factory-extension"
|
7
|
+
|
8
|
+
define_routes do |map|
|
9
|
+
map.namespace :admin do |admin|
|
10
|
+
admin.factory_link '/pages/factories', :controller => 'page_factories', :action => 'index'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def activate
|
15
|
+
Page.send :include, PageFactory::PageExtensions
|
16
|
+
PagePart.send :include, PageFactory::PagePartExtensions
|
17
|
+
Admin::PagesController.send :include, PageFactory::PagesControllerExtensions
|
18
|
+
Admin::PagesController.helper 'admin/part_description'
|
19
|
+
Admin::PagePartsController.helper 'admin/part_description'
|
20
|
+
admin.pages.new.add :form, 'page_factory_field'
|
21
|
+
admin.pages.edit.add :part_controls, 'admin/page_parts/part_description'
|
22
|
+
admin.pages.index.add :bottom, 'admin/pages/page_factories'
|
23
|
+
ActiveSupport::Dependencies.load_paths << File.join(Rails.root, 'lib')
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
/*
|
2
|
+
* dropdown.js
|
3
|
+
*
|
4
|
+
* dependencies: prototype.js, effects.js, lowpro.js
|
5
|
+
*
|
6
|
+
* --------------------------------------------------------------------------
|
7
|
+
*
|
8
|
+
* Allows you to easily create a dropdown menu item. Simply create a link
|
9
|
+
* with a class of "dropdown" that references the ID of the list that you
|
10
|
+
* would like to use as a dropdown menu.
|
11
|
+
*
|
12
|
+
* A link like this:
|
13
|
+
*
|
14
|
+
* <a class="dropdown" href="#dropdown">Menu</a>
|
15
|
+
*
|
16
|
+
* will dropdown a list of choices in the list with the ID of "dropdown".
|
17
|
+
*
|
18
|
+
* You will need to install the following hook:
|
19
|
+
*
|
20
|
+
* Event.addBehavior({'a.dropdown': Dropdown.TriggerBehavior()});
|
21
|
+
*
|
22
|
+
* --------------------------------------------------------------------------
|
23
|
+
*
|
24
|
+
* Copyright (c) 2010, John W. Long
|
25
|
+
*
|
26
|
+
* Permission is hereby granted, free of charge, to any person obtaining a
|
27
|
+
* copy of this software and associated documentation files (the "Software"),
|
28
|
+
* to deal in the Software without restriction, including without limitation
|
29
|
+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
30
|
+
* and/or sell copies of the Software, and to permit persons to whom the
|
31
|
+
* Software is furnished to do so, subject to the following conditions:
|
32
|
+
*
|
33
|
+
* The above copyright notice and this permission notice shall be included in
|
34
|
+
* all copies or substantial portions of the Software.
|
35
|
+
*
|
36
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
37
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
38
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
39
|
+
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
40
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
41
|
+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
42
|
+
* DEALINGS IN THE SOFTWARE.
|
43
|
+
*
|
44
|
+
*/
|
45
|
+
|
46
|
+
var Dropdown = {
|
47
|
+
|
48
|
+
DefaultPosition: 'bottom',
|
49
|
+
|
50
|
+
DefaultEffect: 'slide',
|
51
|
+
DefaultEffectDuration: 0.1,
|
52
|
+
|
53
|
+
EffectPairs: {
|
54
|
+
'slide' : ['SlideDown', 'SlideUp'],
|
55
|
+
'blind' : ['BlindDown', 'BlindUp'],
|
56
|
+
'appear': ['Appear', 'Fade']
|
57
|
+
}
|
58
|
+
|
59
|
+
};
|
60
|
+
|
61
|
+
Dropdown.TriggerBehavior = Behavior.create({
|
62
|
+
initialize: function(options) {
|
63
|
+
var options = options || {};
|
64
|
+
options.position = (options.position || Dropdown.DefaultPosition).toLowerCase();
|
65
|
+
options.effect = (options.effect || Dropdown.DefaultEffect).toLowerCase();
|
66
|
+
options.duration = (options.duration || Dropdown.DefaultEffectDuration);
|
67
|
+
this.options = options;
|
68
|
+
|
69
|
+
var matches = this.element.href.match(/\#(.+)$/);
|
70
|
+
if (matches) this.menu = Dropdown.Menu.findOrCreate(matches[1]);
|
71
|
+
},
|
72
|
+
|
73
|
+
onclick: function(event) {
|
74
|
+
if (this.menu) this.menu.toggle(this.element, this.options);
|
75
|
+
event.stop();
|
76
|
+
}
|
77
|
+
});
|
78
|
+
|
79
|
+
Dropdown.Menu = Class.create({
|
80
|
+
|
81
|
+
initialize: function(element) {
|
82
|
+
element.remove();
|
83
|
+
this.element = element;
|
84
|
+
this.wrapper = $div({'class': 'dropdown_wrapper', 'style': 'position: absolute; display: none'}, element);
|
85
|
+
document.body.insert(this.wrapper);
|
86
|
+
},
|
87
|
+
|
88
|
+
open: function(trigger, options) {
|
89
|
+
this.wrapper.hide();
|
90
|
+
trigger.addClassName('selected');
|
91
|
+
this.position(trigger, options);
|
92
|
+
var name = options.effect;
|
93
|
+
var effect = Effect[Dropdown.EffectPairs[name][0]];
|
94
|
+
effect(this.wrapper, {duration: options.duration});
|
95
|
+
},
|
96
|
+
|
97
|
+
close: function(trigger, options) {
|
98
|
+
var name = options.effect;
|
99
|
+
var effect = Effect[Dropdown.EffectPairs[name][1]];
|
100
|
+
effect(this.wrapper, {duration: options.duration});
|
101
|
+
trigger.removeClassName('selected');
|
102
|
+
},
|
103
|
+
|
104
|
+
toggle: function(trigger, options) {
|
105
|
+
if (this.lastTrigger == trigger) {
|
106
|
+
if (this.wrapper.visible()) {
|
107
|
+
this.close(trigger, options);
|
108
|
+
} else {
|
109
|
+
this.open(trigger, options);
|
110
|
+
}
|
111
|
+
} else {
|
112
|
+
if (this.lastTrigger) this.lastTrigger.removeClassName('selected');
|
113
|
+
this.open(trigger, options);
|
114
|
+
}
|
115
|
+
this.lastTrigger = trigger;
|
116
|
+
},
|
117
|
+
|
118
|
+
position: function(trigger, options) {
|
119
|
+
switch(options.position) {
|
120
|
+
case 'top': this.positionTop(trigger); break;
|
121
|
+
case 'bottom': this.positionBottom(trigger); break;
|
122
|
+
default: this.positionBottom(trigger);
|
123
|
+
}
|
124
|
+
},
|
125
|
+
|
126
|
+
positionTop: function(trigger) {
|
127
|
+
var offset = trigger.cumulativeOffset();
|
128
|
+
var height = this.wrapper.getHeight();
|
129
|
+
this.wrapper.setStyle({
|
130
|
+
left: offset.left + 'px',
|
131
|
+
top: (offset.top - height) + 'px'
|
132
|
+
});
|
133
|
+
},
|
134
|
+
|
135
|
+
positionBottom: function(trigger) {
|
136
|
+
var offset = trigger.cumulativeOffset();
|
137
|
+
var height = trigger.getHeight();
|
138
|
+
this.wrapper.setStyle({
|
139
|
+
left: offset.left + 'px',
|
140
|
+
top: (offset.top + height) + 'px'
|
141
|
+
});
|
142
|
+
}
|
143
|
+
|
144
|
+
});
|
145
|
+
|
146
|
+
Dropdown.Menu.findOrCreate = function(element) {
|
147
|
+
var element = $(element);
|
148
|
+
var key = element.identify();
|
149
|
+
var menu = Dropdown.Menu.controls[key];
|
150
|
+
if (menu == null) menu = Dropdown.Menu.controls[key] = new Dropdown.Menu(element);
|
151
|
+
return menu;
|
152
|
+
}
|
153
|
+
Dropdown.Menu.controls = {};
|