├── VERSION ├── .rspec ├── spec ├── spec_helper.rb └── lib │ ├── mmmenu_level_spec.rb │ └── mmmenu_spec.rb ├── lib ├── mmmenu │ ├── version.rb │ ├── engine.rb │ └── level.rb ├── generators │ ├── templates │ │ ├── views │ │ │ └── mmmenu │ │ │ │ ├── _level_1.erb │ │ │ │ ├── _level_2.erb │ │ │ │ ├── _item.erb │ │ │ │ └── _current_item.erb │ │ └── helpers │ │ │ └── mmmenu_helper.rb │ └── mmmenu │ │ └── mmmenu_generator.rb └── mmmenu.rb ├── Gemfile ├── .gitignore ├── Rakefile ├── Gemfile.lock ├── mmmenu.gemspec └── README.markdown /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.2 -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/mmmenu/version.rb: -------------------------------------------------------------------------------- 1 | class Mmmenu 2 | VERSION = '0.1' 3 | end -------------------------------------------------------------------------------- /lib/generators/templates/views/mmmenu/_level_1.erb: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/generators/templates/views/mmmenu/_level_2.erb: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/generators/templates/views/mmmenu/_item.erb: -------------------------------------------------------------------------------- 1 |
  • <%= link_to text, link, options[:html_options] %>
  • 2 | -------------------------------------------------------------------------------- /lib/generators/templates/views/mmmenu/_current_item.erb: -------------------------------------------------------------------------------- 1 |
  • <%= link_to text, link, options[:html_options].merge({:class => "current"}) %>
  • 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | group :development do 4 | gem "bundler" 5 | gem "rspec" 6 | gem "jeweler" 7 | #gem "rcov", ">= 0" 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov generated 2 | coverage 3 | 4 | # rdoc generated 5 | rdoc 6 | 7 | # yard generated 8 | doc 9 | .yardoc 10 | 11 | # bundler 12 | .bundle 13 | 14 | # jeweler generated 15 | pkg 16 | -------------------------------------------------------------------------------- /lib/mmmenu/engine.rb: -------------------------------------------------------------------------------- 1 | class Mmmenu::Engine < Rails::Engine 2 | 3 | paths["app/helpers"] << File.expand_path("../../generators/templates/helpers", __FILE__) 4 | paths["app/views"] << File.expand_path("../../generators/templates/views", __FILE__) 5 | 6 | end if defined?(Rails::Engine) 7 | 8 | ActionController::Base.class_eval do 9 | 10 | private 11 | 12 | def mmmenu(&block) 13 | @menu = Mmmenu.new(:request => request, &block) 14 | end 15 | 16 | end if defined?(ActionController::Base) 17 | -------------------------------------------------------------------------------- /lib/mmmenu/level.rb: -------------------------------------------------------------------------------- 1 | class Mmmenu 2 | 3 | class Level 4 | 5 | def initialize(&block) 6 | @items = [] 7 | yield(self) 8 | end 9 | 10 | def add(title, href, options={}, &block) 11 | children = {} 12 | if block_given? # which means current item has children 13 | children = { :children => self.class.new(&block).to_a } 14 | end 15 | @items << { :title => title, :href => href }.merge(options).merge(children) 16 | end 17 | 18 | def to_a 19 | @items 20 | end 21 | 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/mmmenu/mmmenu_generator.rb: -------------------------------------------------------------------------------- 1 | class MmmenuGenerator < Rails::Generators::Base 2 | 3 | source_root File.expand_path("../../templates", __FILE__) 4 | 5 | def create_helper 6 | copy_file "helpers/mmmenu_helper.rb", "app/helpers/mmmenu_helper.rb" 7 | copy_file "views/mmmenu/_item.erb", "app/views/mmmenu/_item.erb" 8 | copy_file "views/mmmenu/_current_item.erb", "app/views/mmmenu/_current_item.erb" 9 | copy_file "views/mmmenu/_level_1.erb", "app/views/mmmenu/_level_1.erb" 10 | copy_file "views/mmmenu/_level_2.erb", "app/views/mmmenu/_level_2.erb" 11 | end 12 | 13 | 14 | end 15 | -------------------------------------------------------------------------------- /spec/lib/mmmenu_level_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../../lib/mmmenu/level') 2 | 3 | describe Mmmenu::Level do 4 | 5 | it "builds a menu level hash representation" do 6 | @menu_level = Mmmenu::Level.new do |l1| 7 | l1.add "Title1", "/path1" 8 | l1.add "Title2", "/path2" do |l2| 9 | l2.add "SubTitle", "/path2/subpath" 10 | end 11 | end 12 | 13 | expect(@menu_level.to_a).to eq [ 14 | {:title => "Title1", :href => "/path1"}, 15 | {:title => "Title2", :href => "/path2", :children => [ 16 | { :title => "SubTitle", :href => "/path2/subpath" } 17 | ]} 18 | ] 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /lib/generators/templates/helpers/mmmenu_helper.rb: -------------------------------------------------------------------------------- 1 | module MmmenuHelper 2 | 3 | def build_mmmenu(menu, options = {}) 4 | return nil unless menu 5 | options = { templates_path: 'mmmenu' }.merge(options) 6 | templates_path = options[:templates_path] 7 | menu.item_markup(1) do |link, text, options| 8 | render(:partial => "#{templates_path}/item", :locals => { :link => link, :text => text, :options => options }) 9 | end 10 | menu.current_item_markup(1) do |link, text, options| 11 | render(:partial => "#{templates_path}/current_item", :locals => { :link => link, :text => text, :options => options }) 12 | end 13 | menu.level_markup(1) { |m| render(:partial => "#{templates_path}/level_1", :locals => { :menu => m }) } 14 | menu.level_markup(2) { |m| render(:partial => "#{templates_path}/level_2", :locals => { :menu => m }) } 15 | menu.build.html_safe 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | require 'rspec/core/rake_task' 6 | 7 | begin 8 | Bundler.setup(:default, :development) 9 | rescue Bundler::BundlerError => e 10 | $stderr.puts e.message 11 | $stderr.puts "Run `bundle install` to install missing gems" 12 | exit e.status_code 13 | end 14 | require 'rake' 15 | 16 | require 'jeweler' 17 | Jeweler::Tasks.new do |gem| 18 | # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options 19 | gem.name = "mmmenu" 20 | gem.homepage = "http://github.com/snitko/mmmenu" 21 | gem.license = "MIT" 22 | gem.summary = %Q{Flexible menu generator for Rails.} 23 | gem.description = %Q{Defines multilevel menu structures, uses custom html templates to render them.} 24 | gem.email = "roman.snitko@gmail.com" 25 | gem.authors = ["Roman Snitko"] 26 | # dependencies defined in Gemfile 27 | end 28 | Jeweler::RubygemsDotOrgTasks.new 29 | 30 | RSpec::Core::RakeTask.new(:default) 31 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | addressable (2.3.6) 5 | builder (3.2.2) 6 | descendants_tracker (0.0.4) 7 | thread_safe (~> 0.3, >= 0.3.1) 8 | diff-lcs (1.2.5) 9 | faraday (0.9.0) 10 | multipart-post (>= 1.2, < 3) 11 | git (1.2.7) 12 | github_api (0.11.3) 13 | addressable (~> 2.3) 14 | descendants_tracker (~> 0.0.1) 15 | faraday (~> 0.8, < 0.10) 16 | hashie (>= 1.2) 17 | multi_json (>= 1.7.5, < 2.0) 18 | nokogiri (~> 1.6.0) 19 | oauth2 20 | hashie (3.1.0) 21 | highline (1.6.21) 22 | jeweler (2.0.1) 23 | builder 24 | bundler (>= 1.0) 25 | git (>= 1.2.5) 26 | github_api 27 | highline (>= 1.6.15) 28 | nokogiri (>= 1.5.10) 29 | rake 30 | rdoc 31 | json (1.8.1) 32 | jwt (1.0.0) 33 | mini_portile (0.6.0) 34 | multi_json (1.10.1) 35 | multi_xml (0.5.5) 36 | multipart-post (2.0.0) 37 | nokogiri (1.6.2.1) 38 | mini_portile (= 0.6.0) 39 | oauth2 (0.9.4) 40 | faraday (>= 0.8, < 0.10) 41 | jwt (~> 1.0) 42 | multi_json (~> 1.3) 43 | multi_xml (~> 0.5) 44 | rack (~> 1.2) 45 | rack (1.5.2) 46 | rake (10.3.2) 47 | rdoc (4.1.1) 48 | json (~> 1.4) 49 | rspec (3.0.0) 50 | rspec-core (~> 3.0.0) 51 | rspec-expectations (~> 3.0.0) 52 | rspec-mocks (~> 3.0.0) 53 | rspec-core (3.0.2) 54 | rspec-support (~> 3.0.0) 55 | rspec-expectations (3.0.2) 56 | diff-lcs (>= 1.2.0, < 2.0) 57 | rspec-support (~> 3.0.0) 58 | rspec-mocks (3.0.2) 59 | rspec-support (~> 3.0.0) 60 | rspec-support (3.0.2) 61 | thread_safe (0.3.4) 62 | 63 | PLATFORMS 64 | ruby 65 | 66 | DEPENDENCIES 67 | bundler 68 | jeweler 69 | rspec 70 | -------------------------------------------------------------------------------- /mmmenu.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | # stub: mmmenu 0.5.2 ruby lib 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "mmmenu" 9 | s.version = "0.5.2" 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 12 | s.require_paths = ["lib"] 13 | s.authors = ["Roman Snitko"] 14 | s.date = "2014-07-07" 15 | s.description = "Defines multilevel menu structures, uses custom html templates to render them." 16 | s.email = "roman.snitko@gmail.com" 17 | s.extra_rdoc_files = [ 18 | "README.markdown" 19 | ] 20 | s.files = [ 21 | ".rspec", 22 | "Gemfile", 23 | "Gemfile.lock", 24 | "README.markdown", 25 | "Rakefile", 26 | "VERSION", 27 | "lib/generators/mmmenu/mmmenu_generator.rb", 28 | "lib/generators/templates/helpers/mmmenu_helper.rb", 29 | "lib/generators/templates/views/mmmenu/_current_item.erb", 30 | "lib/generators/templates/views/mmmenu/_item.erb", 31 | "lib/generators/templates/views/mmmenu/_level_1.erb", 32 | "lib/generators/templates/views/mmmenu/_level_2.erb", 33 | "lib/mmmenu.rb", 34 | "lib/mmmenu/engine.rb", 35 | "lib/mmmenu/level.rb", 36 | "lib/mmmenu/version.rb", 37 | "mmmenu.gemspec", 38 | "spec/lib/mmmenu_level_spec.rb", 39 | "spec/lib/mmmenu_spec.rb", 40 | "spec/spec_helper.rb" 41 | ] 42 | s.homepage = "http://github.com/snitko/mmmenu" 43 | s.licenses = ["MIT"] 44 | s.rubygems_version = "2.2.2" 45 | s.summary = "Flexible menu generator for Rails." 46 | 47 | if s.respond_to? :specification_version then 48 | s.specification_version = 4 49 | 50 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 51 | s.add_development_dependency(%q, [">= 0"]) 52 | s.add_development_dependency(%q, [">= 0"]) 53 | s.add_development_dependency(%q, [">= 0"]) 54 | else 55 | s.add_dependency(%q, [">= 0"]) 56 | s.add_dependency(%q, [">= 0"]) 57 | s.add_dependency(%q, [">= 0"]) 58 | end 59 | else 60 | s.add_dependency(%q, [">= 0"]) 61 | s.add_dependency(%q, [">= 0"]) 62 | s.add_dependency(%q, [">= 0"]) 63 | end 64 | end 65 | 66 | -------------------------------------------------------------------------------- /spec/lib/mmmenu_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../../lib/mmmenu/level') 2 | require File.expand_path(File.dirname(__FILE__) + '/../../lib/mmmenu') 3 | 4 | describe Mmmenu do 5 | 6 | before(:all) do 7 | @items = [ 8 | { :name => :numbers, :title => 'Item1', :children => [ 9 | { :title => 'Create', :href => '/items1/new', :paths => ['/items1/new'] }, 10 | { :title => 'Index', :href => '/items1/' }, 11 | { :title => 'Print', :href => '/items1/print' } 12 | ]}, 13 | { :title => 'Item2', :href => '/items2' }, 14 | { :title => 'Item3', :href => '/items3' }, 15 | { :title => 'Item4', :href => '/items4' } 16 | ] 17 | end 18 | 19 | before(:each) do 20 | @request = double('request') 21 | allow(@request).to receive(:path).and_return('/items1/new') 22 | allow(@request).to receive(:method).and_return('get') 23 | allow(@request).to receive(:params).and_return({}) 24 | @menu = Mmmenu.new(:items => @items, :request => @request ) 25 | end 26 | 27 | it "renders one level" do 28 | set_menu_markup 29 | @menu.item_markup(2) do |link, text, options| 30 | " #{text}: #{link}\n" 31 | end 32 | @menu.current_item_markup(2) do |link, text, options| 33 | " #{text}: #{link} current\n" 34 | end 35 | @menu.level_markup(1) { |menu| menu } 36 | expect(@menu.build).to eq < 'item1', :href => '/item1', :paths => [['/item', 'post']] }, 52 | { :title => 'item2', :href => '/item2', :paths => [['/item', 'get']] } 53 | ] 54 | request = double('request') 55 | allow(request).to receive(:path).once.and_return('/item') 56 | allow(request).to receive(:method).once.and_return('get') 57 | allow(request).to receive(:params).once.and_return({}) 58 | @menu = Mmmenu.new(:items => items, :request => request ) 59 | set_menu_markup 60 | expect(@menu.build).to eq < 'item1', :href => '/item1', :paths => [['/item1', 'get', {:param => 1}]] }, 70 | { :title => 'item2', :href => '/item1', :paths => [['/item1', 'get', {:param => 2}]] }, 71 | { :title => 'item3', :href => '/item1', :paths => [['/item1', 'get', {:param => nil}]] } 72 | ] 73 | request = double('request') 74 | allow(request).to receive(:path).once.and_return('/item1') 75 | allow(request).to receive(:params).once.and_return({"param" => "1"}) 76 | allow(request).to receive(:method).once.and_return('get') 77 | @menu = Mmmenu.new(:items => items, :request => request ) 78 | set_menu_markup 79 | expect(@menu.build).to eq < 'item1', :href => '/item1' }, 89 | { :title => 'item2', :href => '/item2' }, 90 | { :title => 'item3', :href => '/item3' } 91 | ] 92 | request = double('request') 93 | allow(request).to receive(:path).once.and_return('/item1') 94 | allow(request).to receive(:method).once.and_return('get') 95 | allow(request).to receive(:params).once 96 | @menu = Mmmenu.new(:items => items, :request => request ) 97 | @menu.current_item = "/item2" 98 | set_menu_markup 99 | 100 | expect(@menu.build).to eq < @request) do |m| 110 | m.add 'Item1', '/items1', :match_subpaths => true 111 | m.add 'Item2', '/items2' do |subm| 112 | subm.add 'New', '/item2/new' 113 | subm.add 'Edit', '/item2/edit' 114 | end 115 | end 116 | set_menu_markup 117 | 118 | expect(@menu.build).to eq < @request) do |m| 130 | m.add 'Item1', '/items1', :match_subpaths => true 131 | m.add 'Item2', '/items2' do |subm| 132 | subm.add 'New', '/item2/new' 133 | subm.add 'Edit', '/item2/edit' 134 | end 135 | end 136 | set_menu_markup 137 | 138 | expect(@menu.build).to eq <"Create", :href=>"/items1/new", :paths=>["/items1/new"], :html_options=>{}}) 151 | end 152 | 153 | def set_menu_markup(level=1) 154 | @menu.item_markup(level) do |link, text, options| 155 | "#{text}: #{link}\n" 156 | end 157 | @menu.current_item_markup(level) do |link, text, options| 158 | "#{text}: #{link} current\n" 159 | end 160 | end 161 | 162 | end 163 | -------------------------------------------------------------------------------- /lib/mmmenu.rb: -------------------------------------------------------------------------------- 1 | require 'mmmenu/level' 2 | require 'mmmenu/engine' 3 | 4 | class Mmmenu 5 | 6 | attr_accessor :current_item, :options 7 | 8 | def initialize(options, &block) 9 | @items = options[:items] || Mmmenu::Level.new(&block).to_a 10 | @current_path = options[:request].path.chomp('/') 11 | @request_params = options[:request].params 12 | @request_type = options[:request].method.to_s.downcase 13 | 14 | @item_markup = [] 15 | @current_item_markup = [] 16 | @level_markup = [] 17 | end 18 | 19 | # The following two methods define 20 | # the markup for each menu item on the current and 21 | # lower levels (unless lower levels are not redefined later). 22 | # Example: 23 | # @menu.item_markup(0, options) do |link, text, options| 24 | # "#{text}" 25 | # end 26 | def item_markup(level, options={}, &block) 27 | level -= 1 28 | @item_markup[level] = { :block => block, :options => options } 29 | end 30 | def current_item_markup(level, options={}, &block) 31 | level -= 1 32 | @current_item_markup[level] = { :block => block, :options => options } 33 | end 34 | 35 | # Defines the markup wrapper for the current menu level and 36 | # lower menu levels (unless lower levels are not redefined later). 37 | # Example: 38 | # @menu.level_markup(0) { |level_content| "
    #{level_content}
    " } 39 | def level_markup(level=1, &block) 40 | level -= 1 41 | @level_markup[level] = block 42 | end 43 | 44 | def build 45 | result = build_level[:output] 46 | @current_item = @deepest_current_item 47 | result 48 | end 49 | 50 | private 51 | 52 | def build_level(items=@items, level=0) 53 | 54 | item_markup = build_item_markup(level) 55 | level_markup = build_level_markup(level) 56 | 57 | # Parsing of a single menu level happens here 58 | output = '' 59 | has_current_item = false 60 | 61 | raise("Mmmenu object #{self} is empty, no items defined!") if items.nil? or items.empty? 62 | items.each do |item| 63 | 64 | item[:html_options] = {} unless item[:html_options] 65 | child_menu = build_level(item[:children], level+1) if item[:children] 66 | child_output = child_menu[:output] if child_menu 67 | 68 | ############################################################# 69 | # Here's where we check if the current item is a current item 70 | # and we should use a special markup for it 71 | ############################################################# 72 | if ( 73 | item[:href] == current_item or 74 | ( current_item.nil? && ( 75 | item_paths_match?(item) or 76 | (child_menu and child_menu[:has_current_item]) or 77 | item_href_match?(item) 78 | )) 79 | ) && !has_current_item 80 | 81 | then 82 | unless item[:children] 83 | @deepest_current_item = item 84 | end 85 | has_current_item = true 86 | item_output = item_markup[:current][:block].call(item[:href], item[:title], item_markup[:current][:options].merge(html_options: {}).merge(item.reject { |k,v| [:href, :title].include?(k) })) 87 | else 88 | item_output = item_markup[:basic][:block].call(item[:href], item[:title], item_markup[:basic][:options].merge(html_options: {}).merge(item.reject { |k,v| [:href, :title].include?(k) })) 89 | end 90 | ############################################################# 91 | 92 | output += "#{item_output}#{child_output}" 93 | end 94 | 95 | output = level_markup.call(output) 96 | { :has_current_item => has_current_item, :output => output } 97 | 98 | end 99 | 100 | 101 | # Matches menu item against :paths option 102 | def item_paths_match?(item) 103 | if item[:paths] 104 | 105 | item[:paths].each do |path| 106 | if path.kind_of?(Array) 107 | # IF path matches perfectly 108 | request_type_option = path[1] || "" 109 | if ((@current_path == path[0].chomp('/') and @request_type == request_type_option.downcase) or 110 | # OR IF * wildcard is used and path matches 111 | (path[0] =~ /\*$/ and @current_path =~ /^#{path[0].chomp('*')}(.+)?$/)) and 112 | # all listed request params match 113 | params_match?(path) 114 | return true 115 | end 116 | else 117 | return true if @current_path == path 118 | end 119 | end 120 | 121 | end 122 | return false 123 | end 124 | 125 | # Matches menu item against the actual path it's pointing to. 126 | # Is only applied when :path option is not present. 127 | def item_href_match?(item) 128 | if item[:href] 129 | item_href = item[:href].chomp('/') 130 | if (@current_path == item_href) or # IF path matches perfectly 131 | (item[:match_subpaths] and @current_path =~ /^#{item_href}(\/.+)?$/) # OR IF :match_subpaths options is used and path matches 132 | return true 133 | end 134 | end unless item[:paths] 135 | return false 136 | end 137 | 138 | def build_item_markup(level) 139 | if @item_markup[level] 140 | { :basic => @item_markup[level], :current => @current_item_markup[level] } 141 | else 142 | unless @item_markup.empty? 143 | { :basic => @item_markup.last, :current => @current_item_markup.last } 144 | else 145 | { :basic => {:block => lambda { |link,text,options| "#{text} #{link} #{options}\n" }, :options => {} }, :current => { :block => lambda { |link,text,options| "#{text} #{link} #{options} current\n" }, :options => {} } } 146 | end 147 | end 148 | end 149 | 150 | def build_level_markup(level) 151 | if @level_markup[level] 152 | @level_markup[level] 153 | else 154 | if @level_markup.empty? 155 | lambda { |menu| menu } 156 | else 157 | @level_markup.last 158 | end 159 | end 160 | end 161 | 162 | def params_match?(path) 163 | path[2].each do |k,v| 164 | return false unless (@request_params[k.to_s].nil? and v.nil?) or @request_params[k.to_s] == v.to_s 165 | end if path[2] 166 | true 167 | end 168 | 169 | end 170 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Mmmenu 2 | ====== 3 | 4 | *Flexible menu generator for rails* 5 | 6 | Why another menu plugin? 7 | ------------------------ 8 | All menu plugins I've seen have HTML markup hardcoded into them. 9 | *Mmmenu* offers you a chance to define your own markup, along with 10 | a nice DSL to describe multi-level menu structures. 11 | 12 | INSTALLATION 13 | ------------ 14 | 15 | 1. gem install mmmenu 16 | 2. rails generate mmmenu (optional) 17 | 18 | Basic Usage 19 | --------------- 20 | 21 | Imagine you put this into your controller: 22 | 23 | mmmenu do |l1| 24 | 25 | l1.add "Articles", "/articles" do |l2| 26 | l2.add "Create article", new_article_path 27 | l2.add "Articles authors", "/articles/authors", :match_subpaths => true 28 | end 29 | l1.add "Item2", "/path2" 30 | l1.add "Item3", "/path3" 31 | l1.add "Item4", "/path4" 32 | 33 | end 34 | 35 | As you can see, we specify the paths, so our menu does not depend on the routes. 36 | `#mmmenu` method automatically puts your menu into @menu instance variable. If you wish to use another variable, 37 | you may use a more explicit syntax: 38 | 39 | @my_menu = Mmmenu.new(:request => request) { |l1| ... } 40 | 41 | Now let's see what happens in the views: 42 | 43 | <%= build_mmmenu(@menu) %> 44 | 45 | And that's it, you get your menu rendered. 46 | 47 | Customizing Views 48 | ------------------------ 49 | Now, like I promised, the html-markup is totally 50 | configurable. 51 | 52 | Run `rails generate mmmenu`, you'll get your app/helpers/mmmenu_helper.rb file and a bunch of templates copied out of the plugin views/mmmenu directory into app/views/mmmenu directory, thus replacing the plugin default files. Here's what those template files are and what they mean: 53 | 54 | `_current_item.erb` is responsible for the current item in the menu 55 | `_item.erb` is responsible for the non-current items 56 | `level_1.erb` is a wrapper for menu level 1 57 | `level_2.erb` is a wrapper for menu level 2 (submenu) 58 | 59 | You can also has various templates for various menus on your page. Simply, provide a :templates_path option to #build_mmmenu helper like this: 60 | 61 | <%= build_mmmenu(@menu, templates_path: 'mmmenu/my_custom_menu') %> 62 | 63 | Then you can put all the same files mentioned above in this directory and change them. This is useful when you have various types of menus 64 | requiring different html-markup. If you wish to customize deeper levels of menus and items in them, you should take a look at the generated 65 | `mmmenu_helper.rb` file. 66 | 67 | Customizing the Helper 68 | ---------------------------- 69 | 70 | Let's take a look at what's inside this helper: 71 | 72 | def build_mmmenu(menu, options = {}) 73 | return nil unless menu 74 | options = {templates_path: 'mmmenu' }.merge(options) 75 | templates_path = options[:templates_path] 76 | menu.item_markup(1) do |link, text, options| 77 | render(:partial => "#{templates_path}/item", :locals => { :link => link, :text => text, :options => options }) 78 | end 79 | menu.current_item_markup(1) do |link, text, options| 80 | render(:partial => "#{templates_path}/current_item", :locals => { :link => link, :text => text, :options => options }) 81 | end 82 | menu.level_markup(1) { |m| render(:partial => "#{templates_path}/level_1", :locals => { :menu => m }) } 83 | menu.level_markup(2) { |m| render(:partial => "#{templates_path}/level_2", :locals => { :menu => m }) } 84 | menu.build.html_safe 85 | end 86 | 87 | You can see now, that `#item_markup` method defines the html markup for menu item, 88 | and `#level_markup` does the same for menu level wrapper. The first argument is the menu level. 89 | You may define as much levels as you want, but you don't need to define markup for each level: the deepest level of the markup 90 | defined will be used for all of the deeper levels. 91 | 92 | By default, build_mmmenu helper defines two partial templates for level 1 of the menu. 93 | It does so by calling two methods on a Mmmenu object: 94 | #current_item_markup defines markup for the current menu item 95 | #item_markup defines markup for all other menu items 96 | Both methods accept an optional second argument - a hash of html_options: 97 | 98 | menu.item_markup(1, :class => "mmmenu") 99 | 100 | which is later used in the templates like this: 101 | 102 |
  • <%= link_to text, link, options %>
  • 103 | 104 | Note, that this is an example from a default template and options hash may not be present in the customized template. 105 | 106 | Disclaimer: if you call Mmmenu#item_markup for a certain level, you MUST call Mmmenu#current_item_markup 107 | for the same level. 108 | 109 | Most of the time, you will want to customize your views, not the helper, so you may as well delete it from your application/helpers dir. 110 | 111 | 112 | Finally, let's take a closer look at some of the options and what they mean. 113 | --------------------------------------------------------------------- 114 | ##### Current item 115 | Mmmenu automatically marks each current menu_item with the markup, that you provide in `#current_item_markup` method's block. The item is considered current not only if the paths 116 | match, but also if one of the children of the item is marked as current. You may as well set the current item manually like that: 117 | 118 | @menu.current_item = '/articles' 119 | 120 | In this case `'/articles'` would match against the second argument that you pass to the `#add` method, which is the path the menu itme points to. 121 | Note that unless the item with such path is not present in the menu, then no item will be makred as current. It is a useful technique when you want 122 | to prevent the item from being current in the controller. For example: 123 | 124 | @menu.current_item = '/search-results-pertending-to-be-a-list' if search_query_empty? 125 | 126 | ##### Paths 127 | For each menu item you may specify a number of paths, that should match for the item to be active. 128 | Unless you provide the `:path` option for `Mmmenu#add`, the second argument is used as the matching path. 129 | If you'd like to specify paths explicitly, do something like this: 130 | 131 | `l1.add "Articles" articles_path, :paths => [[new_article_path, 'get'], [articles_path, 'post'], [articles_path, 'get']]` 132 | 133 | This way, the menu item will appear active even when you're on the /articles/new page. 134 | There's also a third array element, which must be hash. In it may list request_params that should match, for example: 135 | 136 | `l1.add "Articles" articles_path, :paths => [[articles_path, 'get', {:filter => 'published'}]]` 137 | 138 | That way, only a request to "/articles?filter=published" will make this menu item active. 139 | Of course it doesn't matter, if the request contains some other params. But you can make sure it doesn't by saying something like `{:personal => nil}` 140 | 141 | Alernatively, you can do this: 142 | 143 | l1.add "Articles" articles_path, :match_subpaths => true 144 | 145 | Or you may use wildcards: 146 | 147 | l1.add "Articles" articles_path, :paths => [["/articles/*"]] 148 | 149 | That's much easier usually. 150 | --------------------------------------------------------------------------------