navigation 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +10 -0
- data/LICENSE +20 -0
- data/README.textile +158 -0
- data/Rakefile +1 -0
- data/lib/navigation.rb +42 -0
- data/lib/navigation/base.rb +11 -0
- data/lib/navigation/builder.rb +26 -0
- data/lib/navigation/error.rb +37 -0
- data/lib/navigation/menu.rb +22 -0
- data/lib/navigation/navigator.rb +111 -0
- data/lib/navigation/nested_menu.rb +19 -0
- data/lib/navigation/version.rb +3 -0
- data/navigation.gemspec +19 -0
- data/test/navigation_test.rb +135 -0
- data/test/test_helper.rb +71 -0
- metadata +84 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Ryan Heath
|
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.
|
data/README.textile
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
h1. Navigation Plugin for Rails
|
2
|
+
|
3
|
+
This is an experiment with a different way to create navigation menus in Rails. It supports navigation at the controller level, as well as the action level (for both nested or non-nested menus).
|
4
|
+
|
5
|
+
h2. Initialization
|
6
|
+
|
7
|
+
Probably the most fundamental difference with this plugin and most navigation plugins is this one requires the menus to be pre-defined before rendering. Here's an example of a typical menu definition (in @config/initializers/navigation.rb@):
|
8
|
+
|
9
|
+
<pre><code>RPH::Navigation::Builder.config do |navigation|
|
10
|
+
navigation.define :primary do |menu|
|
11
|
+
menu.item :home, :text => 'Welcome'
|
12
|
+
menu.item :about, :path => :about_us_path
|
13
|
+
menu.item :contact, :text => 'Contact Us'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
</code></pre>
|
17
|
+
|
18
|
+
A caveat to this approach is that you don't have access to the named route helpers in an initializer, so you have to pass them as either a symbol or a string and they'll be eval'd at the right time. If you don't pass a route (i.e. the @:path@ option), it will try to do @"#{menu_item_name}_path"@. So the "home" item in the above example would get a path of "home_path" since there was no @:path@ option. And if "home_path" doesn't exist, the fallback is "root_path".
|
19
|
+
|
20
|
+
h2. Rendering the Menu
|
21
|
+
|
22
|
+
Once you have your menu(s) defined, it's pretty easy to render it:
|
23
|
+
|
24
|
+
<pre><code><%= navigation :primary %></code></pre>
|
25
|
+
|
26
|
+
You just have to pass the key of the name you gave it when it was defined. This is good for a number of reasons, but an added bonus is the ability to define and render different menus depending on who is logged in, for example.
|
27
|
+
|
28
|
+
<pre><code><%= navigation current_user.menu %></code></pre>
|
29
|
+
|
30
|
+
All of the logic related to roles/permissions could be abstracted into a @menu()@ method. Or if you're funny about sticking that logic in a model, you could call a helper or something. But you get the idea: key-based menu identification.
|
31
|
+
|
32
|
+
h2. Tips & Advanced Features
|
33
|
+
|
34
|
+
There are some additional things that you may want to know about this plugin, so keep reading if you're not satisfied yet.
|
35
|
+
|
36
|
+
h3. Passing a controller instance...
|
37
|
+
|
38
|
+
When I'm writing code to determine the "current tab" stuff, I basically just want this: when any action inside of the @HomeController@ gets rendered, highlight the "home" tab as the current. So why not allow the ability to pass the controller instance in the menu definition?
|
39
|
+
|
40
|
+
<pre><code>RPH::Navigation::Builder.config do |navigation|
|
41
|
+
navigation.define :primary do |menu|
|
42
|
+
menu.item HomeController, :text => 'Welcome'
|
43
|
+
menu.item AboutController, :path => :about_us_path
|
44
|
+
menu.item ContactController, :text => 'Contact Us'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
</code></pre>
|
48
|
+
|
49
|
+
No more guessing games and no more @current_tab :home@ in your controllers.
|
50
|
+
|
51
|
+
h3. Rule-based navigation menus...
|
52
|
+
|
53
|
+
Sometimes you only want to show a menu based on a some condition. It happens. Just pass an @:if@ condition to the menu definition:
|
54
|
+
|
55
|
+
<pre><code>RPH::Navigation::Builder.config do |navigation|
|
56
|
+
navigation.define :primary, :if => Proc.new { |view| view.allowed_to_show_menu? } do |menu|
|
57
|
+
# ...
|
58
|
+
end
|
59
|
+
end
|
60
|
+
</code></pre>
|
61
|
+
|
62
|
+
You will automatically be handed a reference to the template/view so you can call any method that @ActionView@ is aware of (Note: if you need the controller, just use @view.controller@).
|
63
|
+
|
64
|
+
h3. Rule-based navigation menu items...
|
65
|
+
|
66
|
+
And sometimes you always want the menu present, but one or two of the tabs should only show up for certain reasons. Well, the @:if@ option works exactly the same for menu items as well:
|
67
|
+
|
68
|
+
<pre><code>RPH::Navigation::Builder.config do |navigation|
|
69
|
+
navigation.define :primary do |menu|
|
70
|
+
menu.item :home, :text => 'Welcome'
|
71
|
+
menu.item :about, :path => :about_us_path
|
72
|
+
menu.item :admin, :if => Proc.new { |view| view.logged_in_as_admin? }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
</code></pre>
|
76
|
+
|
77
|
+
h3. Sub-menu navigation...
|
78
|
+
|
79
|
+
As we all know, a controller is made up of actions. And sometimes you want to not only provide navigation around your controllers, but also around the actions within a controller. Well, it's a pretty simple concept and now has a pretty simple solution. Just pass a block to the menu item:
|
80
|
+
|
81
|
+
<pre><code>RPH::Navigation::Builder.config do |navigation|
|
82
|
+
navigation.define :primary do |menu|
|
83
|
+
menu.item :home, :text => 'Welcome' do |sub_menu|
|
84
|
+
sub_menu.item :index, :text => 'Dashboard', :path => :home_path
|
85
|
+
sub_menu.item :friends, :path => :friends_path
|
86
|
+
end
|
87
|
+
menu.item :about, :path => :about_us_path
|
88
|
+
menu.item :contact, :text => 'Contact Us'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
</code></pre>
|
92
|
+
|
93
|
+
If you pass a block to any of the menu items, the plugin will automatically know that this is a sub-menu and will render a completely new menu underneath of the parent menu item. And it's "current tab" will be based on the @action_name@ instead of the @controller_name@. Remember, this is if you want a *nested* action-level menu.
|
94
|
+
|
95
|
+
h3. Action-level navigation...
|
96
|
+
|
97
|
+
If you want to build a separate menu for the actions of a controller, but you _don't_ want to nest it under a parent menu item, you can tell the builder that during the definition by passing @:action_menu => true@:
|
98
|
+
|
99
|
+
<pre><code>RPH::Navigation::Builder.config do |navigation|
|
100
|
+
navigation.define :users, :action_menu => true do |menu|
|
101
|
+
# ...
|
102
|
+
end
|
103
|
+
end
|
104
|
+
</code></pre>
|
105
|
+
|
106
|
+
h3. Multiple menus...
|
107
|
+
|
108
|
+
This isn't a _feature_ per se, but one of the great outcomes of this experiment is the ability to keep all of your menu definitions in a single ruby file and in a single builder...
|
109
|
+
|
110
|
+
<pre><code>RPH::Navigation::Builder.config do |navigation|
|
111
|
+
navigation.define :public do |menu|
|
112
|
+
# ...
|
113
|
+
end
|
114
|
+
|
115
|
+
navigation.define :authenticated do |menu|
|
116
|
+
# ...
|
117
|
+
end
|
118
|
+
|
119
|
+
navigation.define :administrator do |menu|
|
120
|
+
# ...
|
121
|
+
end
|
122
|
+
end
|
123
|
+
</code></pre>
|
124
|
+
|
125
|
+
Having key-based menu identification works out really well for additional menu logic and keeping your views clean.
|
126
|
+
|
127
|
+
h3. Helper-based custom menu text
|
128
|
+
|
129
|
+
Sometimes the text on one of your menu items depend on some conditions. Like, for instance, maybe an inbox link might also show the number of new messages? You could define a helper like so:
|
130
|
+
|
131
|
+
<pre><code>def inbox_menu_text
|
132
|
+
["Inbox", content_tag(:span, current_user.messages.unread.size]].join(" ")
|
133
|
+
end
|
134
|
+
</code></pre>
|
135
|
+
|
136
|
+
Then you can refer to that helper in the navigation definition using a Proc.
|
137
|
+
|
138
|
+
<pre><code>navigation.define :authenticated do |menu|
|
139
|
+
menu.item :messages, :text => Proc.new { |view| view.inbox_menu_text }
|
140
|
+
end
|
141
|
+
</code></pre>
|
142
|
+
|
143
|
+
This works for both controller and action-based menus.
|
144
|
+
|
145
|
+
h3. Manually overriding current sections
|
146
|
+
|
147
|
+
This doesn't work for action-based menus, but in controllers you can override the current section via the @current_section()@ method. For example:
|
148
|
+
|
149
|
+
<pre><code>class Admin::UsersController < ActionController::Base
|
150
|
+
current_section :administrator
|
151
|
+
end
|
152
|
+
</code></pre>
|
153
|
+
|
154
|
+
This is really only useful for controllers that don't have an explicit menu item, but you would still like one of the sections to be highlighted.
|
155
|
+
|
156
|
+
h3. That's all!
|
157
|
+
|
158
|
+
Feedback is welcome, and as always, feel free to fork and improve :-)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/navigation.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "navigation/error"
|
2
|
+
require "navigation/base"
|
3
|
+
require "navigation/navigator"
|
4
|
+
require "navigation/nested_menu"
|
5
|
+
require "navigation/menu"
|
6
|
+
require "navigation/builder"
|
7
|
+
|
8
|
+
module Navigation
|
9
|
+
MENUS, SUBMENU = ActiveSupport::OrderedHash.new, :nested_navigation
|
10
|
+
|
11
|
+
module ControllerMethods
|
12
|
+
def current_section(section = nil)
|
13
|
+
self._current_section = section if section
|
14
|
+
self._current_section
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Helpers
|
19
|
+
# renders the navigation identified by
|
20
|
+
# the passed in name/key
|
21
|
+
#
|
22
|
+
# Ex:
|
23
|
+
# <%= navigation :primary %>
|
24
|
+
#
|
25
|
+
def navigation(key, options = {})
|
26
|
+
options.merge!(:view => self)
|
27
|
+
navigator = Navigator.new(key, options)
|
28
|
+
if navigator.allowed?
|
29
|
+
content_tag(:ul, navigator.links.join("\n").html_safe, options.merge!(:class => navigator.css_class))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
ActionController::Base.class_eval do
|
36
|
+
extend Navigation::ControllerMethods
|
37
|
+
class_attribute :_current_section
|
38
|
+
end
|
39
|
+
|
40
|
+
ActionView::Base.class_eval do
|
41
|
+
include Navigation::Helpers
|
42
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Navigation
|
2
|
+
# superclass for all navigation classes
|
3
|
+
# (put common methods in here)
|
4
|
+
class Base
|
5
|
+
# keep everything in the same format
|
6
|
+
# for setting/getting hash values
|
7
|
+
def normalize(val)
|
8
|
+
val.to_s.underscore.sub(/_controller$/, '')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Navigation
|
2
|
+
# responsible for the initial menu building/configuration
|
3
|
+
#
|
4
|
+
# Ex (config/initializers/navigation.rb):
|
5
|
+
# RPH::Navigation::Builder.config do |navigation|
|
6
|
+
# navigation.define :primary do |menu|
|
7
|
+
# ...
|
8
|
+
# end
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
class Builder < Base
|
12
|
+
def self.config
|
13
|
+
# gives a Builder instance to the block
|
14
|
+
yield self.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# accepts a name/key, and yields
|
18
|
+
# a Menu instance to the block
|
19
|
+
def define(*args)
|
20
|
+
name, options = args.first, args.extract_options!
|
21
|
+
|
22
|
+
raise InvalidMenuDefinition, InvalidMenuDefinition.message if name.blank?
|
23
|
+
yield Menu.new(normalize(name), options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Navigation
|
2
|
+
# generic error superclass for convenient message setting
|
3
|
+
class Error < RuntimeError
|
4
|
+
def self.message(msg = nil)
|
5
|
+
msg.nil? ? @message : self.message = msg
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.message=(msg)
|
9
|
+
@message = msg
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# raised if the navigation() helper is called without any defined menus
|
14
|
+
class NoMenuDefinitions < Error
|
15
|
+
message "There are no menus defined (menus should be defined in config/initializers/navigation.rb)"
|
16
|
+
end
|
17
|
+
|
18
|
+
# raised if a menu definition is attempted, but the menu name/key is blank
|
19
|
+
class InvalidMenuDefinition < Error
|
20
|
+
message "You must pass a name when defining a navigation menu"
|
21
|
+
end
|
22
|
+
|
23
|
+
# raised if the navigation() helper is called, but the menu name/key is blank
|
24
|
+
class BlankMenuIdentifier < Error
|
25
|
+
message "You must pass an identifier (the name used when defining the navigation) to the navigation() helper"
|
26
|
+
end
|
27
|
+
|
28
|
+
# raised if the navigation() helper is called, but the menu name/key doesn't exist
|
29
|
+
class InvalidMenuIdentifier < Error
|
30
|
+
message "There is no menu defined for that identifier"
|
31
|
+
end
|
32
|
+
|
33
|
+
# raised if a block is passed to a nested menu item when being defined
|
34
|
+
class InvalidBlock < Error
|
35
|
+
message "You cannot pass a block to a nested menu item"
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Navigation
|
2
|
+
# handles adding items
|
3
|
+
# to a specific menu
|
4
|
+
class Menu < Base
|
5
|
+
def initialize(name, options = {})
|
6
|
+
@name = name
|
7
|
+
(MENUS[@name] = ActiveSupport::OrderedHash.new).merge!(
|
8
|
+
:action_menu => options[:action_menu], :if => options[:if])
|
9
|
+
end
|
10
|
+
|
11
|
+
def item(key, options = {}, &block)
|
12
|
+
key = normalize(key)
|
13
|
+
|
14
|
+
MENUS[@name].merge!(key => options)
|
15
|
+
|
16
|
+
# if a block is given to this method, it is
|
17
|
+
# assumed that there will be nested navigation,
|
18
|
+
# so yield a NestedMenu instance back to the block
|
19
|
+
yield NestedMenu.new(@name, key) if block_given?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Navigation
|
2
|
+
class Navigator < Base
|
3
|
+
attr_reader :name, :view
|
4
|
+
|
5
|
+
def initialize(name, options)
|
6
|
+
# must have menu definitions at this point
|
7
|
+
raise NoMenuDefinitions, NoMenuDefinitions.message if MENUS.blank?
|
8
|
+
# since multiple menus are supported, must have the name of the desired menu
|
9
|
+
raise BlankMenuIdentifier, BlankMenuIdentifier.message if name.blank?
|
10
|
+
# menu name/key must exist in the current menu definitions
|
11
|
+
raise InvalidMenuIdentifier, InvalidMenuIdentifier.message unless valid?(name)
|
12
|
+
|
13
|
+
# need a reference of the view/view
|
14
|
+
# for building out the HTML
|
15
|
+
@view = options.delete(:view)
|
16
|
+
|
17
|
+
# initialization
|
18
|
+
@name, @options = normalize(name), options
|
19
|
+
|
20
|
+
# support building separate menus for actions,
|
21
|
+
# without forcing it to be nested
|
22
|
+
@action_menu = MENUS[@name][:action_menu]
|
23
|
+
|
24
|
+
# menus can be shown/hidden based on
|
25
|
+
# conditions by passing a Proc object
|
26
|
+
# Ex:
|
27
|
+
# navigation.define :primary, :if => Proc.new { |c| c.logged_in? } do |menu|
|
28
|
+
# ...
|
29
|
+
# end
|
30
|
+
@proc = MENUS[@name][:if]
|
31
|
+
end
|
32
|
+
|
33
|
+
# the css class of the
|
34
|
+
# parent <ul> element
|
35
|
+
def css_class
|
36
|
+
@options[:class] || 'navigation'
|
37
|
+
end
|
38
|
+
|
39
|
+
# returns the list items, all
|
40
|
+
# properly nested (if needed)
|
41
|
+
def links
|
42
|
+
construct_html(self.items)
|
43
|
+
end
|
44
|
+
|
45
|
+
def allowed?
|
46
|
+
execute_proc @proc
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
# calls a proc and hands it
|
51
|
+
# the view/template instance
|
52
|
+
def execute_proc(proc)
|
53
|
+
return true unless proc.is_a?(Proc)
|
54
|
+
proc.call(self.view)
|
55
|
+
end
|
56
|
+
|
57
|
+
# convenience method
|
58
|
+
def items
|
59
|
+
MENUS[@name].except(:action_menu, :if)
|
60
|
+
end
|
61
|
+
|
62
|
+
# checks to make sure the menu
|
63
|
+
# name/key actually exists
|
64
|
+
def valid?(key)
|
65
|
+
MENUS.keys.map(&:to_sym).include?(key.to_sym)
|
66
|
+
end
|
67
|
+
|
68
|
+
# builds the HTML from the MENUS hash
|
69
|
+
# (does a recursive call for nested menus)
|
70
|
+
def construct_html(menu, nested = false)
|
71
|
+
return if menu.blank?
|
72
|
+
|
73
|
+
links = menu.inject([]) do |items, (item, opts)|
|
74
|
+
next(items) unless execute_proc(opts[:if])
|
75
|
+
|
76
|
+
text, path, attrs = self.disect(item, opts)
|
77
|
+
subnav = construct_html(menu[item][SUBMENU], true).to_s
|
78
|
+
attrs.merge!(:class => [attrs[:class], self.current_css(item, nested)].compact.join(' '))
|
79
|
+
items << self.view.content_tag(:li, self.view.link_to(text, path) + subnav, attrs)
|
80
|
+
end
|
81
|
+
|
82
|
+
nested ? self.view.content_tag(:ul, links, :class => 'sub-navigation') : links
|
83
|
+
end
|
84
|
+
|
85
|
+
# determines if the menu item matches the current section
|
86
|
+
# (considers both levels: controller and action)
|
87
|
+
def current_css(item, nested = false)
|
88
|
+
name = if (nested || @action_menu)
|
89
|
+
self.view.action_name
|
90
|
+
else
|
91
|
+
(controller_override = self.view.controller.class.current_section).blank? ?
|
92
|
+
self.view.controller_name : controller_override
|
93
|
+
end
|
94
|
+
|
95
|
+
'current' if normalize(name) == normalize(item)
|
96
|
+
end
|
97
|
+
|
98
|
+
# pulls out the goodies for use
|
99
|
+
# in the HTML construction
|
100
|
+
def disect(item, opts)
|
101
|
+
opts = HashWithIndifferentAccess.new(opts)#.symbolize_keys!
|
102
|
+
|
103
|
+
path = self.view.send(opts.delete(:path) || "#{item.to_s.underscore}_path" || :root_path)
|
104
|
+
text = opts.delete(:text) || item.to_s.titleize
|
105
|
+
text = execute_proc(text) if text.is_a?(Proc)
|
106
|
+
attrs = opts.except(SUBMENU, :if)
|
107
|
+
|
108
|
+
[text, path, attrs]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Navigation
|
2
|
+
# handles adding nested menu items
|
3
|
+
# to a parent menu item
|
4
|
+
class NestedMenu < Base
|
5
|
+
def initialize(menu, parent)
|
6
|
+
@menu, @parent = menu, parent
|
7
|
+
|
8
|
+
# the entire nested menu is stored as a nested ordered hash
|
9
|
+
# since we want multiple items in the same hash, only initialize once
|
10
|
+
MENUS[@menu][@parent][SUBMENU] = ActiveSupport::OrderedHash.new unless MENUS[@menu][@parent].has_key?(SUBMENU)
|
11
|
+
end
|
12
|
+
|
13
|
+
def item(key, options = {})
|
14
|
+
# only support one level of nesting (the action-level)
|
15
|
+
raise InvalkeyBlock, InvalkeyBlock.message if block_given?
|
16
|
+
MENUS[@menu][@parent][SUBMENU].merge!(normalize(key) => options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/navigation.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'navigation/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "navigation"
|
8
|
+
gem.version = Navigation::VERSION
|
9
|
+
gem.authors = ["Ryan Heath"]
|
10
|
+
gem.email = ["ryan@rpheath.com"]
|
11
|
+
gem.description = %q{Navigation helpers for building menus in Rails}
|
12
|
+
gem.summary = %q{Navigation helpers for building menus in Rails}
|
13
|
+
gem.homepage = "http://github.com/rpheath/navigation"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
Nav = Class.new(Navigation::Navigator)
|
4
|
+
|
5
|
+
class NavigationTest < ActiveSupport::TestCase
|
6
|
+
def setup
|
7
|
+
@view = ActionView::Base.new
|
8
|
+
@basic = Nav.new(:basic, { :view => @view })
|
9
|
+
@menu_with_proc = Nav.new(:menu_with_proc, { :view => @view })
|
10
|
+
@custom_menu = Nav.new(:basic, { :view => @view, :class => "navigator" })
|
11
|
+
@menu_with_submenu = Nav.new(:menu_with_submenu, { :view => @view })
|
12
|
+
end
|
13
|
+
|
14
|
+
context "default behavior" do
|
15
|
+
test "should be basic navigation" do
|
16
|
+
assert_equal @basic.name, "basic"
|
17
|
+
end
|
18
|
+
|
19
|
+
test "should have access to the view" do
|
20
|
+
assert_equal @basic.view, @view
|
21
|
+
end
|
22
|
+
|
23
|
+
test "should have a 'navigation' css class" do
|
24
|
+
assert_equal @basic.css_class, "navigation"
|
25
|
+
end
|
26
|
+
|
27
|
+
test "should be 'allowed' if no Proc is specified" do
|
28
|
+
assert @basic.allowed?
|
29
|
+
end
|
30
|
+
|
31
|
+
test "menu items should be wrapped in an OrderedHash" do
|
32
|
+
assert @basic.send(:items).is_a?(ActiveSupport::OrderedHash)
|
33
|
+
end
|
34
|
+
|
35
|
+
test "should have appropriate items (and in order)" do
|
36
|
+
items = ActiveSupport::OrderedHash.new
|
37
|
+
%w(home features events).each do |menu_item|
|
38
|
+
items[menu_item] = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
assert_equal @basic.send(:items), items
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "custom menu options" do
|
46
|
+
test "should have a custom CSS class" do
|
47
|
+
assert_equal @custom_menu.css_class, "navigator"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "menu validation" do
|
52
|
+
test "should be a valid menu" do
|
53
|
+
assert @basic.send(:valid?, :basic)
|
54
|
+
end
|
55
|
+
|
56
|
+
test "should not be a valid menu" do
|
57
|
+
assert !@basic.send(:valid?, :invalid_menu)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "menu with Proc dependency" do
|
62
|
+
test "should be a valid menu" do
|
63
|
+
assert @menu_with_proc.send(:valid?, :menu_with_proc)
|
64
|
+
end
|
65
|
+
|
66
|
+
test "should show the menu" do
|
67
|
+
@view.instance_eval do
|
68
|
+
def show_menu?
|
69
|
+
true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
assert @menu_with_proc.allowed?
|
74
|
+
end
|
75
|
+
|
76
|
+
test "should NOT show the menu" do
|
77
|
+
@view.instance_eval do
|
78
|
+
def show_menu?
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
assert !@menu_with_proc.allowed?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "disecting menu items" do
|
88
|
+
test "should return an array with 3 items" do
|
89
|
+
result = @basic.send(:disect, 'home', {})
|
90
|
+
assert result.is_a?(Array)
|
91
|
+
assert_equal 3, result.size
|
92
|
+
end
|
93
|
+
|
94
|
+
test "should return a disected menu item" do
|
95
|
+
result = @basic.send(:disect, 'home', {})
|
96
|
+
assert_equal ["Home", "/home", {}], result
|
97
|
+
end
|
98
|
+
|
99
|
+
test "should return a disected menu item with custom path and options" do
|
100
|
+
result = @basic.send(:disect, 'home',
|
101
|
+
{ :path => "custom_home_path", :class => 'custom', :text => "Custom Home" })
|
102
|
+
assert_equal ["Custom Home", "/path/to/home", { "class" => "custom" }], result
|
103
|
+
end
|
104
|
+
|
105
|
+
test "should return a disected menu item disregarding SUBMENU and :if options" do
|
106
|
+
result = @basic.send(:disect, 'home',
|
107
|
+
{ RPH::Navigation::SUBMENU => "whatever", :if => Proc.new {} })
|
108
|
+
assert_equal ["Home", "/home", {}], result
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "constructing the HTML" do
|
113
|
+
test "should return the proper HTML links" do
|
114
|
+
@view.controller = HomeController.new
|
115
|
+
assert_equal @basic.links, [
|
116
|
+
"<li class=\"current\"><a href=\"/home\">Home</a></li>",
|
117
|
+
"<li class=\"\"><a href=\"/features\">Features</a></li>",
|
118
|
+
"<li class=\"\"><a href=\"/events\">Events</a></li>"
|
119
|
+
]
|
120
|
+
end
|
121
|
+
|
122
|
+
test "should be current tab on the features link" do
|
123
|
+
@view.controller = FeaturesController.new
|
124
|
+
assert_equal @basic.links[1], "<li class=\"current\"><a href=\"/features\">Features</a></li>"
|
125
|
+
end
|
126
|
+
|
127
|
+
test "should return a nested submenu under the Home link" do
|
128
|
+
@view.controller = HomeController.new
|
129
|
+
assert_equal @menu_with_submenu.links, [
|
130
|
+
"<li class=\"current\"><a href=\"/home\">Home</a><ul class=\"sub-navigation\"><li class=\"current\"><a href=\"/\">Index</a></li></ul></li>",
|
131
|
+
"<li class=\"\"><a href=\"/features\">Features</a></li>"
|
132
|
+
]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'action_view'
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/test_case'
|
5
|
+
require 'context'
|
6
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'navigation')
|
7
|
+
|
8
|
+
class HomeController
|
9
|
+
def controller_name
|
10
|
+
'home'
|
11
|
+
end
|
12
|
+
|
13
|
+
def action_name
|
14
|
+
'index'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class FeaturesController
|
19
|
+
def controller_name
|
20
|
+
'features'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ActionView::Base
|
25
|
+
def root_path
|
26
|
+
'/'
|
27
|
+
end
|
28
|
+
|
29
|
+
def home_path
|
30
|
+
'/home'
|
31
|
+
end
|
32
|
+
|
33
|
+
def custom_home_path
|
34
|
+
'/path/to/home'
|
35
|
+
end
|
36
|
+
|
37
|
+
def features_path
|
38
|
+
'/features'
|
39
|
+
end
|
40
|
+
|
41
|
+
def events_path
|
42
|
+
'/events'
|
43
|
+
end
|
44
|
+
|
45
|
+
def show_menu?
|
46
|
+
true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Navigation::Builder.config do |navigation|
|
51
|
+
navigation.define :basic do |menu|
|
52
|
+
menu.item :home
|
53
|
+
menu.item :features
|
54
|
+
menu.item :events
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
Navigation::Builder.config do |navigation|
|
59
|
+
navigation.define :menu_with_proc, :if => Proc.new { |view| view.show_menu? } do |menu|
|
60
|
+
menu.item :home
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Navigation::Builder.config do |navigation|
|
65
|
+
navigation.define :menu_with_submenu do |menu|
|
66
|
+
menu.item :home do |sub|
|
67
|
+
sub.item :index, :path => :root_path
|
68
|
+
end
|
69
|
+
menu.item :features
|
70
|
+
end
|
71
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: navigation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Ryan Heath
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2014-10-16 00:00:00 -04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Navigation helpers for building menus in Rails
|
23
|
+
email:
|
24
|
+
- ryan@rpheath.com
|
25
|
+
executables: []
|
26
|
+
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files: []
|
30
|
+
|
31
|
+
files:
|
32
|
+
- .gitignore
|
33
|
+
- Gemfile
|
34
|
+
- LICENSE
|
35
|
+
- README.textile
|
36
|
+
- Rakefile
|
37
|
+
- lib/navigation.rb
|
38
|
+
- lib/navigation/base.rb
|
39
|
+
- lib/navigation/builder.rb
|
40
|
+
- lib/navigation/error.rb
|
41
|
+
- lib/navigation/menu.rb
|
42
|
+
- lib/navigation/navigator.rb
|
43
|
+
- lib/navigation/nested_menu.rb
|
44
|
+
- lib/navigation/version.rb
|
45
|
+
- navigation.gemspec
|
46
|
+
- test/navigation_test.rb
|
47
|
+
- test/test_helper.rb
|
48
|
+
has_rdoc: true
|
49
|
+
homepage: http://github.com/rpheath/navigation
|
50
|
+
licenses: []
|
51
|
+
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
hash: 3
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 3
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
version: "0"
|
75
|
+
requirements: []
|
76
|
+
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 1.6.2
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: Navigation helpers for building menus in Rails
|
82
|
+
test_files:
|
83
|
+
- test/navigation_test.rb
|
84
|
+
- test/test_helper.rb
|