14 | # element.
15 | class Breadcrumbs < SimpleNavigation::Renderer::Base
16 | def render(item_container)
17 | content = a_tags(item_container).join(join_with)
18 | content_tag(:div,
19 | prefix_for(content) + content,
20 | item_container.dom_attributes)
21 | end
22 |
23 | protected
24 |
25 | def a_tags(item_container)
26 | item_container.items.each_with_object([]) do |item, list|
27 | next unless item.selected?
28 | list << tag_for(item)
29 |
30 | if include_sub_navigation?(item)
31 | list.concat a_tags(item.sub_navigation)
32 | end
33 | end
34 | end
35 |
36 | def join_with
37 | @join_with ||= options[:join_with] || ' '
38 | end
39 |
40 | def suppress_link?(item)
41 | super || (options[:static_leaf] && item.active_leaf_class)
42 | end
43 |
44 | def prefix_for(content)
45 | if !content.empty? && options[:prefix]
46 | options[:prefix]
47 | else
48 | ''
49 | end
50 | end
51 |
52 | # Extracts the options relevant for the generated link
53 | #
54 | def link_options_for(item)
55 | if options[:allow_classes_and_ids]
56 | opts = super
57 | opts[:id] &&= "breadcrumb_#{opts[:id]}"
58 | opts
59 | else
60 | html_options = item.html_options.except(:class, :id)
61 | { method: item.method }.merge(html_options)
62 | end
63 | end
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'initializers/have_css_matcher'
2 | require 'initializers/memfs'
3 | require 'initializers/coveralls'
4 | require 'initializers/rails'
5 | require 'initializers/rspec'
6 | require 'capybara/rspec'
7 |
8 | require 'bundler/setup'
9 | Bundler.require
10 |
11 | if defined? Rails
12 | require 'fake_app/rails_app'
13 | require 'rspec/rails'
14 |
15 | Capybara.app = RailsApp::Application
16 |
17 | RSpec.configure do |config|
18 | config.before do
19 | SimpleNavigation.config_files.clear
20 | setup_adapter_for :rails
21 | end
22 | end
23 | end
24 |
25 | def setup_adapter_for(framework, context = double(:context))
26 | if framework == :rails
27 | # Rails 6.0 and 6.1 provide ActionView::Base.empty method that creates ActionView with an empty LookupContext.
28 | # The method is not available on older versions
29 | view_context = ActionView::Base.respond_to?(:empty) ? ActionView::Base.empty : ActionView::Base.new
30 | allow(context).to receive_messages(view_context: view_context)
31 | end
32 |
33 | allow(SimpleNavigation).to receive_messages(framework: framework)
34 | SimpleNavigation.load_adapter
35 | SimpleNavigation.init_adapter_from(context)
36 | end
37 |
38 | def select_an_item(item)
39 | allow(item).to receive_messages(selected?: true)
40 | end
41 |
42 | def setup_container(dom_id, dom_class)
43 | container = SimpleNavigation::ItemContainer.new(1)
44 | container.dom_id = dom_id
45 | container.dom_class = dom_class
46 | container
47 | end
48 |
49 | def setup_navigation(dom_id, dom_class)
50 | setup_adapter_for :rails
51 | container = setup_container(dom_id, dom_class)
52 | setup_items(container)
53 | container
54 | end
55 |
56 | # FIXME: adding the :link option for the list renderer messes up the other
57 | # renderers
58 | def setup_items(container)
59 | container.item :users, 'Users', '/users', html: { id: 'users_id' }, link_html: { id: 'users_link_id' }
60 | container.item :invoices, 'Invoices', '/invoices' do |invoices|
61 | invoices.item :paid, 'Paid', '/invoices/paid'
62 | invoices.item :unpaid, 'Unpaid', '/invoices/unpaid'
63 | end
64 | container.item :accounts, 'Accounts', '/accounts', html: { style: 'float:right' }
65 | container.item :miscellany, 'Miscellany'
66 |
67 | container.items.each do |item|
68 | allow(item).to receive_messages(selected?: false, selected_by_condition?: false)
69 |
70 | if item.sub_navigation
71 | item.sub_navigation.items.each do |item|
72 | allow(item).to receive_messages(selected?: false, selected_by_condition?: false)
73 | end
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/spec/simple_navigation/renderer/json_spec.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | module Renderer
3 | describe Json do
4 | describe '#render' do
5 | let!(:navigation) { setup_navigation('nav_id', 'nav_class') }
6 |
7 | let(:item) { :invoices }
8 | let(:options) {{ level: :all }}
9 | let(:output) { renderer.render(navigation) }
10 | let(:parsed_output) { JSON.parse(output) }
11 | let(:renderer) { Json.new(options) }
12 |
13 | before { select_an_item(navigation[item]) if item }
14 |
15 | context 'when an item is selected' do
16 |
17 | it 'renders the selected page' do
18 | invoices_item = parsed_output.find { |item| item['name'] == 'Invoices' }
19 | expect(invoices_item).to include('selected' => true)
20 | end
21 | end
22 |
23 | context 'when the :as_hash option is true' do
24 | let(:options) {{ level: :all, as_hash: true }}
25 |
26 | it 'returns every item as a hash' do
27 | expect(output).to be_an Array
28 |
29 | output.each do |item|
30 | expect(item).to be_an Hash
31 | end
32 | end
33 |
34 | it 'renders the selected page' do
35 | invoices_item = output.find { |item| item[:name] == 'Invoices' }
36 | expect(invoices_item).to include(selected: true)
37 | end
38 | end
39 |
40 | context 'with options' do
41 | it 'should render options for each item' do
42 | parsed_output.each do |item|
43 | expect(item).to have_key('options')
44 | end
45 | end
46 | end
47 |
48 | context 'when a sub navigation item is selected' do
49 | let(:invoices_item) do
50 | parsed_output.find { |item| item['name'] == 'Invoices' }
51 | end
52 | let(:unpaid_item) do
53 | invoices_item['items'].find { |item| item['name'] == 'Unpaid' }
54 | end
55 |
56 | before do
57 | allow(navigation[:invoices]).to receive_messages(selected?: true)
58 |
59 | allow(navigation[:invoices].sub_navigation[:unpaid]).to \
60 | receive_messages(selected?: true, selected_by_condition?: true)
61 | end
62 |
63 | it 'marks all the parent items as selected' do
64 | expect(invoices_item).to include('selected' => true)
65 | end
66 |
67 | it 'marks the item as selected' do
68 | expect(unpaid_item).to include('selected' => true)
69 | end
70 | end
71 | end
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple Navigation
2 |
3 | [](http://badge.fury.io/rb/simple-navigation)
4 | [](http://travis-ci.org/codeplant/simple-navigation)
5 | [](https://codeclimate.com/github/codeplant/simple-navigation)
6 | [](https://coveralls.io/r/codeplant/simple-navigation)
7 |
8 | Simple Navigation is a ruby library for creating navigations (with multiple levels) for your Rails, Sinatra or Padrino applications. It runs with all ruby versions (including ruby 2.x).
9 |
10 | ## Documentation
11 |
12 | For the complete documentation, take a look at the [project's wiki](https://github.com/codeplant/simple-navigation/wiki).
13 |
14 | ## RDoc
15 |
16 | You can consult the project's RDoc on [RubyDoc.info](http://rubydoc.info/github/codeplant/simple-navigation/frames).
17 |
18 | If you need to generate the RDoc files locally, check out the repository and simply call the `rake rdoc` in the project's folder.
19 |
20 | ## Online Demo
21 |
22 | You can try simple-navigation with the [online demo](http://simple-navigation-demo.codeplant.ch).
23 |
24 | The source code of this online demo is [available on Github](http://github.com/codeplant/simple-navigation-demo).
25 |
26 | ## Feedback and Questions
27 |
28 | Don't hesitate to come talk on the [project's group](http://groups.google.com/group/simple-navigation).
29 |
30 | ## Contributing
31 |
32 | Fork, fix, then send a Pull Request.
33 |
34 | To run the test suite locally against all supported frameworks:
35 |
36 | % bundle install
37 | % rake spec:all
38 |
39 | To target the test suite against one framework:
40 |
41 | % rake spec:rails-4-2-stable
42 |
43 | You can find a list of supported spec tasks by running rake -T. You may also find it useful to run a specific test for a specific framework. To do so, you'll have to first make sure you have bundled everything for that configuration, then you can run the specific test:
44 |
45 | % BUNDLE_GEMFILE='gemfiles/rails-4-2-stable.gemfile' bundle install -j 4
46 | % BUNDLE_GEMFILE='gemfiles/rails-4-2-stable.gemfile' bundle exec rspec ./spec/requests/users_spec.rb
47 |
48 | ### Rake and Bundler
49 |
50 | If you use a shell plugin (like oh-my-zsh:bundler) that auto-prefixes commands with `bundle exec` using the `rake` command will fail.
51 |
52 | Get the original command with `type -a rake`:
53 |
54 | % type -a rake
55 | rake is an alias for bundled_rake
56 | rake is /Users/username/.rubies/ruby-2.2.3/bin/rake
57 | rake is /usr/bin/rake
58 |
59 | In this situation `/Users/username/.rubies/ruby-2.2.3/bin/rake` is the command you should use.
60 |
61 | ## License
62 |
63 | Copyright (c) 2022 codeplant GmbH, released under the MIT license
64 |
--------------------------------------------------------------------------------
/spec/simple_navigation/renderer/links_spec.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | module Renderer
3 | describe Links do
4 | describe '#render' do
5 | let!(:navigation) { setup_navigation('nav_id', 'nav_class') }
6 |
7 | let(:item) { nil }
8 | let(:options) { { level: :all } }
9 | let(:output) { renderer.render(navigation) }
10 | let(:renderer) { Links.new(options) }
11 |
12 | before { select_an_item(navigation[item]) if item }
13 |
14 | it "renders a 'div' tag for the navigation" do
15 | expect(output).to have_css('div')
16 | end
17 |
18 | it "sets the right html id on the rendered 'div' tag" do
19 | expect(output).to have_css('div#nav_id')
20 | end
21 |
22 | it "sets the right html classes on the rendered 'div' tag" do
23 | expect(output).to have_css('div.nav_class')
24 | end
25 |
26 | it "renders an 'a' tag for each item" do
27 | expect(output).to have_css('a', 3)
28 | end
29 |
30 | it "renders the 'a' tags with the corresponding item's :html_options" do
31 | expect(output).to have_css('a[style="float:right"]')
32 | end
33 |
34 | context 'when an item has a specified id' do
35 | it "renders the 'a' tags with the specified id" do
36 | expect(output).to have_css('a#users_id')
37 | end
38 | end
39 |
40 | context 'when an item has no specified id' do
41 | it "uses a default id by stringifying the item's key" do
42 | expect(output).to have_css('a#invoices')
43 | end
44 | end
45 |
46 | context 'when no item is selected' do
47 | it "renders items without the 'selected' class" do
48 | expect(output).not_to have_css('a.selected')
49 | end
50 | end
51 |
52 | context 'when an item is selected' do
53 | let(:item) { :invoices }
54 |
55 | it "renders the selected item with the 'selected' class" do
56 | expect(output).to have_css('a#invoices.selected')
57 | end
58 | end
59 |
60 | context "when the :join_with option is set" do
61 | let(:options) {{ level: :all, join_with: ' | ' }}
62 |
63 | it 'separates the items with the specified separator' do
64 | expect(output.scan(' | ').size).to eq 3
65 | end
66 | end
67 |
68 | context 'when a sub navigation item is selected' do
69 | before do
70 | allow(navigation[:invoices]).to receive_messages(selected?: true)
71 |
72 | allow(navigation[:invoices].sub_navigation[:unpaid]).to \
73 | receive_messages(selected?: true, selected_by_condition?: true)
74 | end
75 |
76 | it 'renders the main parent as selected' do
77 | expect(output).to have_css('a#invoices.selected')
78 | end
79 |
80 | it "doesn't render the nested item's link" do
81 | expect(output).not_to have_css('a#unpaid')
82 | end
83 | end
84 | end
85 | end
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/lib/simple_navigation/adapters/rails.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | module Adapters
3 | class Rails < Base
4 | attr_reader :controller, :template
5 |
6 | def self.register
7 | SimpleNavigation.set_env(::Rails.root, ::Rails.env)
8 |
9 | # Autoloading in initializers is deprecated on rails 6.0
10 | # This delays the hook initialization using the on_load
11 | # hooks, but does not change behaviour for existing
12 | # rails versions.
13 | if ::Rails::VERSION::MAJOR >= 6
14 | ActiveSupport.on_load(:action_controller_base) do
15 | SimpleNavigation::Adapters::Rails.register_controller_helpers
16 | end
17 | else
18 | register_controller_helpers
19 | end
20 | end
21 |
22 | def self.register_controller_helpers
23 | ActionController::Base.send(:include, SimpleNavigation::Helpers)
24 | SimpleNavigation::Helpers.instance_methods.each do |m|
25 | ActionController::Base.send(:helper_method, m.to_sym)
26 | end
27 | end
28 |
29 | def initialize(context)
30 | @controller = extract_controller_from context
31 | @template = template_from @controller
32 | @request = @template.request if @template
33 | end
34 |
35 | def request_uri
36 | return '' unless request
37 |
38 | if request.respond_to?(:fullpath)
39 | request.fullpath
40 | else
41 | request.request_uri
42 | end
43 | end
44 |
45 | def request_path
46 | request ? request.path : ''
47 | end
48 |
49 | def context_for_eval
50 | template ||
51 | controller ||
52 | fail('no context set for evaluation the config file')
53 | end
54 |
55 | def current_page?(url)
56 | template && template.current_page?(url)
57 | end
58 |
59 | def link_to(name, url, options = {})
60 | template && template.link_to(link_title(name), url, options)
61 | end
62 |
63 | def content_tag(type, content, options = {})
64 | template && template.content_tag(type, html_safe(content), options)
65 | end
66 |
67 | protected
68 |
69 | def template_from(controller)
70 | if controller.respond_to?(:view_context)
71 | controller.view_context
72 | else
73 | controller.instance_variable_get(:@template)
74 | end
75 | end
76 |
77 | # Marks the specified input as html_safe (for Rails3).
78 | # Does nothing if html_safe is not defined on input.
79 | #
80 | def html_safe(input)
81 | input.respond_to?(:html_safe) ? input.html_safe : input
82 | end
83 |
84 | # Extracts a controller from the context.
85 | def extract_controller_from(context)
86 | if context.respond_to?(:controller)
87 | context.controller || context
88 | else
89 | context
90 | end
91 | end
92 |
93 | def link_title(name)
94 | if SimpleNavigation.config.consider_item_names_as_safe
95 | html_safe(name)
96 | else
97 | name
98 | end
99 | end
100 | end
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/spec/simple_navigation/renderer/list_spec.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | module Renderer
3 | describe List do
4 | let!(:navigation) { setup_navigation('nav_id', 'nav_class') }
5 |
6 | let(:item) { nil }
7 | let(:options) { { level: :all } }
8 | let(:output) { renderer.render(navigation) }
9 | let(:renderer) { List.new(options) }
10 |
11 | before { select_an_item(navigation[item]) if item }
12 |
13 | describe '#render' do
14 | it "renders an 'ul' tag for the navigation" do
15 | expect(output).to have_css('ul')
16 | end
17 |
18 | it "sets the right html id on the rendered 'ul' tag" do
19 | expect(output).to have_css('ul#nav_id')
20 | end
21 |
22 | it "sets the right html classes on the rendered 'ul' tag" do
23 | expect(output).to have_css('ul.nav_class')
24 | end
25 |
26 | context 'when an item has no specified id' do
27 | it "renders the item's 'li' tag with the item's stingified key as id" do
28 | expect(output).to have_css('li#invoices')
29 | end
30 | end
31 |
32 | context 'when an item has a specified id' do
33 | it "renders the item's 'li' tag with the specified id" do
34 | expect(output).to have_css('li#users_id')
35 | end
36 | end
37 |
38 | context 'when no item is selected' do
39 | it "renders each item as 'li' tag without any selected class" do
40 | expect(output).not_to have_css('ul li.selected')
41 | end
42 |
43 | it "renders each item as 'a' tag without any selected class" do
44 | expect(output).not_to have_css('ul li a.selected')
45 | end
46 | end
47 |
48 | context 'when an item is selected' do
49 | let(:item) { :invoices }
50 |
51 | it "renders the item's 'li' tag with its id and selected classes" do
52 | expect(output).to have_css('li#invoices.selected')
53 | end
54 |
55 | it "renders the item's 'a' tag with the selected classes" do
56 | expect(output).to have_css('li#invoices a.selected')
57 | end
58 | end
59 |
60 | context 'when the :ordered option is true' do
61 | let(:options) {{ level: :all, ordered: true }}
62 |
63 | it "renders an 'ol' tag for the navigation" do
64 | expect(output).to have_css('ol')
65 | end
66 |
67 | it "sets the right html id on the rendered 'ol' tag" do
68 | expect(output).to have_css('ol#nav_id')
69 | end
70 |
71 | it "sets the right html classes on the rendered 'ol' tag" do
72 | expect(output).to have_css('ol.nav_class')
73 | end
74 | end
75 |
76 | context 'when a sub navigation item is selected' do
77 | before do
78 | allow(navigation[:invoices]).to receive_messages(selected?: true)
79 |
80 | allow(navigation[:invoices].sub_navigation[:unpaid]).to \
81 | receive_messages(selected?: true, selected_by_condition?: true)
82 | end
83 |
84 | it 'renders the parent items as selected' do
85 | expect(output).to have_css('li#invoices.selected')
86 | end
87 |
88 | it "renders the selected nested item's link as selected" do
89 | expect(output).to have_css('li#unpaid.selected')
90 | end
91 | end
92 | end
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/lib/simple_navigation/renderer/base.rb:
--------------------------------------------------------------------------------
1 | require 'forwardable'
2 |
3 | module SimpleNavigation
4 | module Renderer
5 | # This is the base class for all renderers.
6 | #
7 | # A renderer is responsible for rendering an ItemContainer and its
8 | # containing items to HTML.
9 | class Base
10 | extend Forwardable
11 |
12 | attr_reader :adapter, :options
13 |
14 | def_delegators :adapter, :link_to, :content_tag
15 |
16 | def initialize(options) #:nodoc:
17 | @options = options
18 | @adapter = SimpleNavigation.adapter
19 | end
20 |
21 | def expand_all?
22 | !!options[:expand_all]
23 | end
24 |
25 | def level
26 | options[:level] || :all
27 | end
28 |
29 | def skip_if_empty?
30 | !!options[:skip_if_empty]
31 | end
32 |
33 | def include_sub_navigation?(item)
34 | consider_sub_navigation?(item) && expand_sub_navigation?(item)
35 | end
36 |
37 | def render_sub_navigation_for(item)
38 | item.sub_navigation.render(options)
39 | end
40 |
41 | # Renders the specified ItemContainer to HTML.
42 | #
43 | # When implementing a renderer, please consider to call
44 | # include_sub_navigation? to determine whether an item's sub_navigation
45 | # should be rendered or not.
46 | def render(item_container)
47 | fail NotImplementedError, 'subclass responsibility'
48 | end
49 |
50 | protected
51 |
52 | def consider_sub_navigation?(item)
53 | return false unless item.sub_navigation
54 |
55 | case level
56 | when :all then true
57 | when Range then item.sub_navigation.level <= level.max
58 | else false
59 | end
60 | end
61 |
62 | def expand_sub_navigation?(item)
63 | expand_all? || item.selected?
64 | end
65 |
66 | # to allow overriding when there is specific logic determining
67 | # when a link should not be rendered (eg. breadcrumbs renderer
68 | # does not render the final breadcrumb as a link when instructed
69 | # not to do so.)
70 | def suppress_link?(item)
71 | item.url.nil?
72 | end
73 |
74 | # determine and return link or static content depending on
75 | # item/renderer conditions.
76 | def tag_for(item)
77 | if suppress_link?(item)
78 | content_tag('span', item.name, link_options_for(item).except(:method))
79 | else
80 | link_to(item.name, item.url, options_for(item))
81 | end
82 | end
83 |
84 | # to allow overriding when link options should be special-cased
85 | # (eg. links renderer uses item options for the a-tag rather
86 | # than an li-tag).
87 | def options_for(item)
88 | link_options_for(item)
89 | end
90 |
91 | # Extracts the options relevant for the generated link
92 | def link_options_for(item)
93 | special_options = {
94 | method: item.method,
95 | class: item.selected_class
96 | }.reject { |_, v| v.nil? }
97 |
98 | link_options = item.link_html_options
99 |
100 | return special_options unless link_options
101 |
102 | opts = special_options.merge(link_options)
103 |
104 | classes = [link_options[:class], item.selected_class]
105 | classes = classes.flatten.compact.join(' ')
106 | opts[:class] = classes unless classes.empty?
107 |
108 | opts
109 | end
110 | end
111 | end
112 | end
113 |
--------------------------------------------------------------------------------
/lib/simple_navigation/configuration.rb:
--------------------------------------------------------------------------------
1 | require 'singleton'
2 |
3 | module SimpleNavigation
4 | # Responsible for evaluating and handling the config/navigation.rb file.
5 | class Configuration
6 | include Singleton
7 |
8 | attr_accessor :autogenerate_item_ids,
9 | :auto_highlight,
10 | :consider_item_names_as_safe,
11 | :highlight_on_subpath,
12 | :ignore_query_params_on_auto_highlight,
13 | :ignore_anchors_on_auto_highlight
14 |
15 | attr_reader :primary_navigation
16 |
17 | attr_writer :active_leaf_class,
18 | :id_generator,
19 | :name_generator,
20 | :renderer,
21 | :selected_class
22 |
23 | # Evals the config_file for the given navigation_context
24 | def self.eval_config(navigation_context = :default)
25 | context = SimpleNavigation.config_files[navigation_context]
26 | SimpleNavigation.context_for_eval.instance_eval(context)
27 | end
28 |
29 | # Starts processing the configuration
30 | def self.run(&block)
31 | block.call Configuration.instance
32 | end
33 |
34 | # Sets the config's default-settings
35 | def initialize
36 | @autogenerate_item_ids = true
37 | @auto_highlight = true
38 | @consider_item_names_as_safe = false
39 | @highlight_on_subpath = false
40 | @ignore_anchors_on_auto_highlight = true
41 | @ignore_query_params_on_auto_highlight = true
42 | end
43 |
44 | def active_leaf_class
45 | @active_leaf_class ||= 'simple-navigation-active-leaf'
46 | end
47 |
48 | def id_generator
49 | @id_generator ||= :to_s.to_proc
50 | end
51 |
52 | # This is the main method for specifying the navigation items.
53 | # It can be used in two ways:
54 | #
55 | # 1. Declaratively specify your items in the config/navigation.rb file
56 | # using a block. It then yields an SimpleNavigation::ItemContainer
57 | # for adding navigation items.
58 | # 2. Directly provide your items to the method (e.g. when loading your
59 | # items from the database).
60 | #
61 | # ==== Example for block style (configuration file)
62 | # config.items do |primary|
63 | # primary.item :my_item, 'My item', my_item_path
64 | # ...
65 | # end
66 | #
67 | # ==== To consider when directly providing items
68 | # items_provider should be:
69 | # * a methodname (as symbol) that returns your items. The method needs to
70 | # be available in the view (i.e. a helper method)
71 | # * an object that responds to :items
72 | # * an enumerable containing your items
73 | # The items you specify have to fullfill certain requirements.
74 | # See SimpleNavigation::ItemAdapter for more details.
75 | #
76 | def items(items_provider = nil, &block)
77 | if (items_provider && block) || (items_provider.nil? && block.nil?)
78 | fail('please specify either items_provider or block, but not both')
79 | end
80 |
81 | self.primary_navigation = ItemContainer.new
82 |
83 | if block
84 | block.call primary_navigation
85 | else
86 | primary_navigation.items = ItemsProvider.new(items_provider).items
87 | end
88 | end
89 |
90 | # Returns true if the config_file has already been evaluated.
91 | def loaded?
92 | !primary_navigation.nil?
93 | end
94 |
95 | def name_generator
96 | @name_generator ||= proc { |name| name }
97 | end
98 |
99 | def renderer
100 | @renderer ||= SimpleNavigation.default_renderer ||
101 | SimpleNavigation::Renderer::List
102 | end
103 |
104 | def selected_class
105 | @selected_class ||= 'selected'
106 | end
107 |
108 | private
109 |
110 | attr_writer :primary_navigation
111 | end
112 | end
113 |
--------------------------------------------------------------------------------
/spec/simple_navigation/adapters/sinatra_spec.rb:
--------------------------------------------------------------------------------
1 | describe SimpleNavigation::Adapters::Sinatra do
2 | let(:adapter) { SimpleNavigation::Adapters::Sinatra.new(context) }
3 | let(:context) { double(:context) }
4 | let(:request) { double(:request, fullpath: '/full?param=true', path: '/full') }
5 |
6 | before { allow(context).to receive_messages(request: request) }
7 |
8 | describe '#context_for_eval' do
9 | context "when adapter's context is not set" do
10 | it 'raises an exception' do
11 | allow(adapter).to receive_messages(context: nil)
12 | expect{ adapter.context_for_eval }.to raise_error(RuntimeError, 'no context set for evaluation the config file')
13 | end
14 | end
15 |
16 | context "when adapter's context is set" do
17 | it 'returns the context' do
18 | expect(adapter.context_for_eval).to be context
19 | end
20 | end
21 | end
22 |
23 | describe '#request_uri' do
24 | it 'returns the request.fullpath' do
25 | expect(adapter.request_uri).to eq '/full?param=true'
26 | end
27 | end
28 |
29 | describe '#request_path' do
30 | it 'returns the request.path' do
31 | expect(adapter.request_path).to eq '/full'
32 | end
33 | end
34 |
35 | describe '#current_page?' do
36 | before { allow(request).to receive_messages(scheme: 'http', host_with_port: 'my_host:5000') }
37 |
38 | shared_examples 'detecting current page' do |url, expected|
39 | context "when url is #{url}" do
40 | it "returns #{expected}" do
41 | expect(adapter.current_page?(url)).to be expected
42 | end
43 | end
44 | end
45 |
46 | context 'when URL is not encoded' do
47 | it_behaves_like 'detecting current page', '/full?param=true', true
48 | it_behaves_like 'detecting current page', '/full?param3=true', false
49 | it_behaves_like 'detecting current page', '/full', true
50 | it_behaves_like 'detecting current page', 'http://my_host:5000/full?param=true', true
51 | it_behaves_like 'detecting current page', 'http://my_host:5000/full?param3=true', false
52 | it_behaves_like 'detecting current page', 'http://my_host:5000/full', true
53 | it_behaves_like 'detecting current page', 'https://my_host:5000/full', false
54 | it_behaves_like 'detecting current page', 'http://my_host:6000/full', false
55 | it_behaves_like 'detecting current page', 'http://my_other_host:5000/full', false
56 | end
57 |
58 | context 'when URL is encoded' do
59 | before do
60 | allow(request).to receive_messages(fullpath: '/full%20with%20spaces?param=true',
61 | path: '/full%20with%20spaces')
62 | end
63 |
64 | it_behaves_like 'detecting current page', '/full%20with%20spaces?param=true', true
65 | it_behaves_like 'detecting current page', '/full%20with%20spaces?param3=true', false
66 | it_behaves_like 'detecting current page', '/full%20with%20spaces', true
67 | it_behaves_like 'detecting current page', 'http://my_host:5000/full%20with%20spaces?param=true', true
68 | it_behaves_like 'detecting current page', 'http://my_host:5000/full%20with%20spaces?param3=true', false
69 | it_behaves_like 'detecting current page', 'http://my_host:5000/full%20with%20spaces', true
70 | it_behaves_like 'detecting current page', 'https://my_host:5000/full%20with%20spaces', false
71 | it_behaves_like 'detecting current page', 'http://my_host:6000/full%20with%20spaces', false
72 | it_behaves_like 'detecting current page', 'http://my_other_host:5000/full%20with%20spaces', false
73 | end
74 | end
75 |
76 | describe '#link_to' do
77 | it 'returns a link with the correct class and id' do
78 | link = adapter.link_to('link', 'url', class: 'clazz', id: 'id')
79 | expect(link).to eq "
link"
80 | end
81 | end
82 |
83 | describe '#content_tag' do
84 | it 'returns a tag with the correct class and id' do
85 | tag = adapter.content_tag(:div, 'content', class: 'clazz', id: 'id')
86 | expect(tag).to eq "
content
"
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/spec/simple_navigation/renderer/breadcrumbs_spec.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | module Renderer
3 | describe Breadcrumbs do
4 | let!(:navigation) { setup_navigation('nav_id', 'nav_class') }
5 |
6 | let(:item) { nil }
7 | let(:options) {{ level: :all }}
8 | let(:output) { renderer.render(navigation) }
9 | let(:renderer) { Breadcrumbs.new(options) }
10 |
11 | before { select_an_item(navigation[item]) if item }
12 |
13 | describe '#render' do
14 | it "renders a 'div' tag for the navigation" do
15 | expect(output).to have_css('div')
16 | end
17 |
18 | it "sets the right html id on the rendered 'div' tag" do
19 | expect(output).to have_css('div#nav_id')
20 | end
21 |
22 | it "sets the right html classes on the rendered 'div' tag" do
23 | expect(output).to have_css('div.nav_class')
24 | end
25 |
26 | context 'when no item is selected' do
27 | it "doesn't render any 'a' tag in the 'div' tag" do
28 | expect(output).not_to have_css('div a')
29 | end
30 | end
31 |
32 | context 'when an item is selected' do
33 | let(:item) { :invoices }
34 |
35 | it "renders the selected 'a' tag" do
36 | expect(output).to have_css('div a')
37 | end
38 |
39 | it "remders the 'a' tag without any html id" do
40 | expect(output).not_to have_css('div a[id]')
41 | end
42 |
43 | it "renders the 'a' tag without any html class" do
44 | expect(output).not_to have_css('div a[class]')
45 | end
46 |
47 | context 'and the :allow_classes_and_ids option is true' do
48 | let(:options) {{ level: :all, allow_classes_and_ids: true }}
49 |
50 | it "renders the 'a' tag with the selected class" do
51 | expect(output).to have_css('div a.selected')
52 | end
53 |
54 | context "and the item hasn't any id explicitly set" do
55 | it "renders the 'a' tag without any html id" do
56 | expect(output).not_to have_css('div a[id]')
57 | end
58 | end
59 |
60 | context 'and the item has an explicitly set id' do
61 | let(:item) { :users }
62 |
63 | it "renders the 'a' tag with an html id" do
64 | expect(output).to have_css('div a#breadcrumb_users_link_id')
65 | end
66 | end
67 | end
68 | end
69 |
70 | context 'and the :prefix option is set' do
71 | let(:options) {{ prefix: 'You are here: ' }}
72 |
73 | context 'and there are no items to render' do
74 | let(:item) { nil }
75 |
76 | it "doesn't render the prefix before the breadcrumbs" do
77 | expect(output).not_to match(/^
You are here: /)
78 | end
79 | end
80 |
81 | context 'and there are items to render' do
82 | let(:item) { :invoices }
83 |
84 | it 'renders the prefix before the breadcrumbs' do
85 | expect(output).to match(/^You are here: /)
86 | end
87 | end
88 | end
89 |
90 | context 'when a sub navigation item is selected' do
91 | before do
92 | allow(navigation[:invoices]).to receive_messages(selected?: true)
93 |
94 | allow(navigation[:invoices].sub_navigation[:unpaid]).to \
95 | receive_messages(selected?: true, selected_by_condition?: true)
96 | end
97 |
98 | it 'renders all items as links' do
99 | expect(output).to have_css('div a', 2)
100 | end
101 |
102 | context 'when the :static_leaf option is true' do
103 | let(:options) {{ level: :all, static_leaf: true }}
104 |
105 | it 'renders the items as links' do
106 | expect(output).to have_css('div a')
107 | end
108 |
109 | it 'renders the last item as simple text' do
110 | expect(output).to have_css('div span')
111 | end
112 | end
113 | end
114 | end
115 | end
116 | end
117 | end
118 |
--------------------------------------------------------------------------------
/generators/navigation_config/templates/config/navigation.rb:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Configures your navigation
3 | SimpleNavigation::Configuration.run do |navigation|
4 | # Specify a custom renderer if needed.
5 | # The default renderer is SimpleNavigation::Renderer::List which renders HTML lists.
6 | # The renderer can also be specified as option in the render_navigation call.
7 | #navigation.renderer = Your::Custom::Renderer
8 |
9 | # Specify the class that will be applied to active navigation items. Defaults to 'selected'
10 | #navigation.selected_class = 'selected'
11 |
12 | # Specify the class that will be applied to the current leaf of
13 | # active navigation items. Defaults to 'simple-navigation-active-leaf'
14 | #navigation.active_leaf_class = 'simple-navigation-active-leaf'
15 |
16 | # Specify if item keys are added to navigation items as id. Defaults to true
17 | #navigation.autogenerate_item_ids = true
18 |
19 | # You can override the default logic that is used to autogenerate the item ids.
20 | # To do this, define a Proc which takes the key of the current item as argument.
21 | # The example below would add a prefix to each key.
22 | #navigation.id_generator = Proc.new {|key| "my-prefix-#{key}"}
23 |
24 | # If you need to add custom html around item names, you can define a proc that
25 | # will be called with the name you pass in to the navigation.
26 | # The example below shows how to wrap items spans.
27 | #navigation.name_generator = Proc.new {|name, item| tag.span(name) }
28 |
29 | # Specify if the auto highlight feature is turned on (globally, for the whole navigation). Defaults to true
30 | #navigation.auto_highlight = true
31 |
32 | # Specifies whether auto highlight should ignore query params and/or anchors when
33 | # comparing the navigation items with the current URL. Defaults to true
34 | #navigation.ignore_query_params_on_auto_highlight = true
35 | #navigation.ignore_anchors_on_auto_highlight = true
36 |
37 | # If this option is set to true, all item names will be considered as safe (passed through html_safe). Defaults to false.
38 | #navigation.consider_item_names_as_safe = false
39 |
40 | # Define the primary navigation
41 | navigation.items do |primary|
42 | # Add an item to the primary navigation. The following params apply:
43 | # key - a symbol which uniquely defines your navigation item in the scope of the primary_navigation
44 | # name - will be displayed in the rendered navigation. This can also be a call to your I18n-framework.
45 | # url - the address that the generated item links to. You can also use url_helpers (named routes, restful routes helper, url_for etc.)
46 | # options - can be used to specify attributes that will be included in the rendered navigation item (e.g. id, class etc.)
47 | # some special options that can be set:
48 | # :html - Specifies html attributes that will be included in the rendered navigation item
49 | # :if - Specifies a proc to call to determine if the item should
50 | # be rendered (e.g. if: -> { current_user.admin? }). The
51 | # proc should evaluate to a true or false value and is evaluated in the context of the view.
52 | # :unless - Specifies a proc to call to determine if the item should not
53 | # be rendered (e.g. unless: -> { current_user.admin? }). The
54 | # proc should evaluate to a true or false value and is evaluated in the context of the view.
55 | # :method - Specifies the http-method for the generated link - default is :get.
56 | # :highlights_on - if autohighlighting is turned off and/or you want to explicitly specify
57 | # when the item should be highlighted, you can set a regexp which is matched
58 | # against the current URI. You may also use a proc, or the symbol :subpath.
59 | #
60 | primary.item :key_1, 'name', url, options
61 |
62 | # Add an item which has a sub navigation (same params, but with block)
63 | primary.item :key_2, 'name', url, options do |sub_nav|
64 | # Add an item to the sub navigation (same params again)
65 | sub_nav.item :key_2_1, 'name', url, options
66 | end
67 |
68 | # You can also specify a condition-proc that needs to be fullfilled to display an item.
69 | # Conditions are part of the options. They are evaluated in the context of the views,
70 | # thus you can use all the methods and vars you have available in the views.
71 | primary.item :key_3, 'Admin', url, html: { class: 'special' }, if: -> { current_user.admin? }
72 | primary.item :key_4, 'Account', url, unless: -> { logged_in? }
73 |
74 | # you can also specify html attributes to attach to this particular level
75 | # works for all levels of the menu
76 | #primary.dom_attributes = {id: 'menu-id', class: 'menu-class'}
77 |
78 | # You can turn off auto highlighting for a specific level
79 | #primary.auto_highlight = false
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/spec/simple_navigation/configuration_spec.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | describe Configuration do
3 | subject(:config) { Configuration.instance }
4 |
5 | describe '.run' do
6 | it "yields the singleton Configuration object" do
7 | expect{ |blk| Configuration.run(&blk) }.to yield_with_args(config)
8 | end
9 | end
10 |
11 | describe '.eval_config' do
12 | let(:config_files) {{ default: 'default', my_context: 'my_context' }}
13 | let(:eval_context) { double(:eval_context) }
14 |
15 | before do
16 | allow(eval_context).to receive(:instance_eval)
17 | allow(SimpleNavigation).to \
18 | receive_messages(context_for_eval: eval_context, config_files: config_files)
19 | end
20 |
21 | context "with default navigation context" do
22 | it "calls instance_eval with the default config_file-string inside the context" do
23 | expect(eval_context).to receive(:instance_eval).with('default')
24 | Configuration.eval_config
25 | end
26 | end
27 |
28 | context 'with non default navigation context' do
29 | it "calls instance_eval with the specified config_file-string inside the context" do
30 | expect(eval_context).to receive(:instance_eval).with('my_context')
31 | Configuration.eval_config(:my_context)
32 | end
33 | end
34 | end
35 |
36 | describe '#initialize' do
37 | it 'sets the List-Renderer as default' do
38 | expect(config.renderer).to be Renderer::List
39 | end
40 |
41 | it 'sets the selected_class to "selected" as default' do
42 | expect(config.selected_class).to eq 'selected'
43 | end
44 |
45 | it 'sets the active_leaf_class to "simple-navigation-active-leaf" as default' do
46 | expect(config.active_leaf_class).to eq 'simple-navigation-active-leaf'
47 | end
48 |
49 | it 'sets autogenerate_item_ids to true as default' do
50 | expect(config.autogenerate_item_ids).to be true
51 | end
52 |
53 | it 'sets auto_highlight to true as default' do
54 | expect(config.auto_highlight).to be true
55 | end
56 |
57 | it 'sets the id_generator to a callable object' do
58 | expect(config.id_generator).to respond_to(:call)
59 | end
60 |
61 | it 'sets the name_generator to a callable object' do
62 | expect(config.name_generator).to respond_to(:call)
63 | end
64 |
65 | it 'sets the consider_item_names_as_safe to false' do
66 | expect(config.consider_item_names_as_safe).to be false
67 | end
68 |
69 | it 'sets highlights_on_subpath to false as default' do
70 | expect(config.highlight_on_subpath).to be false
71 | end
72 |
73 | it 'sets ignore_query_params_on_auto_highlight to true as default' do
74 | expect(config.ignore_query_params_on_auto_highlight).to be true
75 | end
76 |
77 | it 'sets ignore_anchors_on_auto_highlight to true as default' do
78 | expect(config.ignore_anchors_on_auto_highlight).to be true
79 | end
80 | end
81 |
82 | describe '#items' do
83 | let(:container) { double(:items_container) }
84 |
85 | before { allow(ItemContainer).to receive_messages(new: container) }
86 |
87 | context 'when a block is given' do
88 | context 'and items_provider is specified' do
89 | let(:provider) { double(:provider) }
90 |
91 | it 'raises an exception' do
92 | expect{ config.items(provider) {} }.to raise_error(RuntimeError, 'please specify either items_provider or block, but not both')
93 | end
94 | end
95 |
96 | context 'when no items_provider is specified' do
97 | it 'yields an new ItemContainer' do
98 | expect{ |blk| config.items(&blk) }.to yield_with_args(container)
99 | end
100 |
101 | it 'assigns the ItemContainer to an instance-var' do
102 | config.items {}
103 | expect(config.primary_navigation).to be container
104 | end
105 |
106 | it "doesn't set the items on the container" do
107 | expect(container).not_to receive(:items=)
108 | config.items {}
109 | end
110 | end
111 | end
112 |
113 | context 'when no block is given' do
114 | context 'and items_provider is specified' do
115 | let(:external_provider) { double(:external_provider) }
116 | let(:items) { double(:items) }
117 | let(:items_provider) { double(:items_provider, items: items) }
118 |
119 | before do
120 | allow(SimpleNavigation::ItemsProvider).to receive_messages(new: items_provider)
121 | allow(container).to receive(:items=)
122 | end
123 |
124 | it 'creates a new Provider object for the specified provider' do
125 | expect(ItemsProvider).to receive(:new).with(external_provider)
126 | config.items(external_provider)
127 | end
128 |
129 | it 'calls items on the provider object' do
130 | expect(items_provider).to receive(:items)
131 | config.items(external_provider)
132 | end
133 |
134 | it 'sets the items on the container' do
135 | expect(container).to receive(:items=).with(items)
136 | config.items(external_provider)
137 | end
138 | end
139 |
140 | context 'when items_provider is not specified' do
141 | it "raises an exception" do
142 | expect{ config.items }.to raise_error(RuntimeError, 'please specify either items_provider or block, but not both')
143 | end
144 | end
145 | end
146 | end
147 |
148 | describe '#loaded?' do
149 | context 'when primary_nav is set' do
150 | it 'returns true' do
151 | config.instance_variable_set(:@primary_navigation, :bla)
152 | expect(config).to be_loaded
153 | end
154 | end
155 |
156 | context 'when primary_nav is not set' do
157 | it 'returns false if no primary_nav is set' do
158 | config.instance_variable_set(:@primary_navigation, nil)
159 | expect(config).not_to be_loaded
160 | end
161 | end
162 | end
163 | end
164 | end
165 |
--------------------------------------------------------------------------------
/spec/simple_navigation/item_adapter_spec.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | describe ItemAdapter do
3 | let(:item_adapter) { ItemAdapter.new(item) }
4 |
5 | context 'when item is an object' do
6 | let(:item) { double(:item, key: 'key', name: 'name', url: 'url') }
7 |
8 | shared_examples 'delegating to item' do |meth|
9 | it "delegates #{meth} to item" do
10 | expect(item).to receive(meth)
11 | item_adapter.public_send(meth)
12 | end
13 | end
14 |
15 | it_behaves_like 'delegating to item', :key
16 | it_behaves_like 'delegating to item', :url
17 | it_behaves_like 'delegating to item', :name
18 |
19 | describe '#initialize' do
20 | it 'sets the item' do
21 | expect(item_adapter.item).to be item
22 | end
23 | end
24 |
25 | describe '#options' do
26 | context 'when item responds to options' do
27 | let(:options) { double(:options) }
28 |
29 | before { allow(item).to receive_messages(options: options) }
30 |
31 | it "returns the item's options" do
32 | expect(item_adapter.options).to be options
33 | end
34 | end
35 |
36 | context 'item does not respond to options' do
37 | it 'returns an empty hash' do
38 | expect(item_adapter.options).to eq({})
39 | end
40 | end
41 | end
42 |
43 | describe '#items' do
44 | context 'when item responds to items' do
45 | context 'and items is nil' do
46 | before { allow(item).to receive_messages(items: nil) }
47 |
48 | it 'returns nil' do
49 | expect(item_adapter.items).to be_nil
50 | end
51 | end
52 |
53 | context 'when items is not nil' do
54 | context 'and items is empty' do
55 | before { allow(item).to receive_messages(items: []) }
56 |
57 | it 'returns nil' do
58 | expect(item_adapter.items).to be_nil
59 | end
60 | end
61 |
62 | context 'and items is not empty' do
63 | let(:items) { [true] }
64 |
65 | before { allow(item).to receive_messages(items: items) }
66 |
67 | it 'returns the items' do
68 | expect(item_adapter.items).to eq items
69 | end
70 | end
71 | end
72 | end
73 |
74 | context "when item doesn't respond to items" do
75 | it 'returns nil' do
76 | expect(item_adapter.items).to be_nil
77 | end
78 | end
79 | end
80 |
81 | describe '#to_simple_navigation_item' do
82 | let(:container) { double(:container) }
83 |
84 | before { allow(item).to receive_messages(items: [], options: {}) }
85 |
86 | it 'creates an Item' do
87 | expect(Item).to receive(:new)
88 | .with(container, 'key', 'name', 'url', {})
89 | item_adapter.to_simple_navigation_item(container)
90 | end
91 | end
92 | end
93 |
94 | context 'when item is a kind of hash' do
95 | class ModifiedHash < Hash; end
96 |
97 | let(:item) { ModifiedHash[key: 'key', url: 'url', name: 'name'] }
98 |
99 | shared_examples 'delegating to item' do |meth|
100 | it "delegates #{meth} to item" do
101 | expect(item_adapter.item).to receive(meth)
102 | item_adapter.public_send(meth)
103 | end
104 | end
105 |
106 | it_behaves_like 'delegating to item', :key
107 | it_behaves_like 'delegating to item', :url
108 | it_behaves_like 'delegating to item', :name
109 |
110 | describe '#initialize' do
111 | it 'sets the item' do
112 | expect(item_adapter.item).not_to be_nil
113 | end
114 |
115 | it 'converts the item into an object' do
116 | expect(item_adapter.item).to respond_to(:url)
117 | end
118 | end
119 |
120 | describe '#options' do
121 | context 'when item responds to options' do
122 | before { item[:options] = { my: :options } }
123 |
124 | it "returns the item's options" do
125 | expect(item_adapter.options).to eq({ my: :options })
126 | end
127 | end
128 |
129 | context 'when item does not respond to options' do
130 | it 'returns an empty hash' do
131 | expect(item_adapter.options).to eq({})
132 | end
133 | end
134 | end
135 |
136 | describe '#items' do
137 | context 'when item responds to items' do
138 | context 'and items is nil' do
139 | before { item[:items] = nil }
140 |
141 | it 'returns nil' do
142 | expect(item_adapter.items).to be_nil
143 | end
144 | end
145 |
146 | context 'when items is not nil' do
147 | context 'and items is empty' do
148 | it 'returns nil' do
149 | expect(item_adapter.items).to be_nil
150 | end
151 | end
152 |
153 | context 'and items is not empty' do
154 | before { item[:items] = ['not', 'empty'] }
155 |
156 | it 'returns the items' do
157 | expect(item_adapter.items).to eq ['not', 'empty']
158 | end
159 | end
160 | end
161 | end
162 |
163 | context 'when item does not respond to items' do
164 | it 'returns nil' do
165 | expect(item_adapter.items).to be_nil
166 | end
167 | end
168 | end
169 |
170 | describe '#to_simple_navigation_item' do
171 | let(:container) { double(:container) }
172 |
173 | before { item.merge(options: {}) }
174 |
175 | it 'passes the right arguments to Item' do
176 | expect(Item).to receive(:new)
177 | .with(container, 'key', 'name', 'url', {})
178 | item_adapter.to_simple_navigation_item(container)
179 | end
180 |
181 | it 'creates an Item' do
182 | created_item = item_adapter.to_simple_navigation_item(container)
183 | expect(created_item).to be_an(Item)
184 | end
185 | end
186 | end
187 | end
188 | end
189 |
--------------------------------------------------------------------------------
/lib/simple_navigation/item.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | # Represents an item in your navigation.
3 | # Gets generated by the item method in the config-file.
4 | class Item
5 | attr_reader :key,
6 | :name,
7 | :sub_navigation,
8 | :url,
9 | :options
10 |
11 | # see ItemContainer#item
12 | #
13 | # The subnavigation (if any) is either provided by a block or
14 | # passed in directly as items
15 | def initialize(container, key, name, url = nil, opts = {}, &sub_nav_block)
16 | self.container = container
17 | self.key = key
18 | self.name = name.respond_to?(:call) ? name.call : name
19 | self.url = url.respond_to?(:call) ? url.call : url
20 | self.options = opts
21 |
22 | setup_sub_navigation(options[:items], &sub_nav_block)
23 | end
24 |
25 | # Returns the item's name.
26 | # If :apply_generator option is set to true (default),
27 | # the name will be passed to the name_generator specified
28 | # in the configuration.
29 | #
30 | def name(options = {})
31 | options = { apply_generator: true }.merge(options)
32 | if options[:apply_generator]
33 | config.name_generator.call(@name, self)
34 | else
35 | @name
36 | end
37 | end
38 |
39 | # Returns true if this navigation item should be rendered as 'selected'.
40 | # An item is selected if
41 | #
42 | # * it has a subnavigation and one of its subnavigation items is selected or
43 | # * its url matches the url of the current request (auto highlighting)
44 | #
45 | def selected?
46 | @selected ||= selected_by_subnav? || selected_by_condition?
47 | end
48 |
49 | # Returns the html-options hash for the item, i.e. the options specified
50 | # for this item in the config-file.
51 | # It also adds the 'selected' class to the list of classes if necessary.
52 | def html_options
53 | html_opts = options.fetch(:html) { Hash.new }
54 | html_opts[:id] ||= autogenerated_item_id
55 |
56 | classes = [html_opts[:class], selected_class, active_leaf_class]
57 | classes = classes.flatten.compact.join(' ')
58 | html_opts[:class] = classes if classes && !classes.empty?
59 |
60 | html_opts
61 | end
62 |
63 | # Returns the configured active_leaf_class if the item is the selected leaf,
64 | # nil otherwise
65 | def active_leaf_class
66 | if !selected_by_subnav? && selected_by_condition?
67 | config.active_leaf_class
68 | end
69 | end
70 |
71 | # Returns the configured selected_class if the item is selected,
72 | # nil otherwise
73 | def selected_class
74 | if selected?
75 | container.selected_class || config.selected_class
76 | end
77 | end
78 |
79 | # Returns the :highlights_on option as set at initialization
80 | def highlights_on
81 | @highlights_on ||= options[:highlights_on]
82 | end
83 |
84 | # Returns the :method option as set at initialization
85 | def method
86 | @method ||= options[:method]
87 | end
88 |
89 | # Returns the html attributes for the link as set with the :link_html option
90 | # at initialization
91 | def link_html_options
92 | @link_html_options ||= options[:link_html]
93 | end
94 |
95 | protected
96 |
97 | # Returns true if item has a subnavigation and
98 | # the sub_navigation is selected
99 | def selected_by_subnav?
100 | sub_navigation && sub_navigation.selected?
101 | end
102 |
103 | # Returns true if the item's url matches the request's current url.
104 | def selected_by_condition?
105 | highlights_on ? selected_by_highlights_on? : selected_by_autohighlight?
106 | end
107 |
108 | # Returns true if both the item's url and the request's url are root_path
109 | def root_path_match?
110 | url == '/' && SimpleNavigation.request_path == '/'
111 | end
112 |
113 | # Returns the item's id which is added to the rendered output.
114 | def autogenerated_item_id
115 | config.id_generator.call(key) if config.autogenerate_item_ids
116 | end
117 |
118 | # Return true if auto_highlight is on for this item.
119 | def auto_highlight?
120 | config.auto_highlight && container.auto_highlight
121 | end
122 |
123 | private
124 |
125 | attr_accessor :container
126 |
127 | attr_writer :key,
128 | :name,
129 | :sub_navigation,
130 | :url,
131 | :options
132 |
133 | def config
134 | SimpleNavigation.config
135 | end
136 |
137 | def request_uri
138 | SimpleNavigation.request_uri
139 | end
140 |
141 | def remove_anchors(url_with_anchors)
142 | url_with_anchors && url_with_anchors.split('#').first
143 | end
144 |
145 | def remove_query_params(url_with_params)
146 | url_with_params && url_with_params.split('?').first
147 | end
148 |
149 | def url_for_autohighlight
150 | relevant_url = remove_anchors(self.url) if config.ignore_anchors_on_auto_highlight
151 | relevant_url = remove_query_params(relevant_url) if config.ignore_query_params_on_auto_highlight
152 | relevant_url
153 | end
154 |
155 | def selected_by_autohighlight?
156 | return false unless auto_highlight?
157 | return false unless self.url
158 |
159 | root_path_match? ||
160 | (url_for_autohighlight && SimpleNavigation.current_page?(url_for_autohighlight)) ||
161 | autohighlight_by_subpath?
162 | end
163 |
164 | def autohighlight_by_subpath?
165 | config.highlight_on_subpath && selected_by_subpath?
166 | end
167 |
168 | def selected_by_highlights_on?
169 | case highlights_on
170 | when Regexp then !!(request_uri =~ highlights_on)
171 | when Proc then highlights_on.call
172 | when :subpath then selected_by_subpath?
173 | else
174 | fail ArgumentError, ':highlights_on must be a Regexp, Proc or :subpath'
175 | end
176 | end
177 |
178 | def selected_by_subpath?
179 | escaped_url = Regexp.escape(url_for_autohighlight)
180 | !!(request_uri =~ /^#{escaped_url}(\/|$||\?)/i)
181 | end
182 |
183 | def setup_sub_navigation(items = nil, &sub_nav_block)
184 | return unless sub_nav_block || items
185 |
186 | self.sub_navigation = ItemContainer.new(container.level + 1)
187 |
188 | if sub_nav_block
189 | sub_nav_block.call sub_navigation
190 | else
191 | sub_navigation.items = items
192 | end
193 | end
194 | end
195 | end
196 |
--------------------------------------------------------------------------------
/lib/simple_navigation.rb:
--------------------------------------------------------------------------------
1 | # cherry picking active_support stuff
2 | require 'active_support/core_ext/array'
3 | require 'active_support/core_ext/hash'
4 | require 'active_support/core_ext/module/attribute_accessors'
5 |
6 | require 'simple_navigation/version'
7 | require 'simple_navigation/configuration'
8 | require 'simple_navigation/item_adapter'
9 | require 'simple_navigation/item'
10 | require 'simple_navigation/item_container'
11 | require 'simple_navigation/items_provider'
12 | require 'simple_navigation/renderer'
13 | require 'simple_navigation/adapters'
14 | require 'simple_navigation/config_file_finder'
15 | require 'simple_navigation/railtie' if defined?(::Rails::Railtie)
16 |
17 | require 'forwardable'
18 |
19 | # A plugin for generating a simple navigation. See README for resources on
20 | # usage instructions.
21 | module SimpleNavigation
22 | mattr_accessor :adapter,
23 | :adapter_class,
24 | :config_files,
25 | :config_file_paths,
26 | :default_renderer,
27 | :environment,
28 | :registered_renderers,
29 | :root
30 |
31 | # Cache for loaded config files
32 | self.config_files = {}
33 |
34 | # Allows for multiple config_file_paths. Needed if a plugin itself uses
35 | # simple-navigation and therefore has its own config file
36 | self.config_file_paths = []
37 |
38 | # Maps renderer keys to classes. The keys serve as shortcut in the
39 | # render_navigation calls (renderer: :list)
40 | self.registered_renderers = {
41 | list: SimpleNavigation::Renderer::List,
42 | links: SimpleNavigation::Renderer::Links,
43 | breadcrumbs: SimpleNavigation::Renderer::Breadcrumbs,
44 | text: SimpleNavigation::Renderer::Text,
45 | json: SimpleNavigation::Renderer::Json
46 | }
47 |
48 | class << self
49 | extend Forwardable
50 |
51 | def_delegators :adapter, :context_for_eval,
52 | :current_page?,
53 | :request,
54 | :request_path,
55 | :request_uri
56 |
57 | def_delegators :adapter_class, :register
58 |
59 | # Sets the root path and current environment as specified. Also sets the
60 | # default config_file_path.
61 | def set_env(root, environment)
62 | self.root = root
63 | self.environment = environment
64 | config_file_paths << default_config_file_path
65 | end
66 |
67 | # Returns the current framework in which the plugin is running.
68 | def framework
69 | return :rails if defined?(Rails)
70 | return :padrino if defined?(Padrino)
71 | return :sinatra if defined?(Sinatra)
72 | return :nanoc if defined?(Nanoc3)
73 | fail 'simple_navigation currently only works for Rails, Sinatra and ' \
74 | 'Padrino apps'
75 | end
76 |
77 | # Loads the adapter for the current framework
78 | def load_adapter
79 | self.adapter_class =
80 | case framework
81 | when :rails then SimpleNavigation::Adapters::Rails
82 | when :sinatra then SimpleNavigation::Adapters::Sinatra
83 | when :padrino then SimpleNavigation::Adapters::Padrino
84 | when :nanoc then SimpleNavigation::Adapters::Nanoc
85 | end
86 | end
87 |
88 | # Creates a new adapter instance based on the context in which
89 | # render_navigation has been called.
90 | def init_adapter_from(context)
91 | self.adapter = adapter_class.new(context)
92 | end
93 |
94 | def default_config_file_path
95 | File.join(root, 'config')
96 | end
97 |
98 | # Resets the list of config_file_paths to the specified path
99 | def config_file_path=(path)
100 | self.config_file_paths = [path]
101 | end
102 |
103 | # Reads the config_file for the specified navigation_context and stores it
104 | # for later evaluation.
105 | def load_config(navigation_context = :default)
106 | if environment == 'production'
107 | update_config(navigation_context)
108 | else
109 | update_config!(navigation_context)
110 | end
111 | end
112 |
113 | # Returns the singleton instance of the SimpleNavigation::Configuration
114 | def config
115 | SimpleNavigation::Configuration.instance
116 | end
117 |
118 | # Returns the ItemContainer that contains the items for the
119 | # primary navigation
120 | def primary_navigation
121 | config.primary_navigation
122 | end
123 |
124 | # Returns the active item container for the specified level.
125 | # Valid levels are
126 | # * :all - in this case the primary_navigation is returned.
127 | # * :leaves - the 'deepest' active item_container will be returned
128 | # * a specific level - the active item_container for the specified level
129 | # will be returned
130 | # * a range of levels - the active item_container for the range's minimum
131 | # will be returned
132 | #
133 | # Returns nil if there is no active item_container for the specified level.
134 | def active_item_container_for(level)
135 | case level
136 | when :all then primary_navigation
137 | when :leaves then primary_navigation.active_leaf_container
138 | when Integer then primary_navigation.active_item_container_for(level)
139 | when Range then primary_navigation.active_item_container_for(level.min)
140 | else
141 | fail ArgumentError, "Invalid navigation level: #{level}"
142 | end
143 | end
144 |
145 | # Registers a renderer.
146 | #
147 | # === Example
148 | # To register your own renderer:
149 | #
150 | # SimpleNavigation.register_renderer my_renderer: My::RendererClass
151 | #
152 | # Then in the view you can call:
153 | #
154 | # render_navigation(renderer: :my_renderer)
155 | def register_renderer(renderer_hash)
156 | registered_renderers.merge!(renderer_hash)
157 | end
158 |
159 | private
160 |
161 | def config_file(navigation_context)
162 | ConfigFileFinder.new(config_file_paths).find(navigation_context)
163 | end
164 |
165 | def read_config(navigation_context)
166 | File.read config_file(navigation_context)
167 | end
168 |
169 | def update_config(navigation_context)
170 | config_files[navigation_context] ||= read_config(navigation_context)
171 | end
172 |
173 | def update_config!(navigation_context)
174 | config_files[navigation_context] = read_config(navigation_context)
175 | end
176 | end
177 | end
178 |
179 | SimpleNavigation.load_adapter
180 |
--------------------------------------------------------------------------------
/spec/simple_navigation_spec.rb:
--------------------------------------------------------------------------------
1 | describe SimpleNavigation do
2 | before { subject.config_file_path = 'path_to_config' }
3 |
4 | describe 'config_file_path=' do
5 | before { subject.config_file_paths = ['existing_path'] }
6 |
7 | it 'overrides the config_file_paths' do
8 | subject.config_file_path = 'new_path'
9 | expect(subject.config_file_paths).to eq ['new_path']
10 | end
11 | end
12 |
13 | describe '.default_config_file_path' do
14 | before { allow(subject).to receive_messages(root: 'root') }
15 |
16 | it 'returns the config file path according to :root setting' do
17 | expect(subject.default_config_file_path).to eq 'root/config'
18 | end
19 | end
20 |
21 | describe 'Regarding renderers' do
22 | it 'registers the builtin renderers by default' do
23 | expect(subject.registered_renderers).not_to be_empty
24 | end
25 |
26 | describe '.register_renderer' do
27 | let(:renderer) { double(:renderer) }
28 |
29 | it 'adds the specified renderer to the list of renderers' do
30 | subject.register_renderer(my_renderer: renderer)
31 | expect(subject.registered_renderers[:my_renderer]).to be renderer
32 | end
33 | end
34 | end
35 |
36 | describe '.set_env' do
37 | before do
38 | subject.config_file_paths = []
39 | allow(subject).to receive_messages(default_config_file_path: 'default_path')
40 | subject.set_env('root', 'my_env')
41 | end
42 |
43 | it 'sets the root' do
44 | expect(subject.root).to eq 'root'
45 | end
46 |
47 | it 'sets the environment' do
48 | expect(subject.environment).to eq 'my_env'
49 | end
50 |
51 | it 'adds the default-config path to the list of config_file_paths' do
52 | expect(subject.config_file_paths).to eq ['default_path']
53 | end
54 | end
55 |
56 | describe '.load_config', memfs: true do
57 | let(:paths) { ['/path/one', '/path/two'] }
58 |
59 | before do
60 | FileUtils.mkdir_p(paths)
61 | allow(subject).to receive_messages(config_file_paths: paths)
62 | end
63 |
64 | context 'when the config file for the context exists' do
65 | before do
66 | File.open('/path/two/navigation.rb', 'w') { |f| f.puts 'default content' }
67 | File.open('/path/one/other_navigation.rb', 'w') { |f| f.puts 'other content' }
68 | end
69 |
70 | context 'when no context is provided' do
71 | it 'stores the configuration in config_files for the default context' do
72 | subject.load_config
73 | expect(subject.config_files[:default]).to eq "default content\n"
74 | end
75 | end
76 |
77 | context 'when a context is provided' do
78 | it 'stores the configuration in config_files for the given context' do
79 | subject.load_config(:other)
80 | expect(subject.config_files[:other]).to eq "other content\n"
81 | end
82 | end
83 |
84 | context 'and environment is production' do
85 | before { allow(subject).to receive_messages(environment: 'production') }
86 |
87 | it 'loads the config file only for the first call' do
88 | subject.load_config
89 | File.open('/path/two/navigation.rb', 'w') { |f| f.puts 'new content' }
90 | subject.load_config
91 | expect(subject.config_files[:default]).to eq "default content\n"
92 | end
93 | end
94 |
95 | context "and environment isn't production" do
96 | it 'loads the config file for every call' do
97 | subject.load_config
98 | File.open('/path/two/navigation.rb', 'w') { |f| f.puts 'new content' }
99 | subject.load_config
100 | expect(subject.config_files[:default]).to eq "new content\n"
101 | end
102 | end
103 | end
104 |
105 | context "when the config file for the context doesn't exists" do
106 | it 'raises an exception' do
107 | expect{ subject.load_config }.to raise_error(RuntimeError, /Config file 'navigation.rb' not found in path\(s\)/)
108 | end
109 | end
110 | end
111 |
112 | describe '.config' do
113 | it 'returns the Configuration singleton instance' do
114 | expect(subject.config).to be SimpleNavigation::Configuration.instance
115 | end
116 | end
117 |
118 | describe '.active_item_container_for' do
119 | let(:primary) { double(:primary) }
120 |
121 | before { allow(subject.config).to receive_messages(primary_navigation: primary) }
122 |
123 | context 'when level is :all' do
124 | it 'returns the primary_navigation' do
125 | nav = subject.active_item_container_for(:all)
126 | expect(nav).to be primary
127 | end
128 | end
129 |
130 | context 'when level is :leaves' do
131 | it 'returns the currently active leaf-container' do
132 | expect(primary).to receive(:active_leaf_container)
133 | subject.active_item_container_for(:leaves)
134 | end
135 | end
136 |
137 | context 'when level is a Range' do
138 | it 'takes the min of the range to lookup the active container' do
139 | expect(primary).to receive(:active_item_container_for).with(2)
140 | subject.active_item_container_for(2..3)
141 | end
142 | end
143 |
144 | context 'when level is an Integer' do
145 | it 'considers the Integer to lookup the active container' do
146 | expect(primary).to receive(:active_item_container_for).with(1)
147 | subject.active_item_container_for(1)
148 | end
149 | end
150 |
151 | context 'when level is something else' do
152 | it 'raises an exception' do
153 | expect{
154 | subject.active_item_container_for('something else')
155 | }.to raise_error(ArgumentError, 'Invalid navigation level: something else')
156 | end
157 | end
158 | end
159 |
160 | describe '.load_adapter' do
161 | shared_examples 'loading the right adapter' do |framework, adapter|
162 | context "when the context is #{framework}" do
163 | before do
164 | allow(subject).to receive_messages(framework: framework)
165 | subject.load_adapter
166 | end
167 |
168 | it "returns the #{framework} adapter" do
169 | adapter_class = SimpleNavigation::Adapters.const_get(adapter)
170 | expect(subject.adapter_class).to be adapter_class
171 | end
172 | end
173 | end
174 |
175 | it_behaves_like 'loading the right adapter', :rails, :Rails
176 | it_behaves_like 'loading the right adapter', :padrino, :Padrino
177 | it_behaves_like 'loading the right adapter', :sinatra, :Sinatra
178 | end
179 |
180 | describe '.init_adapter_from' do
181 | let(:adapter) { double(:adapter) }
182 | let(:adapter_class) { double(:adapter_class, new: adapter) }
183 |
184 | it 'sets the adapter to a new instance of adapter_class' do
185 | subject.adapter_class = adapter_class
186 | subject.init_adapter_from(:default)
187 | expect(subject.adapter).to be adapter
188 | end
189 | end
190 | end
191 |
--------------------------------------------------------------------------------
/lib/simple_navigation/item_container.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | # Holds the Items for a navigation 'level'.
3 | class ItemContainer
4 | attr_accessor :auto_highlight,
5 | :dom_class,
6 | :dom_id,
7 | :renderer,
8 | :selected_class
9 |
10 | attr_reader :items, :level
11 |
12 | attr_writer :dom_attributes
13 |
14 | def initialize(level = 1) #:nodoc:
15 | @level = level
16 | @items ||= []
17 | @renderer = SimpleNavigation.config.renderer
18 | @auto_highlight = true
19 | @dom_attributes = {}
20 | end
21 |
22 | def dom_attributes
23 | # backward compability for #dom_id and #dom_class
24 | dom_id_and_class = {
25 | id: dom_id,
26 | class: dom_class
27 | }.reject { |_, v| v.nil? }
28 |
29 | @dom_attributes.merge(dom_id_and_class)
30 | end
31 |
32 | # Creates a new navigation item.
33 | #
34 | # The key is a symbol which uniquely defines your navigation item
35 | # in the scope of the primary_navigation or the sub_navigation.
36 | #
37 | # The name will be displayed in the rendered navigation.
38 | # This can also be a call to your I18n-framework.
39 | #
40 | # The url is the address that the generated item points to.
41 | # You can also use url_helpers (named routes, restful routes helper,
42 | # url_for, etc). url is optional - items without URLs should not
43 | # be rendered as links.
44 | #
45 | # The options can be used to specify the following things:
46 | # * any html_attributes - will be included in the rendered
47 | # navigation item (e.g. id, class etc.)
48 | # * :if - Specifies a proc to call to determine if the item should
49 | # be rendered (e.g. if: Proc.new { current_user.admin? }). The
50 | # proc should evaluate to a true or false value and is evaluated
51 | # in the context of the view.
52 | # * :unless - Specifies a proc to call to determine if the item
53 | # should not be rendered
54 | # (e.g. unless: Proc.new { current_user.admin? }).
55 | # The proc should evaluate to a true or false value and is evaluated in
56 | # the context of the view.
57 | # * :method - Specifies the http-method for the generated link -
58 | # default is :get.
59 | # * :highlights_on - if autohighlighting is turned off and/or you
60 | # want to explicitly specify when the item should be highlighted, you can
61 | # set a regexp which is matched againstthe current URI.
62 | #
63 | # The block - if specified - will hold the item's sub_navigation.
64 | def item(key, name, url = nil, options = {}, &block)
65 | return unless should_add_item?(options)
66 | item = Item.new(self, key, name, url, options, &block)
67 | add_item item, options
68 | end
69 |
70 | def items=(new_items)
71 | new_items.each do |item|
72 | item_adapter = ItemAdapter.new(item)
73 | next unless should_add_item?(item_adapter.options)
74 | add_item item_adapter.to_simple_navigation_item(self), item_adapter.options
75 | end
76 | end
77 |
78 | # Returns the Item with the specified key, nil otherwise.
79 | #
80 | def [](navi_key)
81 | items.find { |item| item.key == navi_key }
82 | end
83 |
84 | # Returns the level of the item specified by navi_key.
85 | # Recursively works its way down the item's sub_navigations if the desired
86 | # item is not found directly in this container's items.
87 | # Returns nil if item cannot be found.
88 | #
89 | def level_for_item(navi_key)
90 | return level if self[navi_key]
91 |
92 | items.each do |item|
93 | next unless item.sub_navigation
94 | level = item.sub_navigation.level_for_item(navi_key)
95 | return level if level
96 | end
97 | return nil
98 | end
99 |
100 | # Renders the items in this ItemContainer using the configured renderer.
101 | #
102 | # The options are the same as in the view's render_navigation call
103 | # (they get passed on)
104 | def render(options = {})
105 | renderer_instance(options).render(self)
106 | end
107 |
108 | # Returns true if any of this container's items is selected.
109 | #
110 | def selected?
111 | items.any?(&:selected?)
112 | end
113 |
114 | # Returns the currently selected item, nil if no item is selected.
115 | #
116 | def selected_item
117 | items.find(&:selected?)
118 | end
119 |
120 | # Returns the active item_container for the specified level
121 | # (recursively looks up items in selected sub_navigation if level is deeper
122 | # than this container's level).
123 | def active_item_container_for(desired_level)
124 | if level == desired_level
125 | self
126 | elsif selected_sub_navigation?
127 | selected_item.sub_navigation.active_item_container_for(desired_level)
128 | end
129 | end
130 |
131 | # Returns the deepest possible active item_container.
132 | # (recursively searches in the sub_navigation if this container has a
133 | # selected sub_navigation).
134 | def active_leaf_container
135 | if selected_sub_navigation?
136 | selected_item.sub_navigation.active_leaf_container
137 | else
138 | self
139 | end
140 | end
141 |
142 | # Returns true if there are no items defined for this container.
143 | def empty?
144 | items.empty?
145 | end
146 |
147 | private
148 |
149 | def add_item(item, options)
150 | items << item
151 | modify_dom_attributes(options)
152 | end
153 |
154 | def modify_dom_attributes(options)
155 | return unless container_options = options[:container]
156 | self.dom_attributes = container_options.fetch(:attributes) { dom_attributes }
157 | self.dom_class = container_options.fetch(:class) { dom_class }
158 | self.dom_id = container_options.fetch(:id) { dom_id }
159 | self.selected_class = container_options.fetch(:selected_class) { selected_class }
160 | end
161 |
162 | # FIXME: raise an exception if :rederer is a symbol and it is not registred
163 | # in SimpleNavigation.registered_renderers
164 | def renderer_instance(options)
165 | return renderer.new(options) unless options[:renderer]
166 |
167 | if options[:renderer].is_a?(Symbol)
168 | registered_renderer = SimpleNavigation.registered_renderers[options[:renderer]]
169 | registered_renderer.new(options)
170 | else
171 | options[:renderer].new(options)
172 | end
173 | end
174 |
175 | def selected_sub_navigation?
176 | !!(selected_item && selected_item.sub_navigation)
177 | end
178 |
179 | def should_add_item?(options)
180 | [options[:if]].flatten.compact.all? { |m| evaluate_method(m) } &&
181 | [options[:unless]].flatten.compact.none? { |m| evaluate_method(m) }
182 | end
183 |
184 | def evaluate_method(method)
185 | case method
186 | when Proc, Method then method.call
187 | else fail(ArgumentError, ':if or :unless must be procs or lambdas')
188 | end
189 | end
190 | end
191 | end
192 |
--------------------------------------------------------------------------------
/spec/simple_navigation/renderer/base_spec.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | module Renderer
3 | describe Base do
4 | subject(:base) { Base.new(options) }
5 |
6 | let(:adapter) { double(:adapter) }
7 | let(:options) { Hash.new }
8 |
9 | before { allow(SimpleNavigation).to receive_messages(adapter: adapter) }
10 |
11 | it 'delegates the :link_to method to adapter' do
12 | allow(adapter).to receive_messages(link_to: 'link_to')
13 | expect(base.link_to).to eq 'link_to'
14 | end
15 |
16 | it 'delegates the :content_tag method to adapter' do
17 | allow(adapter).to receive_messages(content_tag: 'content_tag')
18 | expect(base.content_tag).to eq 'content_tag'
19 | end
20 |
21 | describe '#initialize' do
22 | it "sets the renderer adapter to the SimpleNavigation one" do
23 | expect(base.adapter).to be adapter
24 | end
25 | end
26 |
27 | describe '#options' do
28 | it "returns the renderer's options" do
29 | expect(base.options).to be options
30 | end
31 | end
32 |
33 | describe '#render' do
34 | it "raise an exception to indicate it's a subclass responsibility" do
35 | expect{ base.render(:container) }.to raise_error(NotImplementedError, 'subclass responsibility')
36 | end
37 | end
38 |
39 | describe '#expand_all?' do
40 | context 'when :options is set' do
41 | context 'and the :expand_all option is true' do
42 | let(:options) {{ expand_all: true }}
43 |
44 | it 'returns true' do
45 | expect(base.expand_all?).to be true
46 | end
47 | end
48 |
49 | context 'and the :expand_all option is false' do
50 | let(:options) {{ expand_all: false }}
51 |
52 | it 'returns false' do
53 | expect(base.expand_all?).to be false
54 | end
55 | end
56 | end
57 |
58 | context "when :options isn't set" do
59 | let(:options) { Hash.new }
60 |
61 | it 'returns false' do
62 | expect(base.expand_all?).to be false
63 | end
64 | end
65 | end
66 |
67 | describe '#skip_if_empty?' do
68 | context 'when :options is set' do
69 | context 'and the :skip_if_empty option is true' do
70 | let(:options) {{ skip_if_empty: true }}
71 |
72 | it 'returns true' do
73 | expect(base.skip_if_empty?).to be true
74 | end
75 | end
76 |
77 | context 'and the :skip_if_empty option is false' do
78 | let(:options) {{ skip_if_empty: false }}
79 |
80 | it 'returns true' do
81 | expect(base.skip_if_empty?).to be false
82 | end
83 | end
84 | end
85 |
86 | context "when :options isn't set" do
87 | let(:options) { Hash.new }
88 |
89 | it 'returns true' do
90 | expect(base.skip_if_empty?).to be false
91 | end
92 | end
93 | end
94 |
95 | describe '#level' do
96 | context 'and the :level option is set' do
97 | let(:options) {{ level: 1 }}
98 |
99 | it 'returns the specified level' do
100 | expect(base.level).to eq 1
101 | end
102 | end
103 |
104 | context "and the :level option isn't set" do
105 | let(:options) { Hash.new }
106 |
107 | it 'returns :all' do
108 | expect(base.level).to eq :all
109 | end
110 | end
111 | end
112 |
113 | describe '#consider_sub_navigation?' do
114 | let(:item) { double(:item) }
115 |
116 | before { allow(item).to receive_messages(sub_navigation: sub_navigation) }
117 |
118 | context 'when the item has no sub navigation' do
119 | let(:sub_navigation) { nil }
120 |
121 | it 'returns false' do
122 | expect(base.send(:consider_sub_navigation?, item)).to be false
123 | end
124 | end
125 |
126 | context 'when the item has sub navigation' do
127 | let(:sub_navigation) { double(:sub_navigation) }
128 |
129 | context 'and the renderer has an unknown level' do
130 | before { allow(base).to receive_messages(level: 'unknown') }
131 |
132 | it 'returns false' do
133 | expect(base.send(:consider_sub_navigation?, item)).to be false
134 | end
135 | end
136 |
137 | context 'and the renderer has a level set to :all' do
138 | before { allow(base).to receive_messages(level: :all) }
139 |
140 | it 'returns false' do
141 | expect(base.send(:consider_sub_navigation?, item)).to be true
142 | end
143 | end
144 |
145 | context "when the renderer's level is a number" do
146 | before { allow(base).to receive_messages(level: 2) }
147 |
148 | it 'returns false' do
149 | expect(base.send(:consider_sub_navigation?, item)).to be false
150 | end
151 | end
152 |
153 | context "when the renderer's level is a Range" do
154 | before { allow(base).to receive_messages(level: 2..3) }
155 |
156 | context "and sub navigation's level is greater than range.max" do
157 | before { allow(sub_navigation).to receive_messages(level: 4) }
158 |
159 | it 'returns false' do
160 | expect(base.send(:consider_sub_navigation?, item)).to be false
161 | end
162 | end
163 |
164 | context "and sub navigation's level is equal to range.max" do
165 | before { allow(sub_navigation).to receive_messages(level: 3) }
166 |
167 | it 'returns true' do
168 | expect(base.send(:consider_sub_navigation?, item)).to be true
169 | end
170 | end
171 |
172 | context "and sub navigation's level is equal to range.min" do
173 | before { allow(sub_navigation).to receive_messages(level: 2) }
174 |
175 | it 'returns true' do
176 | expect(base.send(:consider_sub_navigation?, item)).to be true
177 | end
178 | end
179 | end
180 | end
181 | end
182 |
183 | describe '#include_sub_navigation?' do
184 | let(:item) { double(:item) }
185 |
186 | context 'when consider_sub_navigation? is true' do
187 | before { allow(base).to receive_messages(consider_sub_navigation?: true) }
188 |
189 | context 'and expand_sub_navigation? is true' do
190 | before { allow(base).to receive_messages(expand_sub_navigation?: true) }
191 |
192 | it 'returns true' do
193 | expect(base.include_sub_navigation?(item)).to be true
194 | end
195 | end
196 |
197 | context 'and expand_sub_navigation? is false' do
198 | before { allow(base).to receive_messages(expand_sub_navigation?: false) }
199 |
200 | it 'returns false' do
201 | expect(base.include_sub_navigation?(item)).to be false
202 | end
203 | end
204 | end
205 |
206 | context 'consider_sub_navigation? is false' do
207 | before { allow(base).to receive_messages(consider_sub_navigation?: false) }
208 |
209 | context 'and expand_sub_navigation? is true' do
210 | before { allow(base).to receive_messages(expand_sub_navigation?: true) }
211 |
212 | it 'returns false' do
213 | expect(base.include_sub_navigation?(item)).to be false
214 | end
215 | end
216 |
217 | context 'and expand_sub_navigation? is false' do
218 | before { allow(base).to receive_messages(expand_sub_navigation?: false) }
219 |
220 | it 'returns false' do
221 | expect(base.include_sub_navigation?(item)).to be false
222 | end
223 | end
224 | end
225 | end
226 |
227 | describe '#render_sub_navigation_for' do
228 | let(:item) { double(:item, sub_navigation: sub_navigation) }
229 | let(:sub_navigation) { double(:sub_navigation) }
230 |
231 | it 'renders the sub navigation passing it the options' do
232 | expect(sub_navigation).to receive(:render).with(options)
233 | base.render_sub_navigation_for(item)
234 | end
235 | end
236 | end
237 | end
238 | end
239 |
--------------------------------------------------------------------------------
/lib/simple_navigation/helpers.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | # View helpers to render the navigation.
3 | #
4 | # Use render_navigation as following to render your navigation:
5 | # * call render_navigation without :level option to render your
6 | # complete navigation as nested tree.
7 | # * call render_navigation(level: x) to render a specific
8 | # navigation level (e.g. level: 1 to render your primary navigation,
9 | # level: 2 to render the sub navigation and so forth)
10 | # * call render_navigation(:level => 2..3) to render navigation
11 | # levels 2 and 3).
12 | #
13 | # For example, you could use render_navigation(level: 1) to render your
14 | # primary navigation as tabs and render_navigation(level: 2..3) to render
15 | # the rest of the navigation as a tree in a sidebar.
16 | #
17 | # ==== Examples (using Haml)
18 | # #primary_navigation= render_navigation(level: 1)
19 | #
20 | # #sub_navigation= render_navigation(level: 2)
21 | #
22 | # #nested_navigation= render_navigation
23 | #
24 | # #top_navigation= render_navigation(level: 1..2)
25 | # #sidebar_navigation= render_navigation(level: 3)
26 | module Helpers
27 | def self.load_config(options, includer, &block)
28 | context = options.delete(:context)
29 | SimpleNavigation.init_adapter_from includer
30 | SimpleNavigation.load_config context
31 | SimpleNavigation::Configuration.eval_config context
32 |
33 | if block_given? || options[:items]
34 | SimpleNavigation.config.items(options[:items], &block)
35 | end
36 |
37 | unless SimpleNavigation.primary_navigation
38 | fail 'no primary navigation defined, either use a navigation config ' \
39 | 'file or pass items directly to render_navigation'
40 | end
41 | end
42 |
43 | def self.apply_defaults(options)
44 | options[:level] = options.delete(:levels) if options[:levels]
45 | { context: :default, level: :all }.merge(options)
46 | end
47 |
48 | # Renders the navigation according to the specified options-hash.
49 | #
50 | # The following options are supported:
51 | # * :level - defaults to :all which renders the the sub_navigation
52 | # for an active primary_navigation inside that active
53 | # primary_navigation item.
54 | # Specify a specific level to only render that level of navigation
55 | # (e.g. level: 1 for primary_navigation, etc).
56 | # Specifiy a Range of levels to render only those specific levels
57 | # (e.g. level: 1..2 to render both your first and second levels, maybe
58 | # you want to render your third level somewhere else on the page)
59 | # * :expand_all - defaults to false. If set to true the all
60 | # specified levels will be rendered as a fully expanded
61 | # tree (always open). This is useful for javascript menus like Superfish.
62 | # * :context - specifies the context for which you would render
63 | # the navigation. Defaults to :default which loads the default
64 | # navigation.rb (i.e. config/navigation.rb).
65 | # If you specify a context then the plugin tries to load the configuration
66 | # file for that context, e.g. if you call
67 | # render_navigation(context: :admin) the file
68 | # config/admin_navigation.rb will be loaded and used for rendering
69 | # the navigation.
70 | # * :items - you can specify the items directly (e.g. if items are
71 | # dynamically generated from database).
72 | # See SimpleNavigation::ItemsProvider for documentation on what to
73 | # provide as items.
74 | # * :renderer - specify the renderer to be used for rendering the
75 | # navigation. Either provide the Class or a symbol matching a registered
76 | # renderer. Defaults to :list (html list renderer).
77 | #
78 | # Instead of using the :items option, a block can be passed to
79 | # specify the items dynamically
80 | #
81 | # ==== Examples
82 | # render_navigation do |menu|
83 | # menu.item :posts, "Posts", posts_path
84 | # end
85 | #
86 | def render_navigation(options = {}, &block)
87 | container = active_navigation_item_container(options, &block)
88 | container && container.render(options)
89 | end
90 |
91 | # Returns the name of the currently active navigation item belonging to the
92 | # specified level.
93 | #
94 | # See Helpers#active_navigation_item for supported options.
95 | #
96 | # Returns an empty string if no active item can be found for the specified
97 | # options
98 | def active_navigation_item_name(options = {})
99 | active_navigation_item(options, '') do |item|
100 | item.name(apply_generator: false)
101 | end
102 | end
103 |
104 | # Returns the key of the currently active navigation item belonging to the
105 | # specified level.
106 | #
107 | # See Helpers#active_navigation_item for supported options.
108 | #
109 | # Returns nil if no active item can be found for the specified
110 | # options
111 | def active_navigation_item_key(options = {})
112 | active_navigation_item(options, &:key)
113 | end
114 |
115 | # Returns the currently active navigation item belonging to the specified
116 | # level.
117 | #
118 | # The following options are supported:
119 | # * :level - defaults to :all which returns the
120 | # most specific/deepest selected item (the leaf).
121 | # Specify a specific level to only look for the selected item in the
122 | # specified level of navigation
123 | # (e.g. level: 1 for primary_navigation, etc).
124 | # * :context - specifies the context for which you would like to
125 | # find the active navigation item. Defaults to :default which loads the
126 | # default navigation.rb (i.e. config/navigation.rb).
127 | # If you specify a context then the plugin tries to load the configuration
128 | # file for that context, e.g. if you call
129 | # active_navigation_item_name(context: :admin) the file
130 | # config/admin_navigation.rb will be loaded and used for searching the
131 | # active item.
132 | # * :items - you can specify the items directly (e.g. if items are
133 | # dynamically generated from database).
134 | # See SimpleNavigation::ItemsProvider for documentation on what to provide
135 | # as items.
136 | #
137 | # Returns the supplied value_for_nil object (nil
138 | # by default) if no active item can be found for the specified
139 | # options
140 | def active_navigation_item(options = {}, value_for_nil = nil)
141 | if options[:level].nil? || options[:level] == :all
142 | options[:level] = :leaves
143 | end
144 | container = active_navigation_item_container(options)
145 | if container && (item = container.selected_item)
146 | block_given? ? yield(item) : item
147 | else
148 | value_for_nil
149 | end
150 | end
151 |
152 | # Returns the currently active item container belonging to the specified
153 | # level.
154 | #
155 | # The following options are supported:
156 | # * :level - defaults to :all which returns the
157 | # least specific/shallowest selected item.
158 | # Specify a specific level to only look for the selected item in the
159 | # specified level of navigation
160 | # (e.g. level: 1 for primary_navigation, etc).
161 | # * :context - specifies the context for which you would like to
162 | # find the active navigation item. Defaults to :default which loads the
163 | # default navigation.rb (i.e. config/navigation.rb).
164 | # If you specify a context then the plugin tries to load the configuration
165 | # file for that context, e.g. if you call
166 | # active_navigation_item_name(context: :admin) the file
167 | # config/admin_navigation.rb will be loaded and used for searching the
168 | # active item.
169 | # * :items - you can specify the items directly (e.g. if items are
170 | # dynamically generated from database).
171 | # See SimpleNavigation::ItemsProvider for documentation on what to provide
172 | # as items.
173 | #
174 | # Returns nil if no active item container can be found
175 | def active_navigation_item_container(options = {}, &block)
176 | options = SimpleNavigation::Helpers.apply_defaults(options)
177 | SimpleNavigation::Helpers.load_config(options, self, &block)
178 | SimpleNavigation.active_item_container_for(options[:level])
179 | end
180 | end
181 | end
182 |
--------------------------------------------------------------------------------
/spec/simple_navigation/adapters/rails_spec.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | module Adapters
3 | describe Rails do
4 | let(:action_controller) { ActionController::Base }
5 | let(:adapter) { Rails.new(context) }
6 | let(:context) { double(:context, controller: controller) }
7 | let(:controller) { double(:controller) }
8 | let(:request) { double(:request) }
9 | let(:simple_navigation) { SimpleNavigation }
10 | let(:template) { double(:template, request: request) }
11 |
12 | describe '.register' do
13 | before { allow(action_controller).to receive(:include) }
14 |
15 | it 'calls set_env' do
16 | app_path = RailsApp::Application.root
17 | expect(simple_navigation).to receive(:set_env).with(app_path, 'test')
18 | simple_navigation.register
19 | end
20 |
21 | it 'extends the ActionController::Base with the Helpers' do
22 | expect(action_controller).to receive(:include)
23 | .with(SimpleNavigation::Helpers)
24 | simple_navigation.register
25 | end
26 |
27 | shared_examples 'installing helper method' do |method|
28 | it "installs the #{method} method as helper method" do
29 | simple_navigation.register
30 |
31 | helper_methods = action_controller.send(:_helper_methods)
32 | expect(helper_methods).to include(method)
33 | end
34 | end
35 |
36 | it_behaves_like 'installing helper method', :render_navigation
37 | it_behaves_like 'installing helper method', :active_navigation_item_name
38 | it_behaves_like 'installing helper method', :active_navigation_item_key
39 | it_behaves_like 'installing helper method', :active_navigation_item
40 | it_behaves_like 'installing helper method', :active_navigation_item_container
41 | end
42 |
43 | describe '#initialize' do
44 | context "when the controller's template is set" do
45 | before { allow(controller).to receive_messages(instance_variable_get: template) }
46 |
47 | it "sets the adapter's request accordingly" do
48 | expect(adapter.request).to be request
49 | end
50 | end
51 |
52 | context "when the controller's template is not set" do
53 | before { allow(controller).to receive_messages(instance_variable_get: nil) }
54 |
55 | it "sets the adapter's request to nil" do
56 | expect(adapter.request).to be_nil
57 | end
58 | end
59 |
60 | it "sets the adapter's controller to the context's controller" do
61 | expect(adapter.controller).to be controller
62 | end
63 |
64 | context "when the controller's template is stored as instance var (Rails2)" do
65 | context "when the controller's template is set" do
66 | before { allow(controller).to receive_messages(instance_variable_get: template) }
67 |
68 | it "sets the adapter's template accordingly" do
69 | expect(adapter.template).to be template
70 | end
71 | end
72 |
73 | context "when the controller's template is not set" do
74 | before { allow(controller).to receive_messages(instance_variable_get: nil) }
75 |
76 | it "set the adapter's template to nil" do
77 | expect(adapter.template).to be_nil
78 | end
79 | end
80 | end
81 |
82 | context "when the controller's template is stored as view_context (Rails3)" do
83 | context 'and the template is set' do
84 | before { allow(controller).to receive_messages(view_context: template) }
85 |
86 | it "sets the adapter's template accordingly" do
87 | expect(adapter.template).to be template
88 | end
89 | end
90 |
91 | context 'and the template is not set' do
92 | before { allow(controller).to receive_messages(view_context: nil) }
93 |
94 | it "sets the adapter's template to nil" do
95 | expect(adapter.template).to be_nil
96 | end
97 | end
98 | end
99 | end
100 |
101 | describe '#request_uri' do
102 | context "when the adapter's request is set" do
103 | before { allow(adapter).to receive_messages(request: request) }
104 |
105 | context 'and request.fullpath is defined' do
106 | let(:request) { double(:request, fullpath: '/fullpath') }
107 |
108 | it "sets the adapter's request_uri to the request.fullpath" do
109 | expect(adapter.request_uri).to eq '/fullpath'
110 | end
111 | end
112 |
113 | context 'and request.fullpath is not defined' do
114 | let(:request) { double(:request, request_uri: '/request_uri') }
115 |
116 | before { allow(adapter).to receive_messages(request: request) }
117 |
118 | it "sets the adapter's request_uri to the request.request_uri" do
119 | expect(adapter.request_uri).to eq '/request_uri'
120 | end
121 | end
122 | end
123 |
124 | context "when the adapter's request is not set" do
125 | before { allow(adapter).to receive_messages(request: nil) }
126 |
127 | it "sets the adapter's request_uri to an empty string" do
128 | expect(adapter.request_uri).to eq ''
129 | end
130 | end
131 | end
132 |
133 | describe '#request_path' do
134 | context "when the adapter's request is set" do
135 | let(:request) { double(:request, path: '/request_path') }
136 |
137 | before { allow(adapter).to receive_messages(request: request) }
138 |
139 | it "sets the adapter's request_path to the request.path" do
140 | expect(adapter.request_path).to eq '/request_path'
141 | end
142 | end
143 |
144 | context "when the adapter's request is not set" do
145 | before { allow(adapter).to receive_messages(request: nil) }
146 |
147 | it "sets the adapter's request_path to an empty string" do
148 | expect(adapter.request_path).to eq ''
149 | end
150 | end
151 | end
152 |
153 | describe '#context_for_eval' do
154 | context "when the adapter's controller is set" do
155 | before { adapter.instance_variable_set(:@controller, controller) }
156 |
157 | context "and the adapter's template is set" do
158 | before { adapter.instance_variable_set(:@template, template) }
159 |
160 | it "sets the adapter's context_for_eval to the template" do
161 | expect(adapter.context_for_eval).to be template
162 | end
163 | end
164 |
165 | context "and the adapter's template is not set" do
166 | before { adapter.instance_variable_set(:@template, nil) }
167 |
168 | it "sets the adapter's context_for_eval to the controller" do
169 | expect(adapter.context_for_eval).to be controller
170 | end
171 | end
172 | end
173 |
174 | context "when the adapter's controller is not set" do
175 | before { adapter.instance_variable_set(:@controller, nil) }
176 |
177 | context "and the adapter's template is set" do
178 | before { adapter.instance_variable_set(:@template, template) }
179 |
180 | it "sets the adapter's context_for_eval to the template" do
181 | expect(adapter.context_for_eval).to be template
182 | end
183 | end
184 |
185 | context "and the adapter's template is not set" do
186 | before { adapter.instance_variable_set(:@template, nil) }
187 |
188 | it 'raises an exception' do
189 | expect{ adapter.context_for_eval }.to raise_error(RuntimeError, 'no context set for evaluation the config file')
190 | end
191 | end
192 | end
193 | end
194 |
195 | describe '#current_page?' do
196 | context "when the adapter's template is set" do
197 | before { allow(adapter).to receive_messages(template: template) }
198 |
199 | it 'delegates the call to the template' do
200 | expect(template).to receive(:current_page?).with(:page)
201 | adapter.current_page?(:page)
202 | end
203 | end
204 |
205 | context "when the adapter's template is not set" do
206 | before { allow(adapter).to receive_messages(template: nil) }
207 |
208 | it 'returns false' do
209 | expect(adapter.current_page?(:page)).to be_falsey
210 | end
211 | end
212 |
213 | context 'when the given url is nil' do
214 | it 'returns false' do
215 | expect(adapter.current_page?(nil)).to be_falsey
216 | end
217 | end
218 | end
219 |
220 | describe '#link_to' do
221 | let(:options) { double(:options) }
222 |
223 | context "when the adapter's template is set" do
224 | before { allow(adapter).to receive_messages(template: template, html_safe: 'safe_text') }
225 |
226 | context 'with considering item names as safe' do
227 | before { SimpleNavigation.config.consider_item_names_as_safe = true }
228 | after { SimpleNavigation.config.consider_item_names_as_safe = false }
229 |
230 | it 'delegates the call to the template (with html_safe text)' do
231 | expect(template).to receive(:link_to)
232 | .with('safe_text', 'url', options)
233 | adapter.link_to('text', 'url', options)
234 | end
235 | end
236 |
237 | context 'with considering item names as UNsafe (default)' do
238 |
239 | it 'delegates the call to the template (with html_safe text)' do
240 | expect(template).to receive(:link_to)
241 | .with('text', 'url', options)
242 | adapter.link_to('text', 'url', options)
243 | end
244 | end
245 |
246 | end
247 |
248 | context "when the adapter's template is not set" do
249 | before { allow(adapter).to receive_messages(template: nil) }
250 |
251 | it 'returns nil' do
252 | expect(adapter.link_to('text', 'url', options)).to be_nil
253 | end
254 | end
255 | end
256 |
257 | describe '#content_tag' do
258 | let(:options) { double(:options) }
259 |
260 | context "when the adapter's template is set" do
261 | before { allow(adapter).to receive_messages(template: template, html_safe: 'safe_text') }
262 |
263 | it 'delegates the call to the template (with html_safe text)' do
264 | expect(template).to receive(:content_tag)
265 | .with(:div, 'safe_text', options)
266 | adapter.content_tag(:div, 'text', options)
267 | end
268 | end
269 |
270 | context "when the adapter's template is not set" do
271 | before { allow(adapter).to receive_messages(template: nil) }
272 |
273 | it 'returns nil' do
274 | expect(adapter.content_tag(:div, 'text', options)).to be_nil
275 | end
276 | end
277 | end
278 |
279 | end
280 | end
281 | end
282 |
--------------------------------------------------------------------------------
/spec/simple_navigation/helpers_spec.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | describe Helpers do
3 | subject(:controller) { test_controller_class.new }
4 |
5 | let(:invoices_item) { navigation[:invoices] }
6 | let(:item) { nil }
7 | let(:navigation) { setup_navigation('nav_id', 'nav_class') }
8 | let(:test_controller_class) do
9 | Class.new { include SimpleNavigation::Helpers }
10 | end
11 | let(:unpaid_item) { invoices_item.sub_navigation[:unpaid] }
12 |
13 | before do
14 | allow(Configuration).to receive(:eval_config)
15 | allow(SimpleNavigation).to receive_messages(load_config: nil,
16 | primary_navigation: navigation,
17 | config_file?: true,
18 | context_for_eval: controller)
19 |
20 | select_an_item(navigation[item]) if item
21 | end
22 |
23 | describe '#active_navigation_item_name' do
24 | context 'when no item is selected' do
25 | it 'returns an empty string for no parameters' do
26 | expect(controller.active_navigation_item_name).to eq ''
27 | end
28 |
29 | it "returns an empty string for level: 1" do
30 | item_name = controller.active_navigation_item_name(level: 1)
31 | expect(item_name).to eq ''
32 | end
33 |
34 | it 'returns an empty string for level: 2' do
35 | item_name = controller.active_navigation_item_name(level: 2)
36 | expect(item_name).to eq ''
37 | end
38 |
39 | it 'returns an empty string for level: :all' do
40 | item_name = controller.active_navigation_item_name(level: :all)
41 | expect(item_name).to eq ''
42 | end
43 | end
44 |
45 | context 'when an item is selected' do
46 | context "and it's a primary item" do
47 | let(:item) { :invoices }
48 |
49 | it 'returns an empty string' do
50 | expect(controller.active_navigation_item_name).to eq ''
51 | end
52 |
53 | it "returns the selected item's name for level: 1" do
54 | item_name = controller.active_navigation_item_name(level: 1)
55 | expect(item_name).to eq 'Invoices'
56 | end
57 |
58 | it 'returns an empty string for level: 2' do
59 | item_name = controller.active_navigation_item_name(level: 2)
60 | expect(item_name).to eq ''
61 | end
62 |
63 | it 'returns an empty string for level: :all' do
64 | item_name = controller.active_navigation_item_name(level: :all)
65 | expect(item_name).to eq ''
66 | end
67 | end
68 |
69 | context "and it's a sub navigation item" do
70 | before do
71 | select_an_item(invoices_item)
72 | select_an_item(unpaid_item)
73 | end
74 |
75 | it "returns the selected item's name" do
76 | expect(controller.active_navigation_item_name).to eq 'Unpaid'
77 | end
78 |
79 | it "returns the selected item's parent name for level: 1" do
80 | item_name = controller.active_navigation_item_name(level: 1)
81 | expect(item_name).to eq 'Invoices'
82 | end
83 |
84 | it "returns the selected item's name for level: 2" do
85 | item_name = controller.active_navigation_item_name(level: 2)
86 | expect(item_name).to eq 'Unpaid'
87 | end
88 |
89 | it "returns the selected item's name for level: :all" do
90 | item_name = controller.active_navigation_item_name(level: :all)
91 | expect(item_name).to eq 'Unpaid'
92 | end
93 | end
94 | end
95 | end
96 |
97 | describe '#active_navigation_item_key' do
98 | context 'when no item is selected' do
99 | it 'returns nil' do
100 | expect(controller.active_navigation_item_key).to be_nil
101 | end
102 |
103 | it 'returns nil for no parameters' do
104 | expect(controller.active_navigation_item_key).to be_nil
105 | end
106 |
107 | it "returns nil for level: 1" do
108 | item_key = controller.active_navigation_item_key(level: 1)
109 | expect(item_key).to be_nil
110 | end
111 |
112 | it 'returns nil for level: 2' do
113 | item_key = controller.active_navigation_item_key(level: 2)
114 | expect(item_key).to be_nil
115 | end
116 |
117 | it 'returns nil for level: :all' do
118 | item_key = controller.active_navigation_item_key(level: :all)
119 | expect(item_key).to be_nil
120 | end
121 | end
122 |
123 | context 'when an item is selected' do
124 | context "and it's a primary item" do
125 | let(:item) { :invoices }
126 |
127 | it 'returns nil for no parameters' do
128 | expect(controller.active_navigation_item_key).to be_nil
129 | end
130 |
131 | it "returns the selected item's name for level: 1" do
132 | item_key = controller.active_navigation_item_key(level: 1)
133 | expect(item_key).to eq :invoices
134 | end
135 |
136 | it 'returns nil for level: 2' do
137 | item_key = controller.active_navigation_item_key(level: 2)
138 | expect(item_key).to be_nil
139 | end
140 |
141 | it 'returns nil for level: :all' do
142 | item_key = controller.active_navigation_item_key(level: :all)
143 | expect(item_key).to be_nil
144 | end
145 | end
146 |
147 | context "and it's a sub navigation item" do
148 | before do
149 | select_an_item(invoices_item)
150 | select_an_item(unpaid_item)
151 | end
152 |
153 | it "returns the selected item's name" do
154 | expect(controller.active_navigation_item_key).to eq :unpaid
155 | end
156 |
157 | it "returns the selected item's parent name for level: 1" do
158 | item_key = controller.active_navigation_item_key(level: 1)
159 | expect(item_key).to eq :invoices
160 | end
161 |
162 | it "returns the selected item's name for level: 2" do
163 | item_key = controller.active_navigation_item_key(level: 2)
164 | expect(item_key).to eq :unpaid
165 | end
166 |
167 | it "returns the selected item's name for level: :all" do
168 | item_key = controller.active_navigation_item_key(level: :all)
169 | expect(item_key).to eq :unpaid
170 | end
171 | end
172 | end
173 | end
174 |
175 | describe '#active_navigation_item' do
176 | context 'when no item is selected' do
177 | it 'returns nil for no parameters' do
178 | expect(controller.active_navigation_item).to be_nil
179 | end
180 |
181 | it "returns nil for level: 1" do
182 | item_key = controller.active_navigation_item(level: 1)
183 | expect(item_key).to be_nil
184 | end
185 |
186 | it 'returns nil for level: 2' do
187 | item_key = controller.active_navigation_item(level: 2)
188 | expect(item_key).to be_nil
189 | end
190 |
191 | it 'returns nil for level: :all' do
192 | item_key = controller.active_navigation_item(level: :all)
193 | expect(item_key).to be_nil
194 | end
195 | end
196 |
197 | context 'when an item is selected' do
198 | context "and it's a primary item" do
199 | let(:item) { :invoices }
200 |
201 | it 'returns nil for no parameters' do
202 | expect(controller.active_navigation_item).to be_nil
203 | end
204 |
205 | it "returns the selected item's name for level: 1" do
206 | item_key = controller.active_navigation_item(level: 1)
207 | expect(item_key).to be invoices_item
208 | end
209 |
210 | it 'returns nil for level: 2' do
211 | item_key = controller.active_navigation_item(level: 2)
212 | expect(item_key).to be_nil
213 | end
214 |
215 | it 'returns nil for level: :all' do
216 | item_key = controller.active_navigation_item(level: :all)
217 | expect(item_key).to be_nil
218 | end
219 | end
220 |
221 | context "and it's a sub navigation item" do
222 | before do
223 | select_an_item(invoices_item)
224 | select_an_item(unpaid_item)
225 | end
226 |
227 | it "returns the selected item's name for no parameters" do
228 | expect(controller.active_navigation_item).to be unpaid_item
229 | end
230 |
231 | it "returns the selected item's parent name for level: 1" do
232 | item_key = controller.active_navigation_item(level: 1)
233 | expect(item_key).to be invoices_item
234 | end
235 |
236 | it "returns the selected item's name for level: 2" do
237 | item_key = controller.active_navigation_item(level: 2)
238 | expect(item_key).to eq unpaid_item
239 | end
240 |
241 | it "returns the selected item's name for level: :all" do
242 | item_key = controller.active_navigation_item(level: :all)
243 | expect(item_key).to eq unpaid_item
244 | end
245 | end
246 | end
247 | end
248 |
249 | describe '#active_navigation_item_container' do
250 | shared_examples 'returning items container' do
251 | it 'returns the primary navigation for no parameters' do
252 | expect(controller.active_navigation_item_container).to be navigation
253 | end
254 |
255 | it "returns the primary navigation for level: 1" do
256 | item_container = controller.active_navigation_item_container(level: 1)
257 | expect(item_container).to be navigation
258 | end
259 |
260 | it 'returns the primary navigation level: :all' do
261 | item_container =
262 | controller.active_navigation_item_container(level: :all)
263 | expect(item_container).to be navigation
264 | end
265 | end
266 |
267 | context 'when no item is selected' do
268 | it_behaves_like 'returning items container'
269 |
270 | it 'returns nil for level: 2' do
271 | item_container = controller.active_navigation_item_container(level: 2)
272 | expect(item_container).to be_nil
273 | end
274 | end
275 |
276 | context 'when an item is selected' do
277 | context "and it's a primary item" do
278 | let(:item) { :invoices }
279 |
280 | it_behaves_like 'returning items container'
281 |
282 | it 'returns the invoices items container for level: 2' do
283 | item_container =
284 | controller.active_navigation_item_container(level: 2)
285 | expect(item_container).to be invoices_item.sub_navigation
286 | end
287 | end
288 |
289 | context "and it's a sub navigation item" do
290 | before do
291 | select_an_item(invoices_item)
292 | select_an_item(unpaid_item)
293 | end
294 |
295 | it_behaves_like 'returning items container'
296 |
297 | it 'returns the invoices items container for level: 2' do
298 | item_container =
299 | controller.active_navigation_item_container(level: 2)
300 | expect(item_container).to be invoices_item.sub_navigation
301 | end
302 | end
303 | end
304 | end
305 |
306 | describe '#render_navigation' do
307 | it 'evaluates the configuration on every request' do
308 | expect(SimpleNavigation).to receive(:load_config).twice
309 | 2.times { controller.render_navigation }
310 | end
311 |
312 | it 'loads the :default configuration' do
313 | expect(SimpleNavigation).to receive(:load_config).with(:default)
314 | controller.render_navigation
315 | end
316 |
317 | it "doesn't set the items directly" do
318 | expect(SimpleNavigation.config).not_to receive(:items)
319 | controller.render_navigation
320 | end
321 |
322 | it 'looks up the active_item_container based on the level' do
323 | expect(SimpleNavigation).to receive(:active_item_container_for)
324 | .with(:all)
325 | controller.render_navigation
326 | end
327 |
328 | context 'when the :context option is specified' do
329 | it 'loads the configuration for the specified context' do
330 | expect(SimpleNavigation).to receive(:load_config).with(:my_context)
331 | controller.render_navigation(context: :my_context)
332 | end
333 | end
334 |
335 | context 'when the :items option is specified' do
336 | let(:items) { double(:items) }
337 |
338 | it 'sets the items directly' do
339 | expect(SimpleNavigation.config).to receive(:items).with(items)
340 | controller.render_navigation(items: items)
341 | end
342 | end
343 |
344 | context 'when the :level option is set' do
345 | context 'and its value is 1' do
346 | it 'calls render on the primary navigation' do
347 | expect(navigation).to receive(:render).with(level: 1)
348 | controller.render_navigation(level: 1)
349 | end
350 | end
351 |
352 | context 'and its value is 2' do
353 | context 'and the active_item_container is set' do
354 | let(:item_container) { double(:container).as_null_object }
355 |
356 | before do
357 | allow(SimpleNavigation).to receive_messages(active_item_container_for: item_container)
358 | end
359 |
360 | it 'finds the selected sub navigation for the specified level' do
361 | expect(SimpleNavigation).to receive(:active_item_container_for)
362 | .with(2)
363 | controller.render_navigation(level: 2)
364 | end
365 |
366 | it 'calls render on the active item_container' do
367 | expect(item_container).to receive(:render).with(level: 2)
368 | controller.render_navigation(level: 2)
369 | end
370 | end
371 |
372 | context "and the active_item_container isn't set" do
373 | it "doesn't raise an exception" do
374 | expect{
375 | controller.render_navigation(level: 2)
376 | }.not_to raise_error
377 | end
378 | end
379 | end
380 |
381 | context "and its value isn't a valid level" do
382 | it 'raises an exception' do
383 | expect{
384 | controller.render_navigation(level: :invalid)
385 | }.to raise_error(ArgumentError, 'Invalid navigation level: invalid')
386 | end
387 | end
388 | end
389 |
390 | context 'when the :levels option is set' do
391 | before { allow(SimpleNavigation).to receive_messages(active_item_container_for: navigation) }
392 |
393 | it 'treats it like the :level option' do
394 | expect(navigation).to receive(:render).with(level: 2)
395 | controller.render_navigation(levels: 2)
396 | end
397 | end
398 |
399 | context 'when a block is given' do
400 | it 'calls the block passing it an item container' do
401 | expect{ |blk|
402 | controller.render_navigation(&blk)
403 | }.to yield_with_args(ItemContainer)
404 | end
405 | end
406 |
407 | context 'when no primary configuration is defined' do
408 | before { allow(SimpleNavigation).to receive_messages(primary_navigation: nil) }
409 |
410 | it 'raises an exception' do
411 | expect{controller.render_navigation}.to raise_error(RuntimeError, 'no primary navigation defined, either use a navigation config file or pass items directly to render_navigation')
412 | end
413 | end
414 |
415 | context "when active_item_container is set" do
416 | let(:active_item_container) { double(:container).as_null_object }
417 |
418 | before do
419 | allow(SimpleNavigation).to receive_messages(active_item_container_for: active_item_container)
420 | end
421 |
422 | it 'calls render on the active_item_container' do
423 | expect(active_item_container).to receive(:render)
424 | controller.render_navigation
425 | end
426 | end
427 | end
428 | end
429 | end
430 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 4.4.0
4 |
5 | * add options rendering to json renderer. Credits to Mikhail Kytyzov.
6 |
7 | ## 4.3.0
8 |
9 | * removed warnings from rspec. Thanks mgrunberg.
10 | * add compatibility with rails 6.1. Credits to mgrunberg.
11 | * run specs against 6.1. Credits to mgrunberg.
12 |
13 | ## 4.2.0
14 |
15 | * improvements to generator template. Credits to mgrunberg.
16 | * be able to run 'rake spec:rails-6-0-stable'. Credits to mgrunberg.
17 |
18 | ## 4.1.0
19 |
20 | * Delay rails6 initialization using on_load (getting rid of deprecation warnings in rails 6). Credits to Markus Benning.
21 | * Fix link to wiki in README. Thanks to Greg Molnar.
22 | * Fix uninitialized variable `@dom_attributes` warning. Credits to Johan Tell.
23 | * Fixed tests for rails 5x. Credits to Eugene Gavrilov.
24 |
25 |
26 | ## 4.0.5
27 |
28 | * Fix #188 Blank url and highligh_on_subpath = true causes error. Credits to Tristan Harmer (gondalez) and Ilia Pozhilov (ilyapoz).
29 |
30 | ## 4.0.4
31 |
32 | * Fix #184 uninitialized constant Rails::Railtie (NameError). Credits to n-rodriguez.
33 |
34 | ## 4.0.3
35 |
36 | * Fix #180 Check URL before invoking current_page?
37 |
38 | ## 4.0.2
39 |
40 | * fixing current_page? when url is nil
41 |
42 | ## 4.0.1
43 |
44 | * fixed padrino adapter
45 |
46 | ## 4.0.0
47 |
48 | * added two new configuration options ignore_query_params_on_auto_highlight and ignore_anchors_on_auto_highlight
49 | * Remove dependency on classic-style Sinatra applications and enable use with modular-style apps. Credits to Stefan Kolb.
50 | * Item can now receive a block as `name`
51 | * It's now possible to set a global `highlight_on_subpath` option instead of adding it to every item
52 | * Creating an Item doesn't remove options anymore
53 | * Creating an Item no longer changed its container, only adding it to a container
54 | does
55 | * `Item#autogenerate_item_ids?` has been removed
56 | * `SN.config_file_name`, `SN.config_file` and `SN.config_file?` have been
57 | removed
58 | * `ConfigFileFinder` and `ConfigFile` handle the configuration logic
59 | * File organization was been changed to reflect the Ruby namespacing
60 |
61 | ## 3.13.0
62 |
63 | * consider_item_names_as_safe is now false by default. Removed deprecation warning
64 |
65 | ## 3.12.2
66 |
67 | * Fixing issue #154. Thanks to Simon Curtois.
68 |
69 | ## 3.12.1
70 |
71 | * bugfix (error in generator)
72 |
73 | ## 3.12.0
74 |
75 | * Relax hash constraint on item_adapter. Thanks to Ramon Tayag.
76 | * Fixed hidden special character in navigation template. Credits to Stef
77 | Lewandowski
78 | * Added full MIT license text. Thanks to Ben Armstrong.
79 | * Added license to gemspec. Thanks to Troy Thompson.
80 | * Allow defining other html attributes than :id and :class on menu container.
81 | Credits to Jacek Tomaszewski.
82 | * Added new config option "consider_item_names_as_safe". Thanks to Alexey
83 | Naumov.
84 | * Big cleanup of specs, removed jeweler in favor of the "bundler" way. Huge
85 | thank you to Simon Courtois.
86 | * Added more powerful name generator which yields the item itself in addition to
87 | the item's name. Credits to Simon Curtois.
88 |
89 | ## 3.11.0
90 |
91 | * Added Json renderer. Thanks to Alberto Avila.
92 |
93 | ## 3.10.1
94 |
95 | * Padrino adapter now returns "html_safe"d content_tag
96 |
97 | ## 3.10.0
98 |
99 | * Added ability to set selected_class on container level. Credits to Joost
100 | Hietbrink.
101 | * do not highlight items that are only partial matches. Thanks to Troy Thompson.
102 | * adding support for rails 4. Credits to Samer Masry.
103 |
104 | ## 3.9.0
105 |
106 | * Added ability to pass a block to render_navigation for configuring dynamic
107 | navigation items (instead of passing :items). Credits to Ronald Chan.
108 |
109 | ## 3.8.0
110 |
111 | * Changed the way the context is fetched. Fixes incompatibility with Gretel.
112 | Thanks to Ivan Kasatenko.
113 | * Added :join_with option to links renderer. Thanks to Stefan Melmuk.
114 | * Added :prefix option to breadcrumb renderer. Credits to Rodrigo Manhães.
115 | * Added :ordered option for allowing list renderer to render an `` rather
116 | than a ``.
117 | * Sinatra adapter no longer renders attributes with nil values as attributes
118 | with empty strings in the output, instead electing not to render the attribute
119 | at all. Thanks to revdan for pointing this out.
120 |
121 | ## 3.7.0
122 |
123 | * Added new adapater for working with the Nanoc static site generation
124 | framework.
125 | * Fix issue #22 - last link in a breadcrumb trail may now be rendered as static
126 | text insted by supplying :static_leaf => true as an option
127 | * Allow breadcrumbs to be provided with link id and classes by supplying
128 | `:allow_classes_and_ids => true` as an option
129 |
130 | ## 3.6.0
131 |
132 | * Added linkless items functionality - the `url` parameter is now optional,
133 | items which aren't links will be rendered within a 'span' element rather than
134 | an 'a' element. `options` remain optional (defaults to an empty Hash).
135 | `options` may be provided without providing a `url` (detected by checking if
136 | the `url` parameter is a Hash or otherwise).
137 |
138 | ## 3.5.1
139 |
140 | * Fixed specs related to testing name_generator functionality - stub the
141 | name_generator method rather than calling it to ensure subsequent tests aren't
142 | affected.
143 |
144 | ## 3.5.0
145 |
146 | * Added (configurable) "simple-navigation-active-leaf" class to last selected
147 | element and elements that have :highlights_on return true. Thanks to Frank
148 | Schumacher (thenoseman).
149 |
150 | ## 3.4.2
151 |
152 | * Improve Gemfile dependencies with :development and :rails groups.
153 |
154 | ## 3.4.1
155 |
156 | * Rerelease using ruby-1.8.7 rather than ruby-1.9.2 in order that the
157 | rubygems.org gemspec is generated in a compatible fashion.
158 |
159 | ## 3.4.0
160 |
161 | * Added Gemfile for easier development with Bundler. Thanks to Josep Jaume.
162 | * modified :highlights_on option to accept a :subpath option (as well as Proc
163 | and Regexp forms). This can be used to automatically highlight an item even
164 | for items within a subpath. Thanks to Josep Jaume.
165 |
166 | ## 3.3.4
167 |
168 | * modified :highlights_on option to accept a Proc (as well as the existing
169 | Regexp form). This can be used to provide specific highlighting conditions
170 | inline. Thanks to superlou for sparking the idea for the concept.
171 |
172 | ## 3.3.3
173 |
174 | * Bugfix in Adapters::Sinatra#current_url? (compares unencoded URIs). Thanks to
175 | Matthew Gast.
176 |
177 | ## 3.3.2
178 |
179 | * The patch listed in 3.3.1 somehow did not make it into the gem... sorry.
180 | Re-releasing...
181 |
182 | ## 3.3.1
183 |
184 | * bugfix in sinatra adapter. Use Rack::Request#scheme instead of
185 | `Rack::Request#protocol`. Credits to Matthew Gast.
186 |
187 | ## 3.3.0
188 |
189 | * add a new method `active_navigation_item_key` which returns the symbol for the
190 | currently selected navigation item in a similar way to
191 | `active_navigation_item_name` does for the name (useful for CSS class styling
192 | for eg.)
193 | * open up the helper API to provide `active_navigation_item` and
194 | `active_navigation_item_container` methods to make it easy to access the
195 | items/containers should it be necessary (came for free with the above
196 | refactoring)
197 | * isolate the apply_defaults and load_config private methods from
198 | `ActionController` mixin leakage by refactoring to module class instance
199 | methods
200 | * addition of test coverage for the added helpers within helpers_spec.rb
201 | * inclusion of new helpers within the rails adapter and minor refactoring to DRY
202 | up the helper_method invocations
203 | * addition of test coverage for the newly included helpers
204 | * Credits to Mark J. Titorenko for all the changes in this release! Thanks.
205 |
206 | ## 3.2.0
207 |
208 | * Added Renderer::Text for rendering selected navigation items without markup
209 | (useful for dynamic page titles). Credits to Tim Cowlishaw.
210 | * Added ability to add custom markup around item names specifying a
211 | `name_generator` in the config file. Thanks to Jack Dempsey.
212 |
213 | ## 3.1.1
214 |
215 | * `Item#selected_by_url?` now strips anchors from the item's url before
216 | comparing it with the current request's url. Credits to opengovernment.
217 |
218 | ## 3.1.0
219 |
220 | * added new helper method `active_navigation_item_name` to render the name of
221 | the currently active item
222 |
223 | ## 3.0.2
224 |
225 | * `dom_id` and `dom_class` can now be set as option in the item's definition
226 | (useful for dynamic menu items).
227 |
228 | ## 3.0.1
229 |
230 | * allow controller instance variables named @template for rails3 apps. Credits
231 | to cmartyn.
232 | * added possibility to specify and item's URL as a block which is evaulated
233 | after the :if and :unless conditions have been checked. Credits to Nicholas
234 | Firth-McCoy.
235 | * upgraded to rspec 2.0.0
236 | * fixed cgi error in sinatra adapter. Credits to Jack Dempsey.
237 |
238 | ## 3.0.0
239 |
240 | * added ability to specify dynamic items using an array of hashes. Credits to
241 | Anshul Khandelwal for input and discussion.
242 | * added ability to specify attributes for the generated link-tag in list
243 | renderer (up to now the attributes have been applied to the li-tag). Credits
244 | to Anthony Navarre for input and discussion.
245 |
246 | ## 3.0.0.beta2
247 |
248 | * moving code for initializing plugin in sinatra to separate gem
249 |
250 | ## 3.0.0.beta1
251 |
252 | * moving deprecated rails controller methods (navigation, current_navigation) to
253 | separate file 'rails_controller_methods'. Deprecations removed. File can be
254 | required explicitly if controller methods should be still available.
255 | * decoupling from Rails. Introducing the concept of adapters to work with
256 | several frameworks.
257 | * tested with Rails 3.0.0
258 | * adding support for Sinatra and Padrino frameworks.
259 | * cherry picking active_support stuff instead of requiring the whole bunch
260 | (tested with active_support >= 2.3.2 and 3.0.0)
261 | * created public sample project which includes demo for Rails2, Rails3, Sinatra
262 | and Padrino (will be available on github soon)
263 | * better src file organization (adapters/core/rendering folders)
264 |
265 | ## 2.7.3
266 |
267 | * initializing SimpleNavigation.config_file_path with empty array (was `nil`
268 | before). Allows for adding paths before gem has been initialized.
269 |
270 | ## 2.7.2
271 |
272 | * added ability to have more than one config_file_path (useful if
273 | simple-navigation is used as a part of another gem/plugin). Credits to Luke
274 | Imhoff.
275 |
276 | ## 2.7.1
277 |
278 | * added SimpleNavigation.request and SimpleNavigation.request_uri as abstraction
279 | for getting request_uri (rails2/rails3)
280 | * use request.fullpath instead of request.request_uri for Rails3. Credits to Ben
281 | Langfeld.
282 |
283 | ## 2.7.0
284 |
285 | * added new option :highlights_on to item definition in config-file. Specify a
286 | regexp which is matched against the current_uri to determine if an item is
287 | active or not. Replaces explicit highlighting in controllers.
288 | * deprecated explicit highlighting in the controllers.
289 |
290 | ## 2.6.0
291 |
292 | * added rendering option 'skip_if_empty' to Renderer::List to avoid rendering of
293 | empty ul-tags
294 | * added breadcrumbs renderer incl. specs. A big thanks to Markus Schirp.
295 | * added ability to register a renderer / specify your renderer as symbol in
296 | render_navigation
297 | * renderer can be specified in render_navigation. Credits to Andi Bade from
298 | Galaxy Cats.
299 |
300 | ## 2.5.4
301 |
302 | * bugfix: SimpleNavigation.config_file? without params does not check for
303 | `_navigation.rb` file anymore. Credits to Markus Schirp.
304 |
305 | ## 2.5.3
306 |
307 | * removed deprecated railtie_name from simple_navigation/railtie. Credits to
308 | Markus Schirp.
309 |
310 | ## 2.5.2
311 |
312 | * added Rails3 generator for navigation_config.rb. Thanks to Josep Jaume Rey.
313 |
314 | ## 2.5.1
315 |
316 | * set template correctly for Rails3 (brings auto highlighting to life again).
317 | Credits to Josep Jaume Rey.
318 |
319 | ## 2.5.0
320 |
321 | * added new renderer Renderer::Links to simply render the navigation as links
322 | inside a div.
323 | * also make item.name html_safe (in order you have html_code in the item's
324 | name). Thanks again, Johan Svensson.
325 |
326 | ## 2.4.2
327 |
328 | * Rails 3.0.0.beta2 compatibility
329 | * Renderer::List --> make content of ul-tag html_safe for rails 3.0.0.beta2 (due
330 | to rails3 XSS protection). Credits to Johan Svensson and Disha Albaqui.
331 | * updated copyright
332 | * reduced visibility of 'sn_set_navigation' to protected
333 |
334 | ## 2.4.1
335 |
336 | * removing depencency to rails. It installs the newest rails, even if a matching
337 | version is present. why is that?
338 |
339 | ## 2.4.0
340 |
341 | * added Rails3 compatibility
342 | * added Jeweler::Gemcutter Tasks to Rakefile
343 |
344 | ## 2.2.3
345 |
346 | * changed error handling in config-file. Do not ignore errors in config-file
347 | anymore.
348 | * only load config-file if it is present. Needed when directly providing items
349 | in render_navigation.
350 |
351 | ## 2.2.2
352 |
353 | * added lib/simple-navigation.rb to allow 'require simple-navigation' or
354 | 'config.gem "simple-navigation"'
355 |
356 | ## 2.2.1
357 |
358 | * Corrected URL to API-Doc on rubyforge in README
359 |
360 | ## 2.2.0
361 |
362 | * Allow Ranges for :level option. Credits to Ying Tsen Hong.
363 | * Changing the API of `Helpers#render_navigation`. Now supports Ranges for
364 | `:level` option and :expand_all to render all levels as expanded. Old Api is
365 | still supported, but deprecated.
366 | * Deprecated `render_all_levels` in config-file.
367 |
368 | ## 2.1.0
369 |
370 | * included Ben Marini's commit which allows individual id-generators.
371 | Thanks Ben!
372 | * added ability to specify navigation items through items_provider. This is
373 | useful for generating the navigation dynamically (e.g. from database)
374 | * items can now even be passed directly into render_navigation method.
375 |
376 | ## 2.0.1
377 |
378 | * fixed handling of a non-existent explicit navigation item for a navigation
379 | context
380 |
381 | ## 2.0.0
382 |
383 | * added auto_highlight feature. Active navigation is determined by comparing
384 | urls, no need to explicitly set it in the controllers anymore. Thanks to Jack
385 | Dempsey and Florian Hanke for the support on this.
386 | * added ability to create multi-level navigations (not just limited to primary
387 | and secondary navigation). Thanks again to Jack Dempsey for the motivation ;-)
388 | * simplified the process to explicitly set the navigation in the controller
389 | (where needed) - only deepest level has to be specified
390 | * made auto_highlight feature configurable both on global and item_container's
391 | level
392 | * config file is now evaluated in template if ever possible (not in controller
393 | anymore)
394 |
395 | ## 1.4.2
396 |
397 | * explicitly loading all source files when requiring 'simple_navigation'.
398 |
399 | ## 1.4.0
400 |
401 | * added the capability to have several navigation-contexts
402 | * doc-fix
403 |
404 | ## 1.3.1
405 |
406 | * now compliant with ruby 1.9.1 (thanks to Gernot Kogler for the feedback)
407 |
408 | ## 1.3.0
409 |
410 | * `render_all_levels` option allows to render all subnavigation independent from
411 | the active primary navigation ('full open tree'). Userful for javascript
412 | menus. Thanks to Richard Hulse.
413 | * ability to turn off automatic generation of dom_ids for the list items
414 | (autogenerate_item_ids). Credits again to Richard Hulse.
415 | * ability to specify dom_class for primary and secondary lists. Thanks Richard!
416 |
417 | ## 1.2.2
418 |
419 | * renderers now have access to request_forgery_protection stuff (this allows
420 | delete-links as navigation-items)
421 |
422 | ## 1.2.1
423 |
424 | * changed way to include render_*-helper_methods into view (including them into
425 | Controller and declaring them as helper_methods instead of adding whole module
426 | as Helper). this seems to be more reliable under certain conditions. Credits
427 | to Gernot Kogler.
428 |
429 | ## 1.2.0
430 |
431 | * added capability to add conditions to navigation-items
432 | (`primary.item key, name, url, :if => Proc.new {current_user.admin?}`)
433 |
434 | ## 1.1.2
435 |
436 | * Bugfix: config now gets evaluated on every render_navigation call. Credits to
437 | Joël Azémar.
438 | * Config file gets reloaded on every render_navigation call in development mode.
439 | Only load config file on server start in production mode.
440 |
441 | ## 1.1.1
442 |
443 | * Change plugin into a GemPlugin
444 |
--------------------------------------------------------------------------------
/spec/simple_navigation/item_spec.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | describe Item do
3 | let!(:item_container) { ItemContainer.new }
4 |
5 | let(:adapter) { double(:adapter) }
6 | let(:item_args) { [item_container, :my_key, 'name', url, options] }
7 | let(:item) { Item.new(*item_args) }
8 | let(:options) { Hash.new }
9 | let(:url) { 'url' }
10 |
11 | before { allow(SimpleNavigation).to receive_messages(adapter: adapter) }
12 |
13 | describe '#highlights_on' do
14 | let(:options) {{ highlights_on: :test }}
15 |
16 | it "returns the item's highlights_on option" do
17 | expect(item.highlights_on).to eq :test
18 | end
19 | end
20 |
21 | describe '#initialize' do
22 | context 'when there is a sub_navigation' do
23 | let(:subnav_container) { double(:subnav_container).as_null_object }
24 |
25 | shared_examples 'creating sub navigation container' do
26 | it 'creates a sub navigation container with a level+1' do
27 | expect(item.sub_navigation.level).to eq 2
28 | end
29 | end
30 |
31 | context 'when a block is given' do
32 | it_behaves_like 'creating sub navigation container' do
33 | let(:item) { Item.new(*item_args) {} }
34 | end
35 |
36 | it 'calls the block' do
37 | allow(ItemContainer).to receive_messages(new: subnav_container)
38 |
39 | expect{ |blk|
40 | Item.new(*item_args, &blk)
41 | }.to yield_with_args(subnav_container)
42 | end
43 | end
44 |
45 | context 'when no block is given' do
46 | context 'and items are given' do
47 | let(:items) { [] }
48 | let(:options) {{ items: items }}
49 |
50 | it_behaves_like 'creating sub navigation container'
51 |
52 | it "sets the items on the subnav_container" do
53 | expect(item.sub_navigation.items).to eq items
54 | end
55 | end
56 |
57 | context 'and no items are given' do
58 | it "doesn't create a new ItemContainer" do
59 | item = Item.new(*item_args)
60 | expect(item.sub_navigation).to be_nil
61 | end
62 | end
63 | end
64 | end
65 |
66 | context 'when a :method option is given' do
67 | let(:options) {{ method: :delete }}
68 |
69 | it "sets the item's method" do
70 | expect(item.method).to eq :delete
71 | end
72 | end
73 |
74 | context 'when no :method option is given' do
75 | it "sets the item's method to nil" do
76 | expect(item.method).to be_nil
77 | end
78 | end
79 |
80 | context 'when an :highlights_on option is given' do
81 | let(:highlights_on) { double(:highlights_on) }
82 | let(:options) {{ highlights_on: highlights_on }}
83 |
84 | it "sets the item's highlights_on" do
85 | expect(item.highlights_on).to eq highlights_on
86 | end
87 | end
88 |
89 | context 'when no :highlights_on option is given' do
90 | it "sets the item's highlights_on to nil" do
91 | expect(item.highlights_on).to be_nil
92 | end
93 | end
94 |
95 | context 'when a url is given' do
96 | context 'and it is a string' do
97 | it "sets the item's url accordingly" do
98 | expect(item.url).to eq 'url'
99 | end
100 | end
101 |
102 | context 'and it is a proc' do
103 | let(:url) { proc{ "my_" + "url" } }
104 |
105 | it "sets the item's url accordingly" do
106 | expect(item.url).to eq 'my_url'
107 | end
108 | end
109 |
110 | context 'and it is nil' do
111 | let(:url) { nil }
112 |
113 | it "sets the item's url accordingly" do
114 | expect(item.url).to be_nil
115 | end
116 | end
117 | end
118 |
119 | context 'when no url nor options is specified' do
120 | let(:item_args) { [item_container, :my_key, 'name'] }
121 |
122 | it "sets the item's url to nil" do
123 | expect(item.url).to be_nil
124 | end
125 | end
126 |
127 | context 'when only a url is given' do
128 | let(:item_args) { [item_container, :my_key, 'name', 'url'] }
129 |
130 | it "set the item's url accordingly" do
131 | expect(item.url).to eq 'url'
132 | end
133 | end
134 |
135 | context 'when url and options are given' do
136 | let(:options) {{ html: { option: true } }}
137 |
138 | before { allow(adapter).to receive_messages(current_page?: false) }
139 |
140 | it "set the item's url accordingly" do
141 | expect(item.url).to eq 'url'
142 | end
143 |
144 | it "sets the item's html_options accordingly" do
145 | allow(item).to \
146 | receive_messages(selected_by_subnav?: false,
147 | selected_by_condition?: false)
148 | expect(item.html_options).to include(option: true)
149 | end
150 | end
151 | end
152 |
153 | describe '#link_html_options' do
154 | let(:options) {{ link_html: :test }}
155 |
156 | it "returns the item's link_html option" do
157 | expect(item.link_html_options).to eq :test
158 | end
159 | end
160 |
161 | describe '#method' do
162 | let(:options) {{ method: :test }}
163 |
164 | it "returns the item's method option" do
165 | expect(item.method).to eq :test
166 | end
167 | end
168 |
169 | describe '#name' do
170 | before do
171 | allow(SimpleNavigation.config).to \
172 | receive_messages(name_generator: proc{ |name| "#{name}" })
173 | end
174 |
175 | context 'when no option is given' do
176 | context 'and the name_generator uses only the name' do
177 | it 'uses the default name_generator' do
178 | expect(item.name).to eq 'name'
179 | end
180 | end
181 |
182 | context 'and the name_generator uses only the item itself' do
183 | before do
184 | allow(SimpleNavigation.config).to \
185 | receive_messages(name_generator: proc{ |name, item| "#{item.key}" })
186 | end
187 |
188 | it 'uses the default name_generator' do
189 | expect(item.name).to eq 'my_key'
190 | end
191 | end
192 | end
193 |
194 | context 'when the :apply_generator is false' do
195 | it "returns the item's name" do
196 | expect(item.name(apply_generator: false)).to eq 'name'
197 | end
198 | end
199 |
200 | context 'when a block is given' do
201 | let(:item_args) { [item_container, :my_key, -> { 'Name in block' }, url, options] }
202 |
203 | it "returns the item's name that is defined in the block" do
204 | expect(item.name).to include 'Name in block'
205 | end
206 | end
207 | end
208 |
209 | describe '#selected?' do
210 | context 'when the item has no :highlights_on option' do
211 | before { allow(SimpleNavigation).to receive_messages(config: config) }
212 |
213 | context 'and auto highlighting is off' do
214 | let(:config) { double(:config, auto_highlight: false) }
215 |
216 | it 'returns false' do
217 | expect(item.selected?).to be false
218 | end
219 | end
220 |
221 | context 'and auto highlighting is on' do
222 | let(:config) { double(:config, ignore_query_params_on_auto_highlight: true, ignore_anchors_on_auto_highlight: true, auto_highlight: true) }
223 |
224 | context "and the current url matches the item's url" do
225 | before { allow(adapter).to receive_messages(current_page?: true) }
226 |
227 | it 'returns true' do
228 | expect(item.selected?).to be true
229 | end
230 | end
231 |
232 | context "and the current url does not match the item's url" do
233 | let(:config) do
234 | double(:config, auto_highlight: false, highlight_on_subpath: false)
235 | end
236 |
237 | before { allow(adapter).to receive_messages(current_page?: false) }
238 |
239 | it 'returns false' do
240 | expect(item.selected?).to be false
241 | end
242 | end
243 |
244 | context 'and highlights_on_subpath is on' do
245 | let(:config) do
246 | double(:config, auto_highlight: true, highlight_on_subpath: true, ignore_query_params_on_auto_highlight: true, ignore_anchors_on_auto_highlight: true)
247 | end
248 |
249 | context "but item has no url" do
250 | let(:url) { nil }
251 |
252 | it 'returns false' do
253 | expect(item.selected?).to be false
254 | end
255 | end
256 |
257 | context "and the current url is a sub path of the item's url" do
258 | before do
259 | allow(adapter).to \
260 | receive_messages(current_page?: false, request_uri: 'url/test')
261 | end
262 |
263 | it 'returns true' do
264 | expect(item.selected?).to be true
265 | end
266 | end
267 |
268 | context "and the current url is not a sub path of the item's url" do
269 | before do
270 | allow(adapter).to \
271 | receive_messages(current_page?: false, request_uri: 'other/test')
272 | end
273 |
274 | it 'returns false' do
275 | expect(item.selected?).to be false
276 | end
277 | end
278 | end
279 | end
280 | end
281 |
282 | context 'when the item has a :highlights_on option' do
283 | context 'and it is a regular expression' do
284 | before { allow(adapter).to receive_messages(request_uri: '/test') }
285 |
286 | context 'and the current url matches the expression' do
287 | let(:options) {{ highlights_on: /test/ }}
288 |
289 | it 'returns true' do
290 | expect(item.selected?).to be true
291 | end
292 | end
293 |
294 | context 'and the current url does not match the expression' do
295 | let(:options) {{ highlights_on: /other/ }}
296 |
297 | it 'returns false' do
298 | expect(item.selected?).to be false
299 | end
300 | end
301 | end
302 |
303 | context 'and it is a callable object' do
304 | context 'and the call returns true' do
305 | let(:options) {{ highlights_on: -> { true } }}
306 |
307 | it 'returns true' do
308 | expect(item.selected?).to be true
309 | end
310 | end
311 |
312 | context 'and the call returns false' do
313 | let(:options) {{ highlights_on: -> { false } }}
314 |
315 | it 'returns false' do
316 | expect(item.selected?).to be false
317 | end
318 | end
319 | end
320 |
321 | context 'and it is the :subpath symbol' do
322 | let(:options) {{ highlights_on: :subpath }}
323 |
324 | context "and the current url is a sub path of the item's url" do
325 | before do
326 | allow(adapter).to receive_messages(request_uri: 'url/test')
327 | end
328 |
329 | it 'returns true' do
330 | expect(item.selected?).to be true
331 | end
332 | end
333 |
334 | context "and the current url is not a sub path of the item's url" do
335 | before do
336 | allow(adapter).to receive_messages(request_uri: 'other/test')
337 | end
338 |
339 | it 'returns false' do
340 | expect(item.selected?).to be false
341 | end
342 | end
343 | end
344 |
345 | context 'and it is non usable' do
346 | let(:options) {{ highlights_on: :hello }}
347 |
348 | it 'raises an exception' do
349 | expect{ item.selected? }.to raise_error(ArgumentError, ':highlights_on must be a Regexp, Proc or :subpath')
350 | end
351 | end
352 | end
353 | end
354 |
355 | describe '#selected_class' do
356 | context 'when the item is selected' do
357 | before { allow(item).to receive_messages(selected?: true) }
358 |
359 | it 'returns the default selected_class' do
360 | expect(item.selected_class).to eq 'selected'
361 | end
362 |
363 | context 'and selected_class is defined in the context' do
364 | before { allow(item_container).to receive_messages(selected_class: 'defined') }
365 |
366 | it "returns the context's selected_class" do
367 | expect(item.selected_class).to eq 'defined'
368 | end
369 | end
370 | end
371 |
372 | context 'when the item is not selected' do
373 | before { allow(item).to receive_messages(selected?: false) }
374 |
375 | it 'returns nil' do
376 | expect(item.selected_class).to be_nil
377 | end
378 | end
379 | end
380 |
381 | describe ':html_options argument' do
382 | let(:selected_classes) { 'selected simple-navigation-active-leaf' }
383 |
384 | context 'when the :class option is given' do
385 | let(:options) {{ html: { class: 'my_class' } }}
386 |
387 | context 'and the item is selected' do
388 | before { allow(item).to receive_messages(selected?: true, selected_by_condition?: true) }
389 |
390 | it "adds the specified class to the item's html classes" do
391 | expect(item.html_options[:class]).to include('my_class')
392 | end
393 |
394 | it "doesn't replace the default html classes of a selected item" do
395 | expect(item.html_options[:class]).to include(selected_classes)
396 | end
397 | end
398 |
399 | context "and the item isn't selected" do
400 | before { allow(item).to receive_messages(selected?: false, selected_by_condition?: false) }
401 |
402 | it "sets the specified class as the item's html classes" do
403 | expect(item.html_options[:class]).to include('my_class')
404 | end
405 | end
406 | end
407 |
408 | context "when the :class option isn't given" do
409 | context 'and the item is selected' do
410 | before { allow(item).to receive_messages(selected?: true, selected_by_condition?: true) }
411 |
412 | it "sets the default html classes of a selected item" do
413 | expect(item.html_options[:class]).to include(selected_classes)
414 | end
415 | end
416 |
417 | context "and the item isn't selected" do
418 | before { allow(item).to receive_messages(selected?: false, selected_by_condition?: false) }
419 |
420 | it "doesn't set any html class on the item" do
421 | expect(item.html_options[:class]).to be_blank
422 | end
423 | end
424 | end
425 |
426 | shared_examples 'generating id' do |id|
427 | it "sets the item's html id to the specified id" do
428 | expect(item.html_options[:id]).to eq id
429 | end
430 | end
431 |
432 | describe 'when the :id option is given' do
433 | let(:options) {{ html: { id: 'my_id' } }}
434 |
435 | before do
436 | allow(SimpleNavigation.config).to receive_messages(autogenerate_item_ids: generate_ids)
437 | allow(item).to receive_messages(selected?: false, selected_by_condition?: false)
438 | end
439 |
440 | context 'and :autogenerate_item_ids is true' do
441 | let(:generate_ids) { true }
442 |
443 | it_behaves_like 'generating id', 'my_id'
444 | end
445 |
446 | context 'and :autogenerate_item_ids is false' do
447 | let(:generate_ids) { false }
448 |
449 | it_behaves_like 'generating id', 'my_id'
450 | end
451 | end
452 |
453 | context "when the :id option isn't given" do
454 | before do
455 | allow(SimpleNavigation.config).to receive_messages(autogenerate_item_ids: generate_ids)
456 | allow(item).to receive_messages(selected?: false, selected_by_condition?: false)
457 | end
458 |
459 | context 'and :autogenerate_item_ids is true' do
460 | let(:generate_ids) { true }
461 |
462 | it_behaves_like 'generating id', 'my_key'
463 | end
464 |
465 | context 'and :autogenerate_item_ids is false' do
466 | let(:generate_ids) { false }
467 |
468 | it "doesn't set any html id on the item" do
469 | expect(item.html_options[:id]).to be_blank
470 | end
471 | end
472 | end
473 | end
474 | end
475 | end
476 |
--------------------------------------------------------------------------------
/spec/simple_navigation/item_container_spec.rb:
--------------------------------------------------------------------------------
1 | module SimpleNavigation
2 | describe ItemContainer do
3 | subject(:item_container) { ItemContainer.new }
4 |
5 | shared_examples 'adding the item to the list' do
6 | it 'adds the item to the list' do
7 | allow(Item).to receive_messages(new: item)
8 | item_container.item(*args)
9 | expect(item_container.items).to include(item)
10 | end
11 | end
12 |
13 | shared_examples 'not adding the item to the list' do
14 | it "doesn't add the item to the list" do
15 | allow(Item).to receive_messages(new: item)
16 | item_container.item(*args)
17 | expect(item_container.items).not_to include(item)
18 | end
19 | end
20 |
21 | describe '#initialize' do
22 | it 'sets an empty items array' do
23 | expect(item_container.items).to be_empty
24 | end
25 | end
26 |
27 | describe '#dom_attributes' do
28 | let(:dom_attributes) {{ id: 'test_id', class: 'test_class' }}
29 |
30 | before { item_container.dom_attributes = dom_attributes }
31 |
32 | it "returns the container's dom_attributes" do
33 | expect(item_container.dom_attributes).to eq dom_attributes
34 | end
35 |
36 | context 'when the dom_attributes do not contain any id or class' do
37 | let(:dom_attributes) {{ test: 'test' }}
38 |
39 | context "and the container hasn't any dom_id" do
40 | it "returns the contaier's dom_attributes without any id" do
41 | expect(item_container.dom_attributes).not_to include(:id)
42 | end
43 | end
44 |
45 | context 'and the container has a dom_id' do
46 | before { item_container.dom_id = 'test_id' }
47 |
48 | it "returns the contaier's dom_attributes including the #dom_id" do
49 | expect(item_container.dom_attributes).to include(id: 'test_id')
50 | end
51 | end
52 |
53 | context "and the container hasn't any dom_class" do
54 | it "returns the contaier's dom_attributes without any class" do
55 | expect(item_container.dom_attributes).not_to include(:class)
56 | end
57 | end
58 |
59 | context 'and the container has a dom_class' do
60 | before { item_container.dom_class = 'test_class' }
61 |
62 | it "returns the contaier's dom_attributes including the #dom_class" do
63 | expect(item_container.dom_attributes).to include(class: 'test_class')
64 | end
65 | end
66 | end
67 | end
68 |
69 | describe '#items=' do
70 | let(:item) {{ key: :my_key, name: 'test', url: '/' }}
71 | let(:items) { [item] }
72 | let(:item_adapter) { double(:item_adapter).as_null_object }
73 | let(:real_item) { double(:real_item) }
74 |
75 | before do
76 | allow(ItemAdapter).to receive_messages(new: item_adapter)
77 | allow(item_adapter).to receive(:to_simple_navigation_item)
78 | .with(item_container)
79 | .and_return(real_item)
80 | end
81 |
82 | context 'when the item should be added' do
83 | before { allow(item_container).to receive_messages(should_add_item?: true) }
84 |
85 | it 'converts it to an Item and adds it to the items collection' do
86 | item_container.items = items
87 | expect(item_container.items).to include(real_item)
88 | end
89 | end
90 |
91 | context 'when the item should not be added' do
92 | before { allow(item_container).to receive_messages(should_add_item?: false) }
93 |
94 | it "doesn't add it to the items collection" do
95 | item_container.items = items
96 | expect(item_container.items).not_to include(real_item)
97 | end
98 | end
99 | end
100 |
101 | describe '#selected?' do
102 | let(:item_1) { double(:item, selected?: false) }
103 | let(:item_2) { double(:item, selected?: false) }
104 |
105 | before do
106 | item_container.instance_variable_set(:@items, [item_1, item_2])
107 | end
108 |
109 | context 'when no item is selected' do
110 | it 'returns nil' do
111 | expect(item_container).not_to be_selected
112 | end
113 | end
114 |
115 | context 'when an item is selected' do
116 | it 'returns true' do
117 | allow(item_1).to receive_messages(selected?: true)
118 | expect(item_container).to be_selected
119 | end
120 | end
121 | end
122 |
123 | describe '#selected_item' do
124 | let(:item_1) { double(:item, selected?: false) }
125 | let(:item_2) { double(:item, selected?: false) }
126 |
127 | before(:each) do
128 | allow(SimpleNavigation).to receive_messages(current_navigation_for: :nav)
129 | allow(item_container).to receive_messages(:[] => nil)
130 | item_container.instance_variable_set(:@items, [item_1, item_2])
131 | end
132 |
133 | context "when navigation isn't explicitely set" do
134 | context 'and no item is selected' do
135 | it 'returns nil' do
136 | expect(item_container.selected_item).to be_nil
137 | end
138 | end
139 |
140 | context 'and an item selected' do
141 | before { allow(item_1).to receive_messages(selected?: true) }
142 |
143 | it 'returns the selected item' do
144 | expect(item_container.selected_item).to be item_1
145 | end
146 | end
147 | end
148 | end
149 |
150 | describe '#active_item_container_for' do
151 | context "when the desired level is the same as the container's" do
152 | it 'returns the container itself' do
153 | expect(item_container.active_item_container_for(1)).to be item_container
154 | end
155 | end
156 |
157 | context "when the desired level is different than the container's" do
158 | context 'and no subnavigation is selected' do
159 | before { allow(item_container).to receive_messages(selected_sub_navigation?: false) }
160 |
161 | it 'returns nil' do
162 | expect(item_container.active_item_container_for(2)).to be_nil
163 | end
164 | end
165 |
166 | context 'and a subnavigation is selected' do
167 | let(:sub_navigation) { double(:sub_navigation) }
168 | let(:selected_item) { double(:selected_item) }
169 |
170 | before do
171 | allow(item_container).to \
172 | receive_messages(selected_sub_navigation?: true, selected_item: selected_item)
173 | allow(selected_item).to receive_messages(sub_navigation: sub_navigation)
174 | end
175 |
176 | it 'calls recursively on the sub_navigation' do
177 | expect(sub_navigation).to receive(:active_item_container_for)
178 | .with(2)
179 | item_container.active_item_container_for(2)
180 | end
181 | end
182 | end
183 | end
184 |
185 | describe '#active_leaf_container' do
186 | context 'when the current container has a selected subnavigation' do
187 | let(:sub_navigation) { double(:sub_navigation) }
188 | let(:selected_item) { double(:selected_item) }
189 |
190 | before do
191 | allow(item_container).to receive_messages(selected_sub_navigation?: true,
192 | selected_item: selected_item)
193 | allow(selected_item).to receive_messages(sub_navigation: sub_navigation)
194 | end
195 |
196 | it 'calls recursively on the sub_navigation' do
197 | expect(sub_navigation).to receive(:active_leaf_container)
198 | item_container.active_leaf_container
199 | end
200 | end
201 |
202 | context 'when the current container is the leaf already' do
203 | before { allow(item_container).to receive_messages(selected_sub_navigation?: false) }
204 |
205 | it 'returns itsself' do
206 | expect(item_container.active_leaf_container).to be item_container
207 | end
208 | end
209 | end
210 |
211 | describe '#item' do
212 | let(:options) { Hash.new }
213 | let(:item) { double(:item) }
214 |
215 | context 'when a block is given' do
216 | let(:block) { proc{} }
217 | let(:sub_container) { double(:sub_container) }
218 |
219 | it 'yields a new ItemContainer' do
220 | allow_any_instance_of(Item).to \
221 | receive_messages(sub_navigation: sub_container)
222 |
223 | expect{ |blk|
224 | item_container.item('key', 'name', 'url', options, &blk)
225 | }.to yield_with_args(sub_container)
226 | end
227 |
228 | it "creates a new Item with the given params and block" do
229 | allow(Item).to receive(:new)
230 | .with(item_container, 'key', 'name', 'url', options, &block)
231 | .and_return(item)
232 | item_container.item('key', 'name', 'url', options, &block)
233 | expect(item_container.items).to include(item)
234 | end
235 |
236 | it 'adds the created item to the list of items' do
237 | item_container.item('key', 'name', 'url', options) {}
238 | expect(item_container.items).not_to include(item)
239 | end
240 | end
241 |
242 | context 'when no block is given' do
243 | it 'creates a new Item with the given params and no sub navigation' do
244 | allow(Item).to receive(:new)
245 | .with(item_container, 'key', 'name', 'url', options)
246 | .and_return(item)
247 | item_container.item('key', 'name', 'url', options)
248 | expect(item_container.items).to include(item)
249 | end
250 |
251 | it 'adds the created item to the list of items' do
252 | allow(Item).to receive_messages(new: item)
253 | item_container.item('key', 'name', 'url', options) {}
254 | expect(item_container.items).to include(item)
255 | end
256 | end
257 |
258 | describe 'Optional url and optional options' do
259 | context 'when item specifed without url or options' do
260 | it_behaves_like 'adding the item to the list' do
261 | let(:args) { ['key', 'name'] }
262 | end
263 | end
264 |
265 | context 'when item is specified with only a url' do
266 | it_behaves_like 'adding the item to the list' do
267 | let(:args) { ['key', 'name', 'url'] }
268 | end
269 | end
270 |
271 | context 'when item is specified with only options' do
272 | context 'and options do not contain any condition' do
273 | it_behaves_like 'adding the item to the list' do
274 | let(:args) { ['key', 'name', { option: true }] }
275 | end
276 | end
277 |
278 | context 'and options contains a negative condition' do
279 | it_behaves_like 'not adding the item to the list' do
280 | let(:args) { ['key', 'name', nil, { if: ->{ false }, option: true }] }
281 | end
282 | end
283 |
284 | context 'and options contains a positive condition' do
285 | it_behaves_like 'adding the item to the list' do
286 | let(:args) { ['key', 'name', nil, { if: ->{ true }, option: true }] }
287 | end
288 | end
289 | end
290 |
291 | context 'when item is specified with a url and options' do
292 | context 'and options do not contain any condition' do
293 | it_behaves_like 'adding the item to the list' do
294 | let(:args) { ['key', 'name', 'url', { option: true }] }
295 | end
296 | end
297 |
298 | context 'and options contains a negative condition' do
299 | it_behaves_like 'not adding the item to the list' do
300 | let(:args) { ['key', 'name', 'url', { if: ->{ false }, option: true }] }
301 | end
302 | end
303 |
304 | context 'and options contains a positive condition' do
305 | it_behaves_like 'adding the item to the list' do
306 | let(:args) { ['key', 'name', 'url', { if: ->{ true }, option: true }] }
307 | end
308 | end
309 | end
310 |
311 | context 'when a frozen options hash is given' do
312 | let(:options) do
313 | { html: { id: 'test' } }.freeze
314 | end
315 |
316 | it 'does not raise an exception' do
317 | expect{
318 | item_container.item('key', 'name', 'url', options)
319 | }.not_to raise_error
320 | end
321 | end
322 |
323 | describe "container options" do
324 | before do
325 | allow(item_container).to receive_messages(should_add_item?: add_item)
326 | item_container.item :key, 'name', 'url', options
327 | end
328 |
329 | context 'when the container :id option is specified' do
330 | let(:options) {{ container: { id: 'c_id' } }}
331 |
332 | context 'and the item should be added' do
333 | let(:add_item) { true }
334 |
335 | it 'changes its dom_id' do
336 | expect(item_container.dom_id).to eq 'c_id'
337 | end
338 | end
339 |
340 | context "and the item shouldn't be added" do
341 | let(:add_item) { false }
342 |
343 | it "doesn't change its dom_id" do
344 | expect(item_container.dom_id).to be_nil
345 | end
346 | end
347 | end
348 |
349 | context 'when the container :class option is specified' do
350 | let(:options) {{ container: { class: 'c_class' } }}
351 |
352 | context 'and the item should be added' do
353 | let(:add_item) { true }
354 |
355 | it 'changes its dom_class' do
356 | expect(item_container.dom_class).to eq 'c_class'
357 | end
358 | end
359 |
360 | context "and the item shouldn't be added" do
361 | let(:add_item) { false }
362 |
363 | it "doesn't change its dom_class" do
364 | expect(item_container.dom_class).to be_nil
365 | end
366 | end
367 | end
368 |
369 | context 'when the container :attributes option is specified' do
370 | let(:options) {{ container: { attributes: { option: true } } }}
371 |
372 | context 'and the item should be added' do
373 | let(:add_item) { true }
374 |
375 | it 'changes its dom_attributes' do
376 | expect(item_container.dom_attributes).to eq(option: true)
377 | end
378 | end
379 |
380 | context "and the item shouldn't be added" do
381 | let(:add_item) { false }
382 |
383 | it "doesn't change its dom_attributes" do
384 | expect(item_container.dom_attributes).to eq({})
385 | end
386 | end
387 | end
388 |
389 | context 'when the container :selected_class option is specified' do
390 | let(:options) {{ container: { selected_class: 'sel_class' } }}
391 |
392 | context 'and the item should be added' do
393 | let(:add_item) { true }
394 |
395 | it 'changes its selected_class' do
396 | expect(item_container.selected_class).to eq 'sel_class'
397 | end
398 | end
399 |
400 | context "and the item shouldn't be added" do
401 | let(:add_item) { false }
402 |
403 | it "doesn't change its selected_class" do
404 | expect(item_container.selected_class).to be_nil
405 | end
406 | end
407 | end
408 | end
409 | end
410 |
411 | describe 'Conditions' do
412 | context 'when an :if option is given' do
413 | let(:options) {{ if: proc{condition} }}
414 | let(:condition) { nil }
415 |
416 | context 'and it evals to true' do
417 | let(:condition) { true }
418 |
419 | it 'creates a new Item' do
420 | expect(Item).to receive(:new)
421 | item_container.item('key', 'name', 'url', options)
422 | end
423 | end
424 |
425 | context 'and it evals to false' do
426 | let(:condition) { false }
427 |
428 | it "doesn't create a new Item" do
429 | expect(Item).not_to receive(:new)
430 | item_container.item('key', 'name', 'url', options)
431 | end
432 | end
433 |
434 | context 'and it is not a proc or a method' do
435 | it 'raises an error' do
436 | expect{
437 | item_container.item('key', 'name', 'url', { if: 'text' })
438 | }.to raise_error(ArgumentError, ':if or :unless must be procs or lambdas')
439 | end
440 | end
441 | end
442 |
443 | context 'when an :unless option is given' do
444 | let(:options) {{ unless: proc{condition} }}
445 | let(:condition) { nil }
446 |
447 | context 'and it evals to false' do
448 | let(:condition) { false }
449 |
450 | it 'creates a new Navigation-Item' do
451 | expect(Item).to receive(:new)
452 | item_container.item('key', 'name', 'url', options)
453 | end
454 | end
455 |
456 | context 'and it evals to true' do
457 | let(:condition) { true }
458 |
459 | it "doesn't create a new Navigation-Item" do
460 | expect(Item).not_to receive(:new)
461 | item_container.item('key', 'name', 'url', options)
462 | end
463 | end
464 | end
465 | end
466 | end
467 |
468 | describe '#[]' do
469 | before do
470 | item_container.item(:first, 'first', 'bla')
471 | item_container.item(:second, 'second', 'bla')
472 | item_container.item(:third, 'third', 'bla')
473 | end
474 |
475 | it 'returns the item with the specified navi_key' do
476 | expect(item_container[:second].name).to eq 'second'
477 | end
478 |
479 | context 'when no item exists for the specified navi_key' do
480 | it 'returns nil' do
481 | expect(item_container[:invalid]).to be_nil
482 | end
483 | end
484 | end
485 |
486 | describe '#render' do
487 | # TODO
488 | let(:renderer_instance) { double(:renderer).as_null_object }
489 | let(:renderer_class) { double(:renderer_class, new: renderer_instance) }
490 |
491 | context 'when renderer is specified as an option' do
492 | context 'and is specified as a class' do
493 | it 'instantiates the passed renderer_class with the options' do
494 | expect(renderer_class).to receive(:new)
495 | .with(renderer: renderer_class)
496 | item_container.render(renderer: renderer_class)
497 | end
498 |
499 | it 'calls render on the renderer and passes self' do
500 | expect(renderer_instance).to receive(:render).with(item_container)
501 | item_container.render(renderer: renderer_class)
502 | end
503 | end
504 |
505 | context 'and is specified as a symbol' do
506 | before do
507 | SimpleNavigation.registered_renderers = {
508 | my_renderer: renderer_class
509 | }
510 | end
511 |
512 | it "instantiates the passed renderer_class with the options" do
513 | expect(renderer_class).to receive(:new).with(renderer: :my_renderer)
514 | item_container.render(renderer: :my_renderer)
515 | end
516 |
517 | it 'calls render on the renderer and passes self' do
518 | expect(renderer_instance).to receive(:render).with(item_container)
519 | item_container.render(renderer: :my_renderer)
520 | end
521 | end
522 | end
523 |
524 | context 'when no renderer is specified' do
525 | let(:options) { Hash.new }
526 |
527 | before { allow(item_container).to receive_messages(renderer: renderer_class) }
528 |
529 | it "instantiates the container's renderer with the options" do
530 | expect(renderer_class).to receive(:new).with(options)
531 | item_container.render(options)
532 | end
533 |
534 | it 'calls render on the renderer and passes self' do
535 | expect(renderer_instance).to receive(:render).with(item_container)
536 | item_container.render(options)
537 | end
538 | end
539 | end
540 |
541 | describe '#renderer' do
542 | context 'when no renderer is set explicitly' do
543 | it 'returns globally-configured renderer' do
544 | expect(item_container.renderer).to be Configuration.instance.renderer
545 | end
546 | end
547 |
548 | context 'when a renderer is set explicitly' do
549 | let(:renderer) { double(:renderer) }
550 |
551 | before { item_container.renderer = renderer }
552 |
553 | it 'returns the specified renderer' do
554 | expect(item_container.renderer).to be renderer
555 | end
556 | end
557 | end
558 |
559 | describe '#level_for_item' do
560 | before(:each) do
561 | item_container.item(:p1, 'p1', 'p1')
562 | item_container.item(:p2, 'p2', 'p2') do |p2|
563 | p2.item(:s1, 's1', 's1')
564 | p2.item(:s2, 's2', 's2') do |s2|
565 | s2.item(:ss1, 'ss1', 'ss1')
566 | s2.item(:ss2, 'ss2', 'ss2')
567 | end
568 | p2.item(:s3, 's3', 's3')
569 | end
570 | item_container.item(:p3, 'p3', 'p3')
571 | end
572 |
573 | shared_examples 'returning the level of an item' do |item, level|
574 | specify{ expect(item_container.level_for_item(item)).to eq level }
575 | end
576 |
577 | it_behaves_like 'returning the level of an item', :p1, 1
578 | it_behaves_like 'returning the level of an item', :p3, 1
579 | it_behaves_like 'returning the level of an item', :s1, 2
580 | it_behaves_like 'returning the level of an item', :ss1, 3
581 | it_behaves_like 'returning the level of an item', :x, nil
582 | end
583 |
584 | describe '#empty?' do
585 | context 'when there are no items' do
586 | it 'returns true' do
587 | item_container.instance_variable_set(:@items, [])
588 | expect(item_container).to be_empty
589 | end
590 | end
591 |
592 | context 'when there are some items' do
593 | it 'returns false' do
594 | item_container.instance_variable_set(:@items, [double(:item)])
595 | expect(item_container).not_to be_empty
596 | end
597 | end
598 | end
599 | end
600 | end
601 |
--------------------------------------------------------------------------------