├── .ruby-gemset ├── Gemfile ├── Appraisals ├── gemfiles ├── 4.2.gemfile └── 5.0.gemfile ├── test ├── views │ └── working │ │ ├── default.html.erb │ │ ├── with_open_close_tabs.html.erb │ │ ├── with_option_active_class.html.erb │ │ ├── with_item_options.html.erb │ │ └── with_item_block.html.erb ├── test_helper.rb ├── dummy.rb └── unit │ ├── builder_test.rb │ ├── tabs_builder_test.rb │ ├── tabs_test.rb │ └── action_controller_test.rb ├── .gitignore ├── .travis.yml ├── lib ├── tabs_on_rails │ ├── version.rb │ ├── railtie.rb │ ├── tabs.rb │ ├── tabs │ │ ├── builder.rb │ │ └── tabs_builder.rb │ └── action_controller.rb └── tabs_on_rails.rb ├── Rakefile ├── LICENSE.txt ├── tabs_on_rails.gemspec ├── CHANGELOG.md └── README.md /.ruby-gemset: -------------------------------------------------------------------------------- 1 | tabs_on_rails 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "4.2" do 2 | gem "rails", "~> 4.2.0" 3 | end 4 | appraise "5.0" do 5 | gem "rails", "~> 5.0.0" 6 | end 7 | -------------------------------------------------------------------------------- /gemfiles/4.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 4.2.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/5.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 5.0.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /test/views/working/default.html.erb: -------------------------------------------------------------------------------- 1 | <%= tabs_tag do |tabs| %> 2 | <%= tabs.dashboard 'Dashboard', '/d' %> 3 | <%= tabs.welcome 'Welcome', '/w' %> 4 | <% end -%> 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Bundler 2 | /.bundle/ 3 | /Gemfile.lock 4 | gemfiles/*.gemfile.lock 5 | 6 | # YARD 7 | /.yardoc 8 | /_yardoc/ 9 | 10 | # Artifacts 11 | /pkg/ 12 | /tmp/ 13 | -------------------------------------------------------------------------------- /test/views/working/with_open_close_tabs.html.erb: -------------------------------------------------------------------------------- 1 | <%= tabs_tag(:open_tabs => { :id => "tabs" }, :close_tabs => { :class => "ignored" }) do |tabs| %> 2 | <%= tabs.dashboard 'Dashboard', '/d' %> 3 | <%= tabs.welcome 'Welcome', '/w' %> 4 | <% end -%> -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 2.2.5 3 | - 2.3.1 4 | - 2.4.0 5 | 6 | gemfile: 7 | - gemfiles/4.2.gemfile 8 | - gemfiles/5.0.gemfile 9 | 10 | before_install: 11 | - gem install bundler 12 | 13 | env: 14 | global: 15 | - NOKOGIRI_USE_SYSTEM_LIBRARIES=true 16 | -------------------------------------------------------------------------------- /test/views/working/with_option_active_class.html.erb: -------------------------------------------------------------------------------- 1 | <%= tabs_tag(:open_tabs => { :id => "tabs" }, :close_tabs => { :class => "ignored" }, :active_class => "active") do |tabs| %> 2 | <%= tabs.dashboard 'Dashboard', '/d' %> 3 | <%= tabs.welcome 'Welcome', '/w' %> 4 | <% end -%> -------------------------------------------------------------------------------- /lib/tabs_on_rails/version.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Tabs on Rails 3 | # 4 | # A simple Ruby on Rails plugin for creating and managing Tabs. 5 | # 6 | # Copyright (c) 2009-2017 Simone Carletti 7 | #++ 8 | 9 | 10 | module TabsOnRails 11 | VERSION = "3.0.0" 12 | end 13 | -------------------------------------------------------------------------------- /test/views/working/with_item_options.html.erb: -------------------------------------------------------------------------------- 1 | <%= tabs_tag(:open_tabs => { :id => "tabs" }, :close_tabs => { :class => "ignored" }) do |tabs| %> 2 | <%= tabs.dashboard 'Dashboard', '/d', :class => "custom" %> 3 | <%= tabs.welcome 'Welcome', '/w', :class => "custom" %> 4 | <% end -%> -------------------------------------------------------------------------------- /test/views/working/with_item_block.html.erb: -------------------------------------------------------------------------------- 1 | <%= tabs_tag(:builder => WorkingMixinTestController::BlockBuilder) do |tabs| %> 2 | <%= tabs.dashboard 'Dashboard', '/d', :class => "custom" %> 3 | <%= tabs.welcome 'Welcome', '/w', :class => "custom" do %> 4 | 5 | <% end %> 6 | <% end %> -------------------------------------------------------------------------------- /lib/tabs_on_rails/railtie.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Tabs on Rails 3 | # 4 | # A simple Ruby on Rails plugin for creating and managing Tabs. 5 | # 6 | # Copyright (c) 2009-2017 Simone Carletti 7 | #++ 8 | 9 | 10 | module TabsOnRails 11 | 12 | class Railtie < Rails::Railtie 13 | ActiveSupport.on_load(:action_controller) do 14 | ::ActionController::Base.send(:include, TabsOnRails::ActionController) 15 | end 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /lib/tabs_on_rails.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Tabs on Rails 3 | # 4 | # A simple Ruby on Rails plugin for creating and managing Tabs. 5 | # 6 | # Copyright (c) 2009-2017 Simone Carletti 7 | #++ 8 | 9 | 10 | require 'tabs_on_rails/tabs' 11 | require 'tabs_on_rails/version' 12 | require 'tabs_on_rails/action_controller' 13 | require 'tabs_on_rails/railtie' 14 | 15 | 16 | module TabsOnRails 17 | 18 | NAME = "Tabs on Rails" 19 | GEM = "tabs_on_rails" 20 | 21 | end 22 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'mocha/setup' 3 | require 'dummy' 4 | 5 | $:.unshift File.expand_path('../../lib', __FILE__) 6 | require 'tabs_on_rails' 7 | 8 | Encoding.default_external = Encoding::UTF_8 9 | Encoding.default_internal = Encoding::UTF_8 10 | 11 | 12 | class ActiveSupport::TestCase 13 | def current_tab(namespace) 14 | case namespace 15 | when nil, :default 16 | :dashboard 17 | when :foospace 18 | :footab 19 | else 20 | :elsetab 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'appraisal' 3 | 4 | if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"] 5 | task :default => :appraisal 6 | else 7 | task :default => :test 8 | end 9 | 10 | 11 | require 'rake/testtask' 12 | 13 | Rake::TestTask.new do |t| 14 | t.libs << "test" 15 | t.test_files = FileList["test/**/*_test.rb"] 16 | t.verbose = !!ENV["VERBOSE"] 17 | t.warning = !!ENV["WARNING"] 18 | end 19 | 20 | 21 | require 'yard/rake/yardoc_task' 22 | 23 | YARD::Rake::YardocTask.new(:yardoc) do |y| 24 | y.options = ["--output-dir", "yardoc"] 25 | end 26 | 27 | CLOBBER.include "yardoc" 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2017 Simone Carletti 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/dummy.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | 3 | require "active_support" 4 | require "action_controller" 5 | require "rails/railtie" 6 | 7 | 8 | class Dummy 9 | Routes = ActionDispatch::Routing::RouteSet.new 10 | Routes.draw do 11 | match ':controller(/:action(/:id))', via: [:get] 12 | end 13 | end 14 | 15 | ActionController::Base.view_paths = File.join(File.dirname(__FILE__), 'views') 16 | ActionController::Base.send :include, Dummy::Routes.url_helpers 17 | 18 | class ActiveSupport::TestCase 19 | 20 | setup do 21 | @routes = Dummy::Routes 22 | end 23 | 24 | 25 | def controller 26 | @controller_proxy ||= ControllerProxy.new(@controller) 27 | end 28 | 29 | class ControllerProxy 30 | def initialize(controller) 31 | @controller = controller 32 | end 33 | def method_missing(method, *args) 34 | @controller.instance_eval do 35 | m = method(method) 36 | m.call(*args) 37 | end 38 | end 39 | end 40 | 41 | end 42 | 43 | # Trigger lazy loading and causes the load_hooks to be executed on 44 | # ActionController::API. This is important because breacrumbs_on_rails includes 45 | # BreadcrumbsOnRails::ActionController on any module that executes these hooks 46 | ActionController::API rescue NameError 47 | -------------------------------------------------------------------------------- /tabs_on_rails.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'tabs_on_rails/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "tabs_on_rails" 8 | spec.version = TabsOnRails::VERSION 9 | spec.authors = ["Simone Carletti"] 10 | spec.email = ["weppos@weppos.net"] 11 | 12 | spec.summary = %q{A simple Ruby on Rails plugin for creating tabs and navigation menu} 13 | spec.description = %q{TabsOnRails is a simple Ruby on Rails plugin for creating tabs and navigation menu for a Rails project.} 14 | spec.homepage = "https://simonecarletti.com/code/tabs-on-rails" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | spec.require_paths = ["lib"] 20 | 21 | spec.required_ruby_version = ">= 2.2" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "bundler" 24 | spec.add_development_dependency "appraisal" 25 | spec.add_development_dependency "rails", ">= 4.2" 26 | spec.add_development_dependency "mocha", ">= 1.0" 27 | spec.add_development_dependency "yard" 28 | end 29 | -------------------------------------------------------------------------------- /lib/tabs_on_rails/tabs.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Tabs on Rails 3 | # 4 | # A simple Ruby on Rails plugin for creating and managing Tabs. 5 | # 6 | # Copyright (c) 2009-2017 Simone Carletti 7 | #++ 8 | 9 | 10 | require 'tabs_on_rails/tabs/builder' 11 | require 'tabs_on_rails/tabs/tabs_builder' 12 | 13 | 14 | module TabsOnRails 15 | 16 | class Tabs 17 | class << self 18 | attr_writer :default_builder 19 | 20 | def default_builder 21 | @default_builder ||= TabsBuilder 22 | end 23 | end 24 | 25 | def initialize(context, options = {}) 26 | @context = context 27 | @builder = (options.delete(:builder) || self.class.default_builder).new(@context, options) 28 | @options = options 29 | end 30 | 31 | %w(open_tabs close_tabs).each do |name| 32 | define_method(name) do |*args| # def open_tabs(*args) 33 | method = @builder.method(name) # method = @builder.method(:open_tabs) 34 | if method.arity.zero? # if method.arity.zero? 35 | method.call # method.call 36 | else # else 37 | method.call(*args) # method.call(*args) 38 | end # end 39 | end # end 40 | end 41 | 42 | def method_missing(*args, &block) 43 | @builder.tab_for(*args, &block) 44 | end 45 | 46 | 47 | # Renders the tab stack using the current builder. 48 | # 49 | # Returns the String HTML content. 50 | def render(&block) 51 | raise LocalJumpError, "no block given" unless block_given? 52 | 53 | options = @options.dup 54 | open_tabs_options = options.delete(:open_tabs) || {} 55 | close_tabs_options = options.delete(:close_tabs) || {} 56 | 57 | String.new.tap do |html| 58 | html << open_tabs(open_tabs_options).to_s 59 | html << @context.capture(self, &block) 60 | html << close_tabs(close_tabs_options).to_s 61 | end.html_safe 62 | end 63 | 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /lib/tabs_on_rails/tabs/builder.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Tabs on Rails 3 | # 4 | # A simple Ruby on Rails plugin for creating and managing Tabs. 5 | # 6 | # Copyright (c) 2009-2017 Simone Carletti 7 | #++ 8 | 9 | 10 | module TabsOnRails 11 | class Tabs 12 | 13 | # 14 | # = Builder 15 | # 16 | # The Builder class represents the interface for any custom Builder. 17 | # 18 | # To create a custom Builder, extend this class 19 | # and implement the following abstract methods: 20 | # 21 | # * tab_for 22 | # 23 | # Optionally, you can override the following methods to customize 24 | # the Builder behavior: 25 | # 26 | # * open_tabs 27 | # * close_tabs 28 | # 29 | class Builder 30 | 31 | # Initializes a new builder with the given hash of options, 32 | # providing the current Rails template as context. 33 | # 34 | # Warning: You should not override this method to prevent incompatibility with future versions. 35 | def initialize(context, options = {}) 36 | @context = context 37 | @namespace = options.delete(:namespace) || :default 38 | @options = options 39 | end 40 | 41 | # Returns true if +tab+ is the +current_tab+. 42 | # 43 | # Examples 44 | # 45 | # class MyController < ApplicationController 46 | # tab :foo 47 | # end 48 | # 49 | # current_tab? :foo # => true 50 | # current_tab? 'foo' # => true 51 | # current_tab? :bar # => false 52 | # current_tab? 'bar' # => false 53 | # 54 | def current_tab?(tab) 55 | tab.to_s == @context.current_tab(@namespace).to_s 56 | end 57 | 58 | 59 | # Creates and returns a tab with given +args+. 60 | # 61 | # Raises NotImplemented: you should implement this method in your custom Builder. 62 | def tab_for(*args) 63 | raise NotImplementedError 64 | end 65 | 66 | # Override this method to use a custom open tag for your tabs. 67 | def open_tabs(*args) 68 | end 69 | 70 | # Override this method to use a custom close tag for your tabs. 71 | def close_tabs(*args) 72 | end 73 | 74 | end 75 | 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/tabs_on_rails/tabs/tabs_builder.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Tabs on Rails 3 | # 4 | # A simple Ruby on Rails plugin for creating and managing Tabs. 5 | # 6 | # Copyright (c) 2009-2017 Simone Carletti 7 | #++ 8 | 9 | 10 | module TabsOnRails 11 | class Tabs 12 | 13 | # 14 | # = Tabs Builder 15 | # 16 | # The TabsBuilder is and example of custom Builder. 17 | # 18 | class TabsBuilder < Builder 19 | 20 | # Returns a link_to +tab+ with +name+ and +options+ if +tab+ is not the current tab, 21 | # a simple tab name wrapped by a span tag otherwise. 22 | # 23 | # current_tab? :foo # => true 24 | # 25 | # tab_for :foo, 'Foo', foo_path 26 | # # => "
  • Foo
  • " 27 | # 28 | # tab_for :bar, 'Bar', bar_path 29 | # # => "
  • Bar
  • " 30 | # 31 | # You can pass a hash of item_options 32 | # to customize the behavior and the style of the li element. 33 | # 34 | # # Pass a custom class to the element 35 | # tab_for :bar, 'Bar', bar_path, :class => "custom" 36 | # # => "
  • Bar
  • " 37 | # 38 | # Implements Builder#tab_for. 39 | # 40 | def tab_for(tab, name, url_options, item_options = {}) 41 | item_options[:class] = item_options[:class].to_s.split(" ").push(@options[:active_class] || "current").join(" ") if current_tab?(tab) 42 | content = @context.link_to_unless(current_tab?(tab), name, url_options) do 43 | @context.content_tag(:span, name) 44 | end 45 | @context.content_tag(:li, content, item_options) 46 | end 47 | 48 | # Returns an unordered list open tag. 49 | # 50 | # The options hash is used to customize the HTML attributes of the tag. 51 | # 52 | # open_tag 53 | # # => "
      " 54 | # 55 | # open_tag :class => "centered" 56 | # # => "
        " 57 | # 58 | # Implements Builder#open_tabs. 59 | # 60 | def open_tabs(options = {}) 61 | @context.tag("ul", options, open = true) 62 | end 63 | 64 | # Returns an unordered list close tag. 65 | # 66 | # The options hash is ignored here. 67 | # It exists only for coherence with the parent Builder API. 68 | # 69 | # close_tag 70 | # # => "
      " 71 | # 72 | # close_tag :class => "centered" 73 | # # => "
    " 74 | # 75 | # Implements Builder#close_tabs. 76 | # 77 | def close_tabs(options = {}) 78 | "".html_safe 79 | end 80 | 81 | end 82 | 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/unit/builder_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class BuilderTest < ActionView::TestCase 4 | 5 | def setup 6 | @template = self 7 | @builder = TabsOnRails::Tabs::Builder.new(@template) 8 | end 9 | 10 | 11 | def test_initialize_without_context 12 | assert_raise(ArgumentError) { TabsOnRails::Tabs::Builder.new } 13 | end 14 | 15 | def test_initialize_with_context 16 | assert_nothing_raised { TabsOnRails::Tabs::Builder.new(@template) } 17 | end 18 | 19 | def test_initialize_with_options 20 | assert_nothing_raised { TabsOnRails::Tabs::Builder.new(@template, { :namespace => "foonamespace" }) } 21 | end 22 | 23 | def test_initialize_should_set_context 24 | assert_equal @template, @builder.instance_variable_get(:"@context") 25 | end 26 | 27 | def test_initialize_should_set_namespace 28 | @builder = TabsOnRails::Tabs::Builder.new(@template) 29 | assert_equal :default, @builder.instance_variable_get(:"@namespace") 30 | 31 | @builder = TabsOnRails::Tabs::Builder.new(@template, :namespace => :foobar) 32 | assert_equal :foobar, @builder.instance_variable_get(:"@namespace") 33 | end 34 | 35 | def test_initialize_should_set_options 36 | @builder = TabsOnRails::Tabs::Builder.new(@template) 37 | assert_equal({}, @builder.instance_variable_get(:"@options")) 38 | 39 | @builder = TabsOnRails::Tabs::Builder.new(@template, :weather => :sunny) 40 | assert_equal({ :weather => :sunny }, @builder.instance_variable_get(:"@options")) 41 | 42 | @builder = TabsOnRails::Tabs::Builder.new(@template, :namespace => :foobar, :weather => :sunny) 43 | assert_equal({ :weather => :sunny }, @builder.instance_variable_get(:"@options")) 44 | 45 | @builder = TabsOnRails::Tabs::Builder.new(@template, :namespace => :foobar) 46 | assert_equal({}, @builder.instance_variable_get(:"@options")) 47 | end 48 | 49 | 50 | def test_current_tab 51 | @builder = TabsOnRails::Tabs::Builder.new(@template) 52 | assert @builder.current_tab?(:dashboard) 53 | assert @builder.current_tab?("dashboard") 54 | assert !@builder.current_tab?(:welcome) 55 | assert !@builder.current_tab?("welcome") 56 | end 57 | 58 | def test_current_tab_with_namespace 59 | @builder = TabsOnRails::Tabs::Builder.new(@template, :namespace => :foospace) 60 | assert @builder.current_tab?(:footab) 61 | assert @builder.current_tab?("footab") 62 | assert !@builder.current_tab?(:dashboard) 63 | assert !@builder.current_tab?("dashboard") 64 | end 65 | 66 | 67 | def test_open_tabs 68 | assert_nil @builder.open_tabs 69 | end 70 | 71 | def test_open_tabs_with_options 72 | assert_nil @builder.open_tabs(:foo => "bar") 73 | end 74 | 75 | def test_close_tabs 76 | assert_nil @builder.close_tabs 77 | end 78 | 79 | def test_close_tabs_with_options 80 | assert_nil @builder.close_tabs(:foo => "bar") 81 | end 82 | 83 | 84 | def test_tab_for_should_raise_not_implemented_error 85 | assert_raise(NotImplementedError) { @builder.tab_for } 86 | assert_raise(NotImplementedError) { @builder.tab_for("foo") } 87 | assert_raise(NotImplementedError) { @builder.tab_for("foo", "bar") } 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /test/unit/tabs_builder_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TabsBuilderTest < ActionView::TestCase 4 | 5 | def setup 6 | @builder = TabsOnRails::Tabs::TabsBuilder.new(self) 7 | end 8 | 9 | def test_inherits_from_builder 10 | assert_equal TabsOnRails::Tabs::Builder, TabsOnRails::Tabs::TabsBuilder.superclass 11 | end 12 | 13 | 14 | def test_open_tabs 15 | assert_dom_equal %Q{
      }, @builder.open_tabs 16 | end 17 | 18 | def test_open_tabs_with_options 19 | assert_dom_equal %Q{
        }, @builder.open_tabs(:style => "foo") 20 | end 21 | 22 | def test_close_tabs 23 | assert_dom_equal %Q{
      }, @builder.close_tabs 24 | end 25 | 26 | def test_close_tabs_with_options 27 | assert_dom_equal %Q{
    }, @builder.close_tabs(:foo => "bar") 28 | end 29 | 30 | 31 | def test_tab_for_should_return_link_to_unless_current_tab 32 | assert_dom_equal %Q{
  • Welcome
  • }, 33 | @builder.tab_for(:welcome, 'Welcome', '#') 34 | assert_dom_equal %Q{
  • Foo Welcome
  • }, 35 | @builder.tab_for(:welcome, 'Foo Welcome', '#') 36 | end 37 | 38 | def test_tab_for_should_return_span_if_current_tab 39 | assert_dom_equal %Q{
  • Dashboard
  • }, 40 | @builder.tab_for(:dashboard, 'Dashboard', '#') 41 | assert_dom_equal %Q{
  • Foo Dashboard
  • }, 42 | @builder.tab_for(:dashboard, 'Foo Dashboard', '#') 43 | end 44 | 45 | def test_tab_for_with_options_as_uri 46 | assert_dom_equal %Q{
  • Foo Bar
  • }, 47 | @builder.tab_for(:welcome, 'Foo Bar', 'http://welcome.com/') 48 | assert_dom_equal %Q{
  • Foo Bar
  • }, 49 | @builder.tab_for(:dashboard, 'Foo Bar', 'http://dashboard.com/') 50 | end 51 | 52 | def test_tab_for_with_item_options 53 | assert_dom_equal %Q{
  • Dashboard
  • }, 54 | @builder.tab_for(:dashboard, "Dashboard", "#", :class => "custom") 55 | assert_dom_equal %Q{
  • Welcome
  • }, 56 | @builder.tab_for(:welcome, "Welcome", "#", :class => "custom") 57 | assert_dom_equal %Q{
  • Dashboard
  • }, 58 | @builder.tab_for(:dashboard, "Dashboard", "#", :style => "padding: 10px") 59 | assert_dom_equal %Q{
  • Welcome
  • }, 60 | @builder.tab_for(:welcome, "Welcome", "#", :style => "padding: 10px") 61 | end 62 | 63 | def test_tab_for_with_option_active_class 64 | @builder = TabsOnRails::Tabs::TabsBuilder.new(self, :active_class => "active") 65 | assert_dom_equal %Q{
  • Dashboard
  • }, 66 | @builder.tab_for(:dashboard, "Dashboard", "#") 67 | assert_dom_equal %Q{
  • Dashboard
  • }, 68 | @builder.tab_for(:dashboard, "Dashboard", "#", :class => "custom") 69 | assert_dom_equal %Q{
  • Welcome
  • }, 70 | @builder.tab_for(:welcome, "Welcome", "#") 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/unit/tabs_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TabsTest < ActionView::TestCase 4 | 5 | OpenZeroArgsBuilder = Class.new(TabsOnRails::Tabs::Builder) do 6 | def open_tabs 7 | '
      ' 8 | end 9 | end 10 | 11 | CloseZeroArgsBuilder = Class.new(TabsOnRails::Tabs::Builder) do 12 | def close_tabs 13 | '
    ' 14 | end 15 | end 16 | 17 | BlockBuilder = Class.new(TabsOnRails::Tabs::Builder) do 18 | def tab_for(tab, name, options, item_options = {}, &block) 19 | item_options[:class] = item_options[:class].to_s.split(" ").push("current").join(" ") if current_tab?(tab) 20 | content = @context.link_to(name, options) 21 | content += @context.capture(&block) if block_given? 22 | @context.content_tag(:li, content, item_options) 23 | end 24 | end 25 | 26 | 27 | def setup 28 | @template = self 29 | @tabs = TabsOnRails::Tabs.new(@template) 30 | end 31 | 32 | 33 | def test_initialize 34 | @tabs = TabsOnRails::Tabs.new(@template) 35 | assert_equal @template, @tabs.instance_variable_get(:"@context") 36 | assert_instance_of TabsOnRails::Tabs::TabsBuilder, @tabs.instance_variable_get(:"@builder") 37 | end 38 | 39 | def test_initialize_with_option_builder 40 | builder = Class.new(TabsOnRails::Tabs::TabsBuilder) 41 | @tabs = TabsOnRails::Tabs.new(@template, :builder => builder) 42 | assert_equal @template, @tabs.instance_variable_get(:"@context") 43 | assert_instance_of builder, @tabs.instance_variable_get(:"@builder") 44 | end 45 | 46 | def test_initialize_with_default_builder 47 | default_builder = TabsOnRails::Tabs.default_builder 48 | TabsOnRails::Tabs.default_builder = CustomBuilder 49 | 50 | custom_tab = TabsOnRails::Tabs.new(@template) # doesnt need argument :builder => CustomBuilder 51 | 52 | assert_instance_of CustomBuilder, custom_tab.instance_variable_get(:"@builder") 53 | 54 | TabsOnRails::Tabs.default_builder = default_builder 55 | end 56 | 57 | 58 | def test_open_tabs 59 | assert_equal '
      ', @tabs.open_tabs 60 | end 61 | 62 | def test_open_tabs_with_options 63 | assert_equal '
        ', @tabs.open_tabs(:class => "foo") 64 | end 65 | 66 | def test_open_tabs_should_ignore_options_if_arity_is_zero 67 | @tabs = TabsOnRails::Tabs.new(@template, :builder => OpenZeroArgsBuilder) 68 | assert_nothing_raised do 69 | assert_equal '
          ', @tabs.open_tabs(:class => "foo") 70 | end 71 | end 72 | 73 | 74 | def test_close_tabs 75 | assert_equal '
        ', @tabs.close_tabs 76 | end 77 | 78 | def test_close_tabs_with_options 79 | assert_equal '
      ', @tabs.close_tabs(:class => "foo") 80 | end 81 | 82 | def test_open_tabs_should_ignore_options_if_arity_is_zero 83 | @tabs = TabsOnRails::Tabs.new(@template, :builder => CloseZeroArgsBuilder) 84 | assert_nothing_raised do 85 | assert_equal '
    ', @tabs.close_tabs(:class => "foo") 86 | end 87 | end 88 | 89 | 90 | def test_tab_for 91 | assert_equal %Q{
  • Welcome
  • }, 92 | @tabs.welcome('Welcome', '#') 93 | end 94 | 95 | def test_tab_for_with_block 96 | @tabs = TabsOnRails::Tabs.new(@template, :builder => BlockBuilder) 97 | expected = %Q{
  • Foo BarMore Content
  • } 98 | result = @tabs.dashboard('Foo Bar', 'http://dashboard.com/') do 99 | "More Content" 100 | end 101 | 102 | assert_dom_equal expected, result 103 | end 104 | 105 | class CustomBuilder < TabsOnRails::Tabs::Builder 106 | def open_tabs(options = {}) 107 | @context.tag("ul", options, open = true) 108 | end 109 | 110 | def close_tabs(options = {}) 111 | "".html_safe 112 | end 113 | 114 | def tab_for(tab, name, options, item_options = {}) 115 | item_options[:class] = (current_tab?(tab) ? 'active' : '') 116 | @context.content_tag(:li, item_options) do 117 | @context.link_to(name, options) 118 | end 119 | end 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## main 4 | 5 | - FIXED: Frozen string warning (GH-43) 6 | 7 | 8 | ## Release 3.0.0 9 | 10 | - CHANGED: Compatible with Rails 5. 11 | - CHANGED: Dropped support for Rails before 4.2 and Ruby before 2.2. 12 | 13 | 14 | ## Release 2.2.0 15 | 16 | * NEW: Ability to configure a default builder (GH-20). [Thanks @ilstar] 17 | 18 | * NEW: Ability to pass a custom value for the "active" class (GH-23). [Thanks @ruurd] 19 | 20 | 21 | ## Release 2.1.1 22 | 23 | * FIXED: An invalid replace command caused a bug in 2.1.0. 24 | 25 | 26 | ## Release 2.1.0 27 | 28 | * NEW: Support for Rails 3.2. 29 | 30 | * FIXED: Fixed Rails 3.2 ActiveSupport::Concern deprecation warning (GH-18). 31 | 32 | 33 | ## Release 2.0.2 34 | 35 | * NEW: Added support for nested tabs and ability 36 | to pass a block to the #tab_for method (#12, #9, #11). 37 | 38 | However, the default TabsBuilder builder does not implement it. 39 | Create a custom builder and specify how the block should be rendered. 40 | 41 | * CHANGED: Removed deprecated #has_rdoc= RubyGems attribute. 42 | 43 | 44 | ## Release 2.0.1 45 | 46 | * NEW: Added support for rubygems-test. 47 | 48 | 49 | ## Release 2.0.0 50 | 51 | Nothing changed. No longer a --pre release. 52 | 53 | 54 | ## Release 2.0.0.pre2 55 | 56 | * FIXED: Fixed permission error (#8) 57 | 58 | 59 | ## Release 2.0.0.pre 60 | 61 | * CHANGED: Dropped support for Rails 2. 62 | 63 | * CHANGED: Remove "Incomplete `close_tabs' definition" warning. open_tabs/close_tabs can now have zero or more arguments. 64 | 65 | 66 | ## Release 1.3.2 67 | 68 | * CHANGED: Use Bundler to declare dependencies. 69 | 70 | * FIXED: Some Rails 2 user can experience a NoMethodError: undefined method `html_safe'. 71 | 72 | 73 | ## Release 1.3.1 74 | 75 | * FIXED: Error wrong number of arguments (2 for 3) (closes #3) 76 | 77 | 78 | ## Release 1.3.0 79 | 80 | * NEW: Ability to customize the behavior and the style of the li tab item 81 | passing a hash of options. 82 | 83 | <%= tabs_tag do |tab| %> 84 | <%= tab.home 'Homepage', root_path, :style => "padding: 10px" %> 85 | <%= tab.dashboard 'Dashboard', dashboard_path %> 86 | <% end %> 87 | 88 | 93 | 94 | 95 | ## Release 1.2.0 96 | 97 | * NEW: Rails 3 compatibility. 98 | 99 | * CHANGED: Simplified tabs_tag helper by moving the rendering logic into TabsOnRails::Tabs#render. 100 | 101 | * CHANGED: TabsOnRails::Tabs::Builder (and all child classes) now has full access to @options hash. 102 | 103 | 104 | ## Release 1.1.0 105 | 106 | * FIXED: Incompatibility with release < 1.0.0 caused by open_tabs and close_tabs methods changes compared to 0.8.0. 107 | 108 | * CHANGED: Removed BUILD and STATUS constants. Added Version::ALPHA constant to be used when I need to package prereleases (see RubyGem --prerelease flag). 109 | 110 | * CHANGED: Removed empty install/uninstall hooks and tasks folder. 111 | 112 | * CHANGED: Removed rails/init hook because deprecated in Rails 3. 113 | 114 | * CHANGED: Drop dependency from Echoe. Change Rakefile to use custom tasks. 115 | 116 | * REMOVED: Deleted empty install.rb file. 117 | 118 | 119 | ## Release 1.0.0 120 | 121 | * NEW: Ability to pass arbitrary options to open_tabs and close_tags method. Thanks to aaronchi (closes #315) 122 | 123 | * REMOVED: tabs_tag no longer accepts a Builder as first parameter. Removed deprecation warning. 124 | 125 | First stable release. 126 | 127 | 128 | ## Release 0.8.2 129 | 130 | * CHANGED: GitHub Gem Building is Defunct. The gem is now hosted on Gemcutter (see http://github.com/blog/515-gem-building-is-defunct) 131 | 132 | 133 | ## Release 0.8.1 134 | 135 | * CHANGED: Controller#set_tab now uses #send instead of #instance_eval (better performance and more security) 136 | 137 | * CHANGED: run test against Rails ~> 2.3.0 but ensure compatibility with Rails 2.2.x. 138 | 139 | 140 | ## Release 0.8.0 141 | 142 | * FIXED: Invalid usage of the word namescope instead of namespace. 143 | 144 | * FIXED: :current_tab? not available as helper method (closes #229). 145 | 146 | * FIXED: GitHub now requires the Manifest file to be included in the repos. 147 | 148 | * FIXED: Controller methods should be protected/private (closes #228). 149 | 150 | * CHANGED: Status to Beta and bumped release (closes #227). 151 | 152 | * REMOVED: Deprecated current_tab setter method. Use set_tab instead. 153 | 154 | 155 | ## Release 0.3.0 156 | 157 | * NEW: Support for namespaces in order to manage concurrent tab menus (closes #144). 158 | 159 | * FIXED: `Uninitialized constant RAILS_DEFAULT_LOGGER (NameError)' error message when running tests outside a Rails environment. 160 | 161 | * FIXED: Tests complains when Rails 2.3.2 is installed. This library is 100% compatible with Rails 2.3.2 but for now let's force tests to use Rails 2.2.2. 162 | 163 | * CHANGED: current_tab= controller instance/class methods are now deprecated. Use set_tab instead. 164 | 165 | * CHANGED: Calling tabs_tag with a custom builder as first parameter is now deprecated. Use :builder option instead. 166 | 167 | 168 | ## Release 0.2.0 169 | 170 | * NEW: The README file is definitely more useful now, filled up with some basic documentation. 171 | 172 | * CHANGED: Use the standard way to initialize a Rails plugin when packaged as a GEM (closes #146). 173 | 174 | * CHANGED: Removed development version warning (closes #145). 175 | 176 | 177 | ## Release 0.1.0 178 | 179 | * Initial version 180 | -------------------------------------------------------------------------------- /lib/tabs_on_rails/action_controller.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Tabs on Rails 3 | # 4 | # A simple Ruby on Rails plugin for creating and managing Tabs. 5 | # 6 | # Copyright (c) 2009-2017 Simone Carletti 7 | #++ 8 | 9 | 10 | module TabsOnRails 11 | 12 | module ActionController 13 | extend ActiveSupport::Concern 14 | 15 | included do 16 | extend ClassMethods 17 | helper HelperMethods 18 | helper_method :current_tab, :current_tab? 19 | end 20 | 21 | 22 | protected 23 | 24 | # Sets the value for current tab to given name. 25 | # If you need to manage multiple tabs, 26 | # then you can pass an optional namespace. 27 | # 28 | # Examples 29 | # 30 | # set_tab :homepage 31 | # set_tab :dashboard, :menu 32 | # 33 | # Returns nothing. 34 | def set_tab(name, namespace = nil) 35 | tab_stack[namespace || :default] = name 36 | end 37 | 38 | # Returns the value for current tab in the default namespace, 39 | # or nil if no tab has been set before. 40 | # You can pass namespace to get the value 41 | # of the current tab for a different namespace. 42 | # 43 | # Examples 44 | # 45 | # current_tab # => nil 46 | # current_tab :menu # => nil 47 | # 48 | # set_tab :homepage 49 | # set_tab :dashboard, :menu 50 | # 51 | # current_tab # => :homepage 52 | # current_tab :menu # => :dashboard 53 | # 54 | # Returns the String/Symbol current tab. 55 | def current_tab(namespace = nil) 56 | tab_stack[namespace || :default] 57 | end 58 | 59 | # Checks if the current tab in namespace 60 | # matches name. 61 | # 62 | # Returns a Boolean. 63 | def current_tab?(name, namespace = nil) 64 | current_tab(namespace).to_s == name.to_s 65 | end 66 | 67 | # Initializes and/or returns the tab stack. 68 | # You won't probably need to use this method directly 69 | # unless you are trying to hack the plugin architecture. 70 | # 71 | # Returns the Hash stack. 72 | def tab_stack 73 | @tab_stack ||= {} 74 | end 75 | 76 | 77 | module ClassMethods 78 | 79 | # Sets the value for current tab to given name. 80 | # 81 | # set_tab :foo 82 | # 83 | # If you need to manage multiple tabs, then you can pass an optional namespace. 84 | # 85 | # set_tab :foo, :namespace 86 | # 87 | # The set_tab method understands all options you are used to pass to a Rails controller filter. 88 | # In fact, behind the scenes this method uses a before_filter 89 | # to store the tab in the @tab_stack variable. 90 | # For example, you can set the tab only for a restricted group of actions in the same controller 91 | # using the :only and :except options. 92 | # 93 | # Examples 94 | # 95 | # set_tab :foo 96 | # set_tab :foo, :except => :new 97 | # set_tab :foo, :only => [ :index, :show ] 98 | # 99 | # set_tab :foo, :namespace 100 | # set_tab :foo, :namespace, :only => [ :index, :show ] 101 | # 102 | def set_tab(*args) 103 | options = args.extract_options! 104 | name, namespace = args 105 | 106 | before_action(options) do |controller| 107 | controller.send(:set_tab, name, namespace) 108 | end 109 | end 110 | end 111 | 112 | module HelperMethods 113 | 114 | # In your template use the tabs_tag helper to create your tab. 115 | # 116 | # <%= tabs_tag do |tab| %> 117 | # <%= tab.home 'Homepage', root_path %> 118 | # <%= tab.dashboard 'Dashboard', dashboard_path %> 119 | # <%= tab.account 'Account', account_path %> 120 | # <% end %> 121 | # 122 | # The example above produces the following HTML output. 123 | # 124 | # 129 | # 130 | # The usage is similar to the Rails route file. 131 | # You create named tabs with the syntax tab.name_of_tab. 132 | # 133 | # The name you use creating a tab is the same you're going to refer to in your controller 134 | # when you want to mark a tab as the current tab. 135 | # 136 | # class DashboardController < ApplicationController 137 | # set_tab :dashboard 138 | # end 139 | # 140 | # Now, if the action belongs to DashboardController, 141 | # the template will automatically render the following HTML code. 142 | # 143 | # 148 | # 149 | # Use the current_tab helper method if you need to access 150 | # the value of current tab in your controller or template. 151 | # 152 | # class DashboardController < ApplicationController 153 | # set_tab :dashboard 154 | # end 155 | # 156 | # # In your view 157 | #

    The name of current tab is <%= current_tab %>.

    158 | # 159 | # 160 | # == Options 161 | # 162 | # You can pass the following options: 163 | # 164 | # - builder: the custom builder to use 165 | # - active_class: the custom CSS class to use for active links 166 | # 167 | # == Customizing a Tab 168 | # 169 | # You can pass a hash of options to customize the style and the behavior of the tab item. 170 | # Behind the scenes, each time you create a tab, the #tab_for 171 | # method is invoked. 172 | # 173 | # <%= tabs_tag do |tab| %> 174 | # <%= tab.home 'Homepage', root_path, :style => "padding: 10px" %> 175 | # <%= tab.dashboard 'Dashboard', dashboard_path %> 176 | # <%= tab.account 'Account', account_path, :class => "custom" %> 177 | # <% end %> 178 | # 179 | # 184 | # 185 | # You can pass any option supported by the
  • content_tag
  • Rails helper. 186 | # 187 | # See TabsOnRails::Tabs::TabsBuilder#tab_for for more details. 188 | # 189 | # == Customizing open_tabs and close_tabs 190 | # 191 | # The open_tabs and the close_tabs methods can be customized 192 | # with the :open_tabs and :close_tabs option. 193 | # 194 | # <%= tabs_tag :open_tabs => { :id => "tabs", :class => "cool" } do |tab| %> 195 | # <%= tab.home 'Homepage', root_path %> 196 | # <%= tab.dashboard 'Dashboard', dashboard_path %> 197 | # <%= tab.account 'Account', account_path %> 198 | # <% end %> 199 | # 200 | # 205 | # 206 | # Further customizations require a custom Builder. 207 | # 208 | def tabs_tag(options = {}, &block) 209 | Tabs.new(self, { :namespace => :default }.merge(options)).render(&block) 210 | end 211 | end 212 | 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /test/unit/action_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MixinTestController < ActionController::Base 4 | end 5 | 6 | class MixinTest < ActionController::TestCase 7 | tests MixinTestController 8 | 9 | def test_set_tab 10 | controller.set_tab :footab 11 | assert_equal(:footab, controller.tab_stack[:default]) 12 | end 13 | 14 | def test_set_tab_with_namespace 15 | controller.set_tab :footab, :namespace 16 | assert_equal(:footab, controller.tab_stack[:namespace]) 17 | end 18 | 19 | def test_set_tab_with_default_namespace 20 | controller.set_tab :footab, :default 21 | assert_equal(:footab, controller.tab_stack[:default]) 22 | end 23 | 24 | def test_set_tab_with_and_without_namespace 25 | controller.set_tab :firsttab 26 | controller.set_tab :secondtab, :custom 27 | assert_equal(:firsttab, controller.tab_stack[:default]) 28 | assert_equal(:secondtab, controller.tab_stack[:custom]) 29 | end 30 | 31 | 32 | def test_current_tab 33 | controller.tab_stack[:default] = :mytab 34 | assert_equal(:mytab, controller.current_tab) 35 | end 36 | 37 | def test_current_tab_with_namespace 38 | controller.tab_stack[:namespace] = :mytab 39 | assert_equal(:mytab, controller.current_tab(:namespace)) 40 | end 41 | 42 | def test_current_tab_with_default_namespace 43 | controller.tab_stack[:default] = :mytab 44 | assert_equal(:mytab, controller.current_tab(:default)) 45 | end 46 | 47 | def test_set_tab_with_and_without_namespace 48 | controller.tab_stack[:default] = :firsttab 49 | controller.tab_stack[:custom] = :secondtab 50 | assert_equal(:firsttab, controller.current_tab(:default)) 51 | assert_equal(:secondtab, controller.current_tab(:custom)) 52 | end 53 | 54 | 55 | def test_current_tab_question 56 | controller.tab_stack[:default] = :mytab 57 | assert( controller.current_tab?(:mytab)) 58 | assert(!controller.current_tab?(:yourtab)) 59 | end 60 | 61 | def test_current_tab_question_with_namespace 62 | controller.tab_stack[:custom] = :mytab 63 | assert( controller.current_tab?(:mytab, :custom)) 64 | assert(!controller.current_tab?(:yourtab, :custom)) 65 | end 66 | 67 | def test_current_tab_question_with_default_namespace 68 | controller.tab_stack[:default] = :mytab 69 | assert( controller.current_tab?(:mytab, :default)) 70 | assert(!controller.current_tab?(:yourtab, :default)) 71 | end 72 | 73 | def test_current_tab_question_with_and_without_namespace 74 | controller.tab_stack[:default] = :firsttab 75 | controller.tab_stack[:custom] = :secondtab 76 | assert( controller.current_tab?(:firsttab, :default)) 77 | assert(!controller.current_tab?(:secondtab, :default)) 78 | assert( controller.current_tab?(:secondtab, :custom)) 79 | assert(!controller.current_tab?(:firsttab, :custom)) 80 | end 81 | 82 | end 83 | 84 | 85 | class WorkingMixinTestController < ActionController::Base 86 | def self.controller_name; "working"; end 87 | def self.controller_path; "working"; end 88 | 89 | layout false 90 | 91 | set_tab :dashboard 92 | set_tab :welcome, :only => %w( action_welcome ) 93 | set_tab :dashboard, :only => %w( action_namespace ) 94 | set_tab :homepage, :namespace, :only => %w( action_namespace ) 95 | 96 | def action_dashboard 97 | execute("action_dashboard") 98 | end 99 | 100 | def action_namespace 101 | execute("action_namespace") 102 | end 103 | 104 | def action_welcome 105 | execute("action_welcome") 106 | end 107 | 108 | 109 | private 110 | 111 | def execute(method) 112 | if method.to_s =~ /^action_(.*)/ 113 | render :action => (params[:template] || 'default') 114 | end 115 | end 116 | 117 | 118 | class BlockBuilder < TabsOnRails::Tabs::TabsBuilder 119 | def tab_for(tab, name, options, item_options = {}, &block) 120 | item_options[:class] = item_options[:class].to_s.split(" ").push("current").join(" ") if current_tab?(tab) 121 | content = @context.link_to_unless(current_tab?(tab), name, options) do 122 | @context.content_tag(:span, name) 123 | end 124 | content += @context.capture(&block) if block_given? 125 | @context.content_tag(:li, content, item_options) 126 | end 127 | end 128 | 129 | end 130 | 131 | class WorkingMixinTest < ActionController::TestCase 132 | tests WorkingMixinTestController 133 | 134 | def test_render_default 135 | get :action_dashboard 136 | assert_dom_equal(%Q{
      137 |
    • Dashboard
    • 138 |
    • Welcome
    • 139 |
    }, @response.body) 140 | end 141 | 142 | def test_render_with_open_close_tabs 143 | get :action_dashboard, :template => "with_open_close_tabs" 144 | assert_dom_equal(%Q{
      145 |
    • Dashboard
    • 146 |
    • Welcome
    • 147 |
    }, @response.body) 148 | end 149 | 150 | def test_render_with_item_options 151 | get :action_dashboard, :template => "with_item_options" 152 | assert_dom_equal(%Q{
      153 |
    • Dashboard
    • 154 |
    • Welcome
    • 155 |
    }, @response.body) 156 | end 157 | 158 | def test_render_with_item_block 159 | get :action_dashboard, :template => "with_item_block" 160 | assert_dom_equal(%Q{
      161 |
    • Dashboard
    • 162 |
    • Welcome 163 | 164 |
    }, @response.body) 165 | end 166 | 167 | def test_render_with_option_active_class 168 | get :action_dashboard, :template => "with_option_active_class" 169 | assert_dom_equal(%Q{
      170 |
    • Dashboard
    • 171 |
    • Welcome
    • 172 |
    }, @response.body) 173 | end 174 | 175 | 176 | def test_set_tab 177 | get :action_dashboard 178 | assert_equal(:dashboard, controller.current_tab) 179 | assert_equal(:dashboard, controller.current_tab(:default)) 180 | assert_dom_equal(%Q{
      181 |
    • Dashboard
    • 182 |
    • Welcome
    • 183 |
    }, @response.body) 184 | end 185 | 186 | def test_set_tab_with_only_option 187 | get :action_welcome 188 | assert_equal :welcome, controller.current_tab 189 | assert_equal :welcome, controller.current_tab(:default) 190 | assert_dom_equal(%Q{}, @response.body) 194 | end 195 | 196 | def test_set_tab_with_namespace 197 | get :action_namespace 198 | assert_equal :dashboard, controller.current_tab 199 | assert_equal :dashboard, controller.current_tab(:default) 200 | assert_equal :homepage, controller.current_tab(:namespace) 201 | assert_dom_equal(%Q{
      202 |
    • Dashboard
    • 203 |
    • Welcome
    • 204 |
    }, @response.body) 205 | end 206 | 207 | 208 | def test_current_tab 209 | get :action_dashboard 210 | assert_equal :dashboard, controller.current_tab 211 | assert_equal :dashboard, controller.current_tab(:default) 212 | end 213 | 214 | def test_current_tab_question 215 | get :action_dashboard 216 | assert controller.current_tab?(:dashboard) 217 | assert controller.current_tab?(:dashboard, :default) 218 | assert !controller.current_tab?(:foobar) 219 | assert !controller.current_tab?(:foobar, :default) 220 | end 221 | 222 | end 223 | 224 | 225 | class ControllerMixinHelpersTest < ActionView::TestCase 226 | tests TabsOnRails::ActionController::HelperMethods 227 | include ActionView::Helpers::TagHelper 228 | include ActionView::Helpers::UrlHelper 229 | 230 | MockBuilder = Class.new(TabsOnRails::Tabs::Builder) do 231 | def tab_for(tab, name, *args) 232 | "-> #{name}" 233 | end 234 | end 235 | 236 | NilBoundariesBuilder = Class.new(TabsOnRails::Tabs::Builder) do 237 | def tab_for(tab, name, *args) 238 | @context.content_tag(:span, name) 239 | end 240 | end 241 | 242 | NilOpenBoundaryBuilder = Class.new(NilBoundariesBuilder) do 243 | def close_tabs(options = {}) 244 | '
    ' 245 | end 246 | end 247 | 248 | NilCloseBoundaryBuilder = Class.new(NilBoundariesBuilder) do 249 | def open_tabs(options = {}) 250 | '
    ' 251 | end 252 | end 253 | 254 | 255 | def test_tabs_tag_should_raise_local_jump_error_without_block 256 | assert_raise(LocalJumpError) { tabs_tag } 257 | end 258 | 259 | def test_tabs_tag_with_namespace 260 | tabs_tag(:builder => MockBuilder, :namespace => :custom) do |tabs| 261 | builder = tabs.instance_variable_get(:'@builder') 262 | assert_equal(:custom, builder.instance_variable_get(:'@namespace')) 263 | "" 264 | end 265 | end 266 | 267 | 268 | def test_tabs_tag_should_not_concat_open_close_tabs_when_nil 269 | content = tabs_tag(:builder => NilBoundariesBuilder) do |t| 270 | concat t.single('Single', '#') 271 | end 272 | 273 | assert_dom_equal 'Single', content 274 | end 275 | 276 | def test_tabs_tag_should_not_concat_open_tabs_when_nil 277 | content = tabs_tag(:builder => NilOpenBoundaryBuilder) do |t| 278 | concat t.single('Single', '#') 279 | end 280 | 281 | assert_dom_equal 'Single
    ', content 282 | end 283 | 284 | def test_tabs_tag_should_not_concat_close_tabs_when_nil 285 | content = tabs_tag(:builder => NilCloseBoundaryBuilder) do |t| 286 | concat t.single('Single', '#') 287 | end 288 | 289 | assert_dom_equal '
    Single', content 290 | end 291 | 292 | end 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tabs on Rails 2 | 3 | TabsOnRails is a simple Rails plugin for creating tabs and navigation menus. It provides helpers for generating navigation menus with a flexible interface. 4 | 5 | 6 | ## Requirements 7 | 8 | - Rails 4.2 or Rails 5 9 | 10 | For older versions of Ruby or Ruby on Rails, see the [CHANGELOG](CHANGELOG.md). 11 | 12 | 13 | ## Installation 14 | 15 | Add this line to your application's `Gemfile`: 16 | 17 | gem "tabs_on_rails" 18 | 19 | And then execute `bundle` to install the dependencies: 20 | 21 | $ bundle 22 | 23 | Use [Bundler](http://bundler.io/) and the `:git` option if you want to grab the latest version from the Git repository. 24 | 25 | 26 | ## Usage 27 | 28 | In your template use the `tabs_tag` helper to create your tab. 29 | 30 | ```ruby 31 | <%= tabs_tag do |tab| %> 32 | <%= tab.home 'Homepage', root_path %> 33 | <%= tab.dashboard 'Dashboard', dashboard_path %> 34 | <%= tab.account 'Account', account_path %> 35 | <% end %> 36 | ``` 37 | 38 | renders 39 | 40 | ```html 41 | 46 | ``` 47 | 48 | The usage is similar to the Rails route file. You create named tabs with the syntax `tab.name_of_tab`. The name you use creating a tab is the same you're going to refer to in your controller when you want to mark a tab as the current tab. 49 | 50 | ```ruby 51 | class DashboardController < ApplicationController 52 | set_tab :dashboard 53 | end 54 | ``` 55 | 56 | Now, if the action belongs to `DashboardController`, the template will automatically render the following HTML code. 57 | 58 | ```html 59 | 64 | ``` 65 | 66 | Use the `current_tab` helper method if you need to access the value of current tab in your controller or template. 67 | 68 | ```ruby 69 | class DashboardController < ApplicationController 70 | set_tab :dashboard 71 | end 72 | ``` 73 | 74 | In your view 75 | 76 | ```html 77 |

    The name of current tab is <%= current_tab %>.

    78 | ``` 79 | 80 | ### Customizing a Tab 81 | 82 | You can pass a hash of options to customize the style and the behavior of the tab item. 83 | Behind the scenes, each time you create a tab, the `#tab_for` method is invoked. 84 | 85 | ```ruby 86 | <%= tabs_tag do |tab| %> 87 | <%= tab.home 'Homepage', root_path, :style => "padding: 10px" %> 88 | <%= tab.dashboard 'Dashboard', dashboard_path %> 89 | <% end %> 90 | ``` 91 | 92 | renders 93 | 94 | ``` 95 | 100 | ``` 101 | 102 | See `TabsOnRails::Tabs::TabsBuilder#tab_for` for more details. 103 | 104 | ### Customizing `open_tabs` and `close_tabs` 105 | 106 | The `open_tabs` and the `close_tabs` methods can be customized with the `:open_tabs` and `:close_tabs` option. 107 | 108 | ```ruby 109 | <%= tabs_tag open_tabs: { id: 'tabs', class: 'cool' } do |tab| %> 110 | <%= tab.home 'Homepage', root_path %> 111 | <%= tab.dashboard 'Dashboard', dashboard_path %> 112 | <%= tab.account 'Account', account_path %> 113 | <% end %> 114 | ``` 115 | 116 | renders 117 | 118 | ```html 119 | 124 | ``` 125 | 126 | Further customizations require a custom `Builder` (see below). 127 | 128 | 129 | ## Restricting `set_tab` scope 130 | 131 | The `set_tab` method understands all options you are used to pass to a Rails controller filter. 132 | In fact, behind the scenes this method uses a `before_filter` to store the tab in the `@tab_stack` variable. 133 | 134 | Taking advantage of Rails filter options, you can restrict a tab to a selected group of actions in the same controller. 135 | 136 | ```ruby 137 | class PostsController < ApplicationController 138 | set_tab :admin 139 | set_tab :posts, :only => %w(index show) 140 | end 141 | 142 | class ApplicationController < ActionController::Base 143 | set_tab :admin, :if => :admin_controller? 144 | 145 | def admin_controller? 146 | self.class.name =~ /^Admin(::|Controller)/ 147 | end 148 | end 149 | ``` 150 | 151 | ## Using Namespaces to create Multiple Tabs 152 | 153 | Namespaces enable you to create and manage tabs in parallels. The best way to demonstrate namespace usage is with an example. 154 | 155 | Let's assume your application provides a first level navigation menu with 3 elements: `:home`, `:dashboard`, `:projects`. The relationship between your tabs and your controllers is 1:1 so you should end up with the following source code. 156 | 157 | ```ruby 158 | class HomeController 159 | set_tab :home 160 | end 161 | 162 | class DashboardController 163 | set_tab :dashboard 164 | end 165 | 166 | class ProjectsController 167 | set_tab :projects 168 | 169 | def first; end 170 | def second; end 171 | def third; end 172 | end 173 | ``` 174 | 175 | The project controller contains 3 actions and you might want to create a second-level navigation menu. This menu should reflect the navigation status of the user in the project page. 176 | 177 | Without namespaces, you wouldn't be able to accomplish this task because you already set the current tab value to :projects. You need to create a parallel navigation menu and uniquely identify it with a custom namespace. 178 | Let's call it :navigation. 179 | 180 | ```ruby 181 | class ProjectsController 182 | set_tab :projects 183 | 184 | # Create an other tab navigation level 185 | set_tab :first, :navigation, :only => %w(first) 186 | set_tab :second, :navigation, :only => %w(second) 187 | set_tab :third, :navigation, :only => %w(third) 188 | 189 | def first; end 190 | def second; end 191 | def third; end 192 | end 193 | ``` 194 | 195 | Voilà! That's all you need to do. And you can create an unlimited number of namespaces as long as you use an unique name to identify them. 196 | 197 | The default namespace is called `:default`. Passing `:default` as name is the same as don't using any namespace at all. The following lines are equivalent. 198 | 199 | ```ruby 200 | set_tab :projects 201 | set_tab :projects, :default 202 | ``` 203 | 204 | ### Rendering Tabs with Namespaces 205 | 206 | To switch namespace in your template, just pass the `:namespace` option to the `tabs_tag` helper method. 207 | 208 | ```ruby 209 | <%= tabs_tag do |tab| %> 210 | <%= tab.home 'Homepage', root_path %> 211 | <%= tab.dashboard 'Dashboard', dashboard_path %> 212 | <%= tab.projects 'Projects', projects_path %> 213 | <% end %> 214 | 215 | <%= tabs_tag :namespace => :navigation do |tab| %> 216 | <%= tab.first 'First', project_first_path %> 217 | <%= tab.second 'Second', project_second_path %> 218 | <%= tab.third 'Account', project_third_path %> 219 | <% end %> 220 | ``` 221 | 222 | ### Namespace scope 223 | 224 | As a bonus feature, the namespace needs to be unique within current request scope, not necessarily across the entire application. 225 | 226 | Back to the previous example, you can reuse the same namespace in the other controllers. In this way, you can reuse your templates as well. 227 | 228 | ```ruby 229 | class HomeController 230 | set_tab :home 231 | end 232 | 233 | class DashboardController 234 | set_tab :dashboard 235 | 236 | set_tab :index, :navigation, :only => %w(index) 237 | set_tab :common, :navigation, :only => %w(foo bar) 238 | 239 | # ... 240 | end 241 | 242 | class ProjectsController 243 | set_tab :projects 244 | 245 | set_tab :first, :navigation, :only => %w(first) 246 | set_tab :second, :navigation, :only => %w(second) 247 | set_tab :third, :navigation, :only => %w(third) 248 | 249 | # ... 250 | end 251 | ``` 252 | 253 | 254 | ## Tab Builders 255 | 256 | The `Builder` is responsible for creating the tabs HTML code. This library is bundled with two `Builders`: 257 | 258 | - `Tabs::Builder`: this is the abstract interface for any custom builder. 259 | - `Tabs::TabsBuilder`: this is the default builder. 260 | 261 | ### Understanding the Builder 262 | 263 | Builders act as formatters. A Builder encapsulates all the logic behind the tab creation including the code required to toggle tabs status. 264 | 265 | When the `tabs_tag` helper is called, it creates a new `Tabs` instance with selected Builder. If you don't provide a custom builder, then `Tabs::TabsBuilder` is used by default. 266 | 267 | ### Creating a custom Builder 268 | 269 | All builders must extend the base `Tabs::Builder` class and implement at least the `tab_for` method. 270 | Additional overridable methods include: 271 | 272 | - `open_tabs`: the method called before the tab set 273 | - `close_tabs`: the method called after the tab set 274 | - `tab_for`: the method called to create a single tab item 275 | 276 | The following example creates a custom tab builder called `MenuTabBuilder`. 277 | 278 | ```ruby 279 | class MenuTabBuilder < TabsOnRails::Tabs::Builder 280 | def open_tabs(options = {}) 281 | @context.tag("ul", options, open = true) 282 | end 283 | 284 | def close_tabs(options = {}) 285 | "".html_safe 286 | end 287 | 288 | def tab_for(tab, name, options, item_options = {}) 289 | item_options[:class] = (current_tab?(tab) ? 'active' : '') 290 | @context.content_tag(:li, item_options) do 291 | @context.link_to(name, options) 292 | end 293 | end 294 | end 295 | ``` 296 | 297 | ### Using a custom Builder 298 | 299 | In your view, simply pass the builder class to the `tabs_tag` method. 300 | 301 | ```ruby 302 | <%= tabs_tag(:builder => MenuTabBuilder) do |tab| %> 303 | <%= tab.home 'Homepage', root_path %> 304 | <%= tab.dashboard, 'Dashboard', dashboard_path %> 305 | <%= tab.account 'Account', account_path, style: 'float: right;' %> 306 | <% end %> 307 | ``` 308 | 309 | renders 310 | 311 | ```html 312 | 317 | ``` 318 | 319 | 320 | ## Credits 321 | 322 | TabsOnRails was created and is maintained by [Simone Carletti](https://simonecarletti.com/). Many improvements and bugfixes were contributed by the [open source community](https://github.com/weppos/tabs_on_rails/graphs/contributors). 323 | 324 | 325 | ## Contributing 326 | 327 | Direct questions and discussions to [Stack Overflow](http://stackoverflow.com/questions/tagged/tabs-on-rails). 328 | 329 | [Pull requests](https://github.com/weppos/tabs_on_rails) are very welcome! Please include tests for every patch, and create a topic branch for every separate change you make. 330 | 331 | Report issues or feature requests to [GitHub Issues](https://github.com/weppos/tabs_on_rails/issues). 332 | 333 | 334 | ## More Information 335 | 336 | - [Homepage](https://simonecarletti.com/code/tabs-on-rails) 337 | - [RubyGems](https://rubygems.org/gems/tabs_on_rails) 338 | - [Issues](https://github.com/weppos/tabs_on_rails/issues) 339 | 340 | 341 | ## License 342 | 343 | Copyright (c) 2009-2017 Simone Carletti. This is Free Software distributed under the MIT license. 344 | --------------------------------------------------------------------------------