radiant-page_factory-extension 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 = {};