class MenuBlock attr_reader :items attr_reader :context attr_reader :html_options def initialize(view_context, html_options = {}) @context = view_context @html_options = html_options @items = [] end # Adds a menu item to the menu block # # ==== Options # * default: false - Whether or not this item is default if nothing else is active. # * match_params: {} - Pass a hash to compare with params. Leave blank for defaults matchers. # To use: {action: [:new, :create], id: '12345'}. # # ==== Examples # For a regular default menu item # # = menu.add_item('Profile', default: true) { edit_person_path(@person, page: 'profile') } # # For a subitem you can so something like this # .subitems # = menu.add_item('New Custom Profile', match_params: {action: [:new, :create]}) { new_person_custom_profile_path(@person) } # def add_item(name, default: false, match_params: {}, &route_block) items << MenuItem.new(context, name, default: default, match_params: match_params, &route_block) placeholder(items.length - 1) end def render(&block) # Grab the HTML from the block so that custom HTML can be injected into the menu html = context.capture(self, &block) items.each_with_index do |item, i| opts = html_options.dup || {} if item.active? || (items.none?(&:active?) && item.default?) opts[:class] = [html_options[:class], 'active'].compact.join(' ') end html[placeholder(i)] = context.link_to(item.name, item.route, opts) end html end private def placeholder(number) # Placeholder for delayed rendering. We need this so that we can work # with all of the menu items so that by the time we render the first one, # which could be the default, we know whether or not any others are active. "[[menu_block:#{number}]]" end class MenuItem attr_reader :name attr_reader :route_block attr_reader :default attr_reader :match_params alias :default? :default def initialize(context, name, default: false, match_params: {}, &route_block) @context = context @name = name @route_block = route_block @default = default @match_params = match_params end def route @route ||= route_block.call end def active? matches = [] # Set default match_params. For example: This is the default matcher for :controller if :controller isn't specified match_params[:controller] ||= recognized_route[:controller] # Make sure everything in match_params is contained in params. match_params.each do |key,val| # This will handle arrays, strings, numbers, etc. params should always return a string or nil. matches << Array(val).compact.map(&:to_s).include?(params[key]) end # Loop through query parameters to make sure they match ::Rack::Utils.parse_query(URI.parse(route).query).each do |param, val| matches << (params[param] == val) end matches.all? end private def request @context.request end def params @context.params end def recognized_route @recognized_route ||= ( recognizable_path = without_script_name { route_block.call } Rails.application.routes.recognize_path(recognizable_path) ) end # The router won't recognize routes with a script_name # So we need to temporarily set the script_name to nil # so we can generate a path the router will like. def without_script_name(&block) script_name = request.script_name request.script_name = nil val = block.call request.script_name = script_name val end end end