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.
Files changed (41) hide show
  1. data/EXAMPLES.md +136 -0
  2. data/README.md +59 -0
  3. data/Rakefile +136 -0
  4. data/VERSION +1 -0
  5. data/app/controllers/admin/page_factories_controller.rb +42 -0
  6. data/app/helpers/admin/part_description_helper.rb +9 -0
  7. data/app/views/admin/page_factories/_page_factory.html.haml +4 -0
  8. data/app/views/admin/page_factories/index.haml +0 -0
  9. data/app/views/admin/page_parts/_part_description.html.haml +2 -0
  10. data/app/views/admin/pages/_add_child_column.html.haml +3 -0
  11. data/app/views/admin/pages/_edit_header.html.haml +1 -0
  12. data/app/views/admin/pages/_page_factories.html.haml +4 -0
  13. data/app/views/admin/pages/_page_factory_field.html.haml +1 -0
  14. data/config/locales/en.yml +3 -0
  15. data/config/routes.rb +5 -0
  16. data/db/migrate/20100321222140_add_page_factory.rb +9 -0
  17. data/lib/page_factory.rb +12 -0
  18. data/lib/page_factory/base.rb +81 -0
  19. data/lib/page_factory/manager.rb +120 -0
  20. data/lib/page_factory/page_extensions.rb +25 -0
  21. data/lib/page_factory/page_part_extensions.rb +9 -0
  22. data/lib/page_factory/pages_controller_extensions.rb +32 -0
  23. data/lib/tasks/page_factory_extension_tasks.rake +95 -0
  24. data/page_factory_extension.rb +25 -0
  25. data/public/javascripts/admin/dropdown.js +153 -0
  26. data/public/javascripts/admin/pagefactory.js +30 -0
  27. data/public/stylesheets/admin/page_factory.css +14 -0
  28. data/public/stylesheets/sass/admin/dropdown.sass +32 -0
  29. data/public/stylesheets/sass/modules/_gradient.sass +47 -0
  30. data/public/stylesheets/sass/modules/_rounded.sass +41 -0
  31. data/public/stylesheets/sass/modules/_shadow.sass +9 -0
  32. data/spec/controllers/admin/page_factories_controller_spec.rb +47 -0
  33. data/spec/controllers/admin/pages_controller_spec.rb +63 -0
  34. data/spec/helpers/admin/part_description_helper_spec.rb +42 -0
  35. data/spec/lib/manager_spec.rb +218 -0
  36. data/spec/lib/page_extensions_spec.rb +15 -0
  37. data/spec/models/page_factory_spec.rb +95 -0
  38. data/spec/models/page_spec.rb +27 -0
  39. data/spec/spec.opts +6 -0
  40. data/spec/spec_helper.rb +36 -0
  41. 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,9 @@
1
+ module PageFactory
2
+ module PagePartExtensions
3
+ def self.included(base)
4
+ base.class_eval do
5
+ attr_accessor :description
6
+ end
7
+ end
8
+ end
9
+ 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 = {};