phlexi-menu 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- phlexi-menu (0.1.0)
4
+ phlexi-menu (0.2.0)
5
5
  phlex (~> 1.11)
6
6
  phlexi-field
7
7
  zeitwerk
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "phlex"
4
+
5
+ module Phlexi
6
+ module Menu
7
+ # A component for rendering badge elements in menus
8
+ #
9
+ # @example Basic usage
10
+ # Badge.new("New!", class: "badge-primary")
11
+ #
12
+ # @example With custom styling
13
+ # Badge.new("2", class: "badge-notification")
14
+ #
15
+ class Badge < COMPONENT_BASE
16
+ # Initialize a new badge component
17
+ #
18
+ # @param content [String] The text content to display in the badge
19
+ # @param options [Hash] Additional HTML attributes for the badge element
20
+ # @option options [String] :class CSS classes to apply to the badge
21
+ def initialize(content, **options)
22
+ @content = content
23
+ @options = options
24
+ super()
25
+ end
26
+
27
+ def view_template
28
+ span(class: @options[:class]) { @content }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -47,27 +47,6 @@ module Phlexi
47
47
  new_item
48
48
  end
49
49
 
50
- # Checks if the menu has any items.
51
- #
52
- # @return [Boolean] true if the menu has no items, false otherwise
53
- def empty?
54
- @items.empty?
55
- end
56
-
57
- # Returns the number of top-level items in the menu.
58
- #
59
- # @return [Integer] The count of top-level menu items
60
- def size
61
- @items.size
62
- end
63
-
64
- # Checks if this menu item has any nested items.
65
- #
66
- # @return [Boolean] true if the item has nested items, false otherwise
67
- def nested?
68
- !empty?
69
- end
70
-
71
50
  # Returns a string representation of the menu structure.
72
51
  #
73
52
  # @return [String] A human-readable representation of the menu
@@ -23,6 +23,8 @@ module Phlexi
23
23
  # Theme class for customizing menu appearance
24
24
  class Theme < Phlexi::Menu::Theme; end
25
25
 
26
+ class Badge < Phlexi::Menu::Badge; end
27
+
26
28
  # @return [Integer] The default maximum nesting depth for menu items
27
29
  DEFAULT_MAX_DEPTH = 3
28
30
 
@@ -31,7 +33,10 @@ module Phlexi
31
33
  # @param menu [Phlexi::Menu::Builder] The menu structure to render
32
34
  # @param max_depth [Integer] Maximum nesting depth for menu items
33
35
  # @param options [Hash] Additional options passed to rendering methods
36
+ # @raise [ArgumentError] If menu is nil
34
37
  def initialize(menu, max_depth: default_max_depth, **options)
38
+ raise ArgumentError, "Menu cannot be nil" if menu.nil?
39
+
35
40
  @menu = menu
36
41
  @max_depth = max_depth
37
42
  @options = options
@@ -39,9 +44,7 @@ module Phlexi
39
44
  end
40
45
 
41
46
  def view_template
42
- nav(class: themed(:nav)) do
43
- render_items(@menu.items)
44
- end
47
+ nav(class: themed(:nav)) { render_items(@menu.items) }
45
48
  end
46
49
 
47
50
  protected
@@ -50,14 +53,12 @@ module Phlexi
50
53
  #
51
54
  # @param items [Array<Phlexi::Menu::Item>] The items to render
52
55
  # @param depth [Integer] Current nesting depth
56
+ # @return [void]
53
57
  def render_items(items, depth = 0)
54
- return if depth >= @max_depth
55
- return if items.empty?
58
+ return if depth >= @max_depth || items.empty?
56
59
 
57
60
  ul(class: themed(:items_container, depth)) do
58
- items.each do |item|
59
- render_item_wrapper(item, depth)
60
- end
61
+ items.each { |item| render_item_wrapper(item, depth) }
61
62
  end
62
63
  end
63
64
 
@@ -65,35 +66,41 @@ module Phlexi
65
66
  #
66
67
  # @param item [Phlexi::Menu::Item] The item to wrap
67
68
  # @param depth [Integer] Current nesting depth
69
+ # @return [void]
68
70
  def render_item_wrapper(item, depth)
69
- li(class: tokens(
70
- themed(:item_wrapper, depth),
71
- item_parent_class(item, depth),
72
- active?(item) ? themed(:active, depth) : nil
73
- )) do
71
+ li(class: compute_item_wrapper_classes(item, depth)) do
74
72
  render_item_content(item, depth)
75
- render_items(item.items, depth + 1) if nested?(item, depth)
73
+ render_nested_items(item, depth)
76
74
  end
77
75
  end
78
76
 
79
- def nested?(item, depth)
80
- has_children = item.items.any?
81
- within_depth = (depth + 1) < @max_depth
82
- has_children && within_depth
83
- end
84
-
85
- def item_parent_class(item, depth)
86
- nested?(item, depth) ? themed(:item_parent, depth) : nil
77
+ # Computes CSS classes for item wrapper
78
+ #
79
+ # @param item [Phlexi::Menu::Item] The menu item
80
+ # @param depth [Integer] Current nesting depth
81
+ # @return [String] Space-separated CSS classes
82
+ def compute_item_wrapper_classes(item, depth)
83
+ tokens(
84
+ themed(:item_wrapper, depth),
85
+ item_parent_class(item, depth),
86
+ active?(item) ? themed(:active, depth) : nil
87
+ )
87
88
  end
88
89
 
89
- def active?(item)
90
- item.active?(self)
90
+ # Renders nested items if present and within depth limit
91
+ #
92
+ # @param item [Phlexi::Menu::Item] The parent menu item
93
+ # @param depth [Integer] Current nesting depth
94
+ # @return [void]
95
+ def render_nested_items(item, depth)
96
+ render_items(item.items, depth + 1) if nested?(item, depth)
91
97
  end
92
98
 
93
99
  # Renders the content of a menu item, choosing between link and span.
94
100
  #
95
101
  # @param item [Phlexi::Menu::Item] The item to render content for
96
102
  # @param depth [Integer] Current nesting depth
103
+ # @return [void]
97
104
  def render_item_content(item, depth)
98
105
  if item.url
99
106
  render_item_link(item, depth)
@@ -106,13 +113,11 @@ module Phlexi
106
113
  #
107
114
  # @param item [Phlexi::Menu::Item] The item to render as a link
108
115
  # @param depth [Integer] Current nesting depth
116
+ # @return [void]
109
117
  def render_item_link(item, depth)
110
118
  a(
111
119
  href: item.url,
112
- class: tokens(
113
- themed(:item_link, depth),
114
- active?(item) ? themed(:active, depth) : nil
115
- )
120
+ class: tokens(themed(:item_link, depth), active_class(item, depth))
116
121
  ) do
117
122
  render_item_interior(item, depth)
118
123
  end
@@ -122,6 +127,7 @@ module Phlexi
122
127
  #
123
128
  # @param item [Phlexi::Menu::Item] The item to render as a span
124
129
  # @param depth [Integer] Current nesting depth
130
+ # @return [void]
125
131
  def render_item_span(item, depth)
126
132
  span(class: themed(:item_span, depth)) do
127
133
  render_item_interior(item, depth)
@@ -132,47 +138,69 @@ module Phlexi
132
138
  #
133
139
  # @param item [Phlexi::Menu::Item] The item to render interior content for
134
140
  # @param depth [Integer] Current nesting depth
141
+ # @return [void]
135
142
  def render_item_interior(item, depth)
136
- render_leading_badge(item.leading_badge, depth) if item.leading_badge
143
+ render_leading_badge(item, depth) if item.leading_badge
137
144
  render_icon(item.icon, depth) if item.icon
138
145
  render_label(item.label, depth)
139
- render_trailing_badge(item.trailing_badge, depth) if item.trailing_badge
146
+ render_trailing_badge(item, depth) if item.trailing_badge
140
147
  end
141
148
 
142
149
  # Renders the item's label.
143
150
  #
144
151
  # @param label [String, Component] The label to render
145
152
  # @param depth [Integer] Current nesting depth
153
+ # @return [void]
146
154
  def render_label(label, depth)
147
- phlexi_render(label) {
155
+ phlexi_render(label) do
148
156
  span(class: themed(:item_label, depth)) { label }
149
- }
157
+ end
158
+ end
159
+
160
+ # Renders the leading badge if present
161
+ #
162
+ # @param item [Phlexi::Menu::Item] The menu item
163
+ # @param depth [Integer] Current nesting depth
164
+ # @return [void]
165
+ def render_leading_badge(item, depth)
166
+ return unless item.leading_badge
167
+
168
+ div(class: themed(:leading_badge_wrapper, depth)) do
169
+ render_badge(item.leading_badge, item.leading_badge_options, :leading_badge, depth)
170
+ end
150
171
  end
151
172
 
152
- # Renders the item's leading badge.
173
+ # Renders the trailing badge if present
153
174
  #
154
- # @param badge [String, Component] The leading badge to render
175
+ # @param item [Phlexi::Menu::Item] The menu item
155
176
  # @param depth [Integer] Current nesting depth
156
- def render_leading_badge(badge, depth)
157
- phlexi_render(badge) {
158
- span(class: themed(:leading_badge, depth)) { badge }
159
- }
177
+ # @return [void]
178
+ def render_trailing_badge(item, depth)
179
+ return unless item.trailing_badge
180
+
181
+ div(class: themed(:trailing_badge_wrapper, depth)) do
182
+ render_badge(item.trailing_badge, item.trailing_badge_options, :trailing_badge, depth)
183
+ end
160
184
  end
161
185
 
162
- # Renders the item's trailing badge.
186
+ # Renders a badge with given options
163
187
  #
164
- # @param badge [String, Component] The trailing badge to render
188
+ # @param badge [Object] The badge content
189
+ # @param options [Hash] Badge rendering options
190
+ # @param type [Symbol] Badge type (leading or trailing)
165
191
  # @param depth [Integer] Current nesting depth
166
- def render_trailing_badge(badge, depth)
167
- phlexi_render(badge) {
168
- span(class: themed(:trailing_badge, depth)) { badge }
169
- }
192
+ # @return [void]
193
+ def render_badge(badge, options, type, depth)
194
+ phlexi_render(badge) do
195
+ render self.class::Badge.new(badge, **options)
196
+ end
170
197
  end
171
198
 
172
199
  # Renders the item's icon.
173
200
  #
174
201
  # @param icon [Class] The icon component class to render
175
202
  # @param depth [Integer] Current nesting depth
203
+ # @return [void]
176
204
  def render_icon(icon, depth)
177
205
  return unless icon
178
206
 
@@ -205,7 +233,9 @@ module Phlexi
205
233
  # @param depth [Integer] Current nesting depth
206
234
  # @return [Boolean] Whether the item should be treated as nested
207
235
  def nested?(item, depth)
208
- item.nested? && (depth + 1) < @max_depth
236
+ has_children = item.items.any?
237
+ within_depth = (depth + 1) < @max_depth
238
+ has_children && within_depth
209
239
  end
210
240
 
211
241
  # Determines the parent state class for an item.
@@ -234,6 +264,7 @@ module Phlexi
234
264
  # @param arg [Object] The value to render
235
265
  # @yield The default rendering block
236
266
  # @raise [ArgumentError] If no block is provided
267
+ # @return [void]
237
268
  def phlexi_render(arg, &)
238
269
  return unless arg
239
270
  raise ArgumentError, "phlexi_render requires a default render block" unless block_given?
@@ -247,6 +278,7 @@ module Phlexi
247
278
  end
248
279
  end
249
280
 
281
+ # @return [Integer] The default maximum depth for the menu
250
282
  def default_max_depth = self.class::DEFAULT_MAX_DEPTH
251
283
  end
252
284
  end
@@ -20,6 +20,11 @@ module Phlexi
20
20
  # admin.item "Users", url: "/admin/users"
21
21
  # admin.item "Settings", url: "/admin/settings"
22
22
  # end
23
+ #
24
+ # @example Custom active state logic
25
+ # Item.new("Dashboard", url: "/dashboard", active: -> (context) {
26
+ # context.controller.controller_name == "dashboards"
27
+ # })
23
28
  class Item
24
29
  # @return [String] The display text for the menu item
25
30
  attr_reader :label
@@ -33,9 +38,15 @@ module Phlexi
33
38
  # @return [String, Component, nil] The badge displayed before the label
34
39
  attr_reader :leading_badge
35
40
 
41
+ # @return [Hash] Options for the leading badge
42
+ attr_reader :leading_badge_options
43
+
36
44
  # @return [String, Component, nil] The badge displayed after the label
37
45
  attr_reader :trailing_badge
38
46
 
47
+ # @return [Hash] Options for the trailing badge
48
+ attr_reader :trailing_badge_options
49
+
39
50
  # @return [Array<Item>] Collection of nested menu items
40
51
  attr_reader :items
41
52
 
@@ -55,14 +66,12 @@ module Phlexi
55
66
  # @raise [ArgumentError] If the label is nil or empty
56
67
  def initialize(label, url: nil, icon: nil, leading_badge: nil, trailing_badge: nil, **options, &)
57
68
  raise ArgumentError, "Label cannot be nil" unless label
58
-
59
69
  @label = label
60
70
  @url = url
61
71
  @icon = icon
62
- @leading_badge = leading_badge
63
- @trailing_badge = trailing_badge
64
- @options = options
65
72
  @items = []
73
+ @options = options
74
+ setup_badges(leading_badge, trailing_badge, options)
66
75
 
67
76
  yield self if block_given?
68
77
  end
@@ -70,16 +79,44 @@ module Phlexi
70
79
  # Creates and adds a nested menu item.
71
80
  #
72
81
  # @param label [String] The display text for the nested item
73
- # @param ** [Hash] Additional options passed to the Item constructor
82
+ # @param args [Hash] Additional options passed to the Item constructor
74
83
  # @yield [item] Optional block for adding further nested items
75
84
  # @yieldparam item [Item] The newly created nested item
76
85
  # @return [Item] The created nested item
77
- def item(label, **, &)
78
- new_item = self.class.new(label, **, &)
86
+ def item(label, **args, &)
87
+ new_item = self.class.new(label, **args, &)
79
88
  @items << new_item
80
89
  new_item
81
90
  end
82
91
 
92
+ # Add a leading badge to the menu item
93
+ #
94
+ # @param badge [String, Component] The badge content
95
+ # @param opts [Hash] Additional options for the badge
96
+ # @return [self] Returns self for method chaining
97
+ # @raise [ArgumentError] If badge is nil
98
+ def with_leading_badge(badge, **opts)
99
+ raise ArgumentError, "Badge cannot be nil" if badge.nil?
100
+
101
+ @leading_badge = badge
102
+ @leading_badge_options = opts.freeze
103
+ self
104
+ end
105
+
106
+ # Add a trailing badge to the menu item
107
+ #
108
+ # @param badge [String, Component] The badge content
109
+ # @param opts [Hash] Additional options for the badge
110
+ # @return [self] Returns self for method chaining
111
+ # @raise [ArgumentError] If badge is nil
112
+ def with_trailing_badge(badge, **opts)
113
+ raise ArgumentError, "Badge cannot be nil" if badge.nil?
114
+
115
+ @trailing_badge = badge
116
+ @trailing_badge_options = opts.freeze
117
+ self
118
+ end
119
+
83
120
  # Determines if this menu item should be shown as active.
84
121
  # Checks in the following order:
85
122
  # 1. Custom active logic if provided in options
@@ -89,44 +126,54 @@ module Phlexi
89
126
  # @param context [Object] The context object (typically a controller) for active state checking
90
127
  # @return [Boolean] true if the item should be shown as active, false otherwise
91
128
  def active?(context)
92
- # First check custom active logic if provided
93
- return @options[:active].call(context) if @options[:active].respond_to?(:call)
94
-
95
- # Then check if this item's URL matches current page
96
- if context.respond_to?(:helpers) && @url
97
- return true if context.helpers.current_page?(@url)
98
- end
129
+ check_custom_active_state(context) ||
130
+ check_current_page_match(context) ||
131
+ check_nested_items_active(context)
132
+ end
99
133
 
100
- # Finally check if any child items are active
101
- @items.any? { |item| item.active?(context) }
134
+ # Returns a string representation of the menu item.
135
+ #
136
+ # @return [String] A human-readable representation of the menu item
137
+ def inspect
138
+ "#<#{self.class} label=#{@label.inspect} url=#{@url.inspect} items=#{@items.inspect}>"
102
139
  end
103
140
 
104
- # Checks if the menu has any items.
141
+ private
142
+
143
+ # Sets up the badge attributes
105
144
  #
106
- # @return [Boolean] true if the menu has no items, false otherwise
107
- def empty?
108
- @items.empty?
145
+ # @param leading_badge [String, Component, nil] The leading badge
146
+ # @param trailing_badge [String, Component, nil] The trailing badge
147
+ # @param options [Hash] Options containing badge configurations
148
+ def setup_badges(leading_badge, trailing_badge, options)
149
+ @leading_badge = leading_badge
150
+ @leading_badge_options = (options.delete(:leading_badge_options) || {}).freeze
151
+ @trailing_badge = trailing_badge
152
+ @trailing_badge_options = (options.delete(:trailing_badge_options) || {}).freeze
109
153
  end
110
154
 
111
- # Returns the number of top-level items in the menu.
155
+ # Checks if there's custom active state logic
112
156
  #
113
- # @return [Integer] The count of top-level menu items
114
- def size
115
- @items.size
157
+ # @param context [Object] The context for active state checking
158
+ # @return [Boolean] Result of custom active check
159
+ def check_custom_active_state(context)
160
+ @options[:active].respond_to?(:call) && @options[:active].call(context)
116
161
  end
117
162
 
118
- # Checks if this menu item has any nested items.
163
+ # Checks if the current page matches the item's URL
119
164
  #
120
- # @return [Boolean] true if the item has nested items, false otherwise
121
- def nested?
122
- !empty?
165
+ # @param context [Object] The context for URL matching
166
+ # @return [Boolean] Whether the current page matches
167
+ def check_current_page_match(context)
168
+ context.respond_to?(:helpers) && @url && context.helpers.current_page?(@url)
123
169
  end
124
170
 
125
- # Returns a string representation of the menu item.
171
+ # Checks if any nested items are active
126
172
  #
127
- # @return [String] A human-readable representation of the menu item
128
- def inspect
129
- "#<#{self.class} label=#{@label} url=#{@url} items=#{@items.map(&:inspect)}>"
173
+ # @param context [Object] The context for checking nested items
174
+ # @return [Boolean] Whether any nested items are active
175
+ def check_nested_items_active(context)
176
+ @items.any? { |item| item.active?(context) }
130
177
  end
131
178
  end
132
179
  end
@@ -25,6 +25,8 @@ module Phlexi
25
25
  hover: nil, # Hover state
26
26
 
27
27
  # Badge elements
28
+ leading_badge_wrapper: nil, # Wrapper for leading badge
29
+ trailing_badge_wrapper: nil, # Wrapper for trailing badge
28
30
  leading_badge: nil, # Badge before label
29
31
  trailing_badge: nil, # Badge after label
30
32
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlexi
4
4
  module Menu
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlexi-menu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-11 00:00:00.000000000 Z
11
+ date: 2024-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phlex
@@ -180,6 +180,7 @@ files:
180
180
  - LICENSE.txt
181
181
  - README.md
182
182
  - Rakefile
183
+ - changes.patch
183
184
  - config.ru
184
185
  - export.json
185
186
  - export.rb
@@ -189,6 +190,7 @@ files:
189
190
  - gemfiles/rails_7.gemfile.lock
190
191
  - lib/phlexi-menu.rb
191
192
  - lib/phlexi/menu.rb
193
+ - lib/phlexi/menu/badge.rb
192
194
  - lib/phlexi/menu/builder.rb
193
195
  - lib/phlexi/menu/component.rb
194
196
  - lib/phlexi/menu/item.rb