phlexi-menu 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Appraisals +1 -1
- data/README.md +151 -126
- data/changes.patch +393 -0
- data/gemfiles/default.gemfile.lock +1 -1
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/lib/phlexi/menu/badge.rb +32 -0
- data/lib/phlexi/menu/builder.rb +0 -21
- data/lib/phlexi/menu/component.rb +87 -54
- data/lib/phlexi/menu/item.rb +79 -32
- data/lib/phlexi/menu/theme.rb +2 -0
- data/lib/phlexi/menu/version.rb +1 -1
- metadata +10 -10
- data/export.json +0 -82
- data/export.rb +0 -48
data/changes.patch
ADDED
@@ -0,0 +1,393 @@
|
|
1
|
+
diff --git a/lib/phlexi/menu/component.rb b/lib/phlexi/menu/component.rb
|
2
|
+
index 7f35b74..b9ecc5e 100644
|
3
|
+
--- a/lib/phlexi/menu/component.rb
|
4
|
+
+++ b/lib/phlexi/menu/component.rb
|
5
|
+
@@ -31,7 +31,10 @@ module Phlexi
|
6
|
+
# @param menu [Phlexi::Menu::Builder] The menu structure to render
|
7
|
+
# @param max_depth [Integer] Maximum nesting depth for menu items
|
8
|
+
# @param options [Hash] Additional options passed to rendering methods
|
9
|
+
+ # @raise [ArgumentError] If menu is nil
|
10
|
+
def initialize(menu, max_depth: default_max_depth, **options)
|
11
|
+
+ raise ArgumentError, "Menu cannot be nil" if menu.nil?
|
12
|
+
+
|
13
|
+
@menu = menu
|
14
|
+
@max_depth = max_depth
|
15
|
+
@options = options
|
16
|
+
@@ -39,9 +42,7 @@ module Phlexi
|
17
|
+
end
|
18
|
+
|
19
|
+
def view_template
|
20
|
+
- nav(class: themed(:nav)) do
|
21
|
+
- render_items(@menu.items)
|
22
|
+
- end
|
23
|
+
+ nav(class: themed(:nav)) { render_items(@menu.items) }
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
@@ -50,14 +51,12 @@ module Phlexi
|
28
|
+
#
|
29
|
+
# @param items [Array<Phlexi::Menu::Item>] The items to render
|
30
|
+
# @param depth [Integer] Current nesting depth
|
31
|
+
+ # @return [void]
|
32
|
+
def render_items(items, depth = 0)
|
33
|
+
- return if depth >= @max_depth
|
34
|
+
- return if items.empty?
|
35
|
+
+ return if depth >= @max_depth || items.empty?
|
36
|
+
|
37
|
+
ul(class: themed(:items_container, depth)) do
|
38
|
+
- items.each do |item|
|
39
|
+
- render_item_wrapper(item, depth)
|
40
|
+
- end
|
41
|
+
+ items.each { |item| render_item_wrapper(item, depth) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
@@ -65,21 +64,41 @@ module Phlexi
|
46
|
+
#
|
47
|
+
# @param item [Phlexi::Menu::Item] The item to wrap
|
48
|
+
# @param depth [Integer] Current nesting depth
|
49
|
+
+ # @return [void]
|
50
|
+
def render_item_wrapper(item, depth)
|
51
|
+
- li(class: tokens(
|
52
|
+
+ li(class: compute_item_wrapper_classes(item, depth)) do
|
53
|
+
+ render_item_content(item, depth)
|
54
|
+
+ render_nested_items(item, depth)
|
55
|
+
+ end
|
56
|
+
+ end
|
57
|
+
+
|
58
|
+
+ # Computes CSS classes for item wrapper
|
59
|
+
+ #
|
60
|
+
+ # @param item [Phlexi::Menu::Item] The menu item
|
61
|
+
+ # @param depth [Integer] Current nesting depth
|
62
|
+
+ # @return [String] Space-separated CSS classes
|
63
|
+
+ def compute_item_wrapper_classes(item, depth)
|
64
|
+
+ tokens(
|
65
|
+
themed(:item_wrapper, depth),
|
66
|
+
item_parent_class(item, depth),
|
67
|
+
active?(item) ? themed(:active, depth) : nil
|
68
|
+
- )) do
|
69
|
+
- render_item_content(item, depth)
|
70
|
+
- render_items(item.items, depth + 1) if nested?(item, depth)
|
71
|
+
- end
|
72
|
+
+ )
|
73
|
+
+ end
|
74
|
+
+
|
75
|
+
+ # Renders nested items if present and within depth limit
|
76
|
+
+ #
|
77
|
+
+ # @param item [Phlexi::Menu::Item] The parent menu item
|
78
|
+
+ # @param depth [Integer] Current nesting depth
|
79
|
+
+ # @return [void]
|
80
|
+
+ def render_nested_items(item, depth)
|
81
|
+
+ render_items(item.items, depth + 1) if nested?(item, depth)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Renders the content of a menu item, choosing between link and span.
|
85
|
+
#
|
86
|
+
# @param item [Phlexi::Menu::Item] The item to render content for
|
87
|
+
# @param depth [Integer] Current nesting depth
|
88
|
+
+ # @return [void]
|
89
|
+
def render_item_content(item, depth)
|
90
|
+
if item.url
|
91
|
+
render_item_link(item, depth)
|
92
|
+
@@ -92,13 +111,11 @@ module Phlexi
|
93
|
+
#
|
94
|
+
# @param item [Phlexi::Menu::Item] The item to render as a link
|
95
|
+
# @param depth [Integer] Current nesting depth
|
96
|
+
+ # @return [void]
|
97
|
+
def render_item_link(item, depth)
|
98
|
+
a(
|
99
|
+
href: item.url,
|
100
|
+
- class: tokens(
|
101
|
+
- themed(:item_link, depth),
|
102
|
+
- active_class(item, depth)
|
103
|
+
- )
|
104
|
+
+ class: tokens(themed(:item_link, depth), active_class(item, depth))
|
105
|
+
) do
|
106
|
+
render_item_interior(item, depth)
|
107
|
+
end
|
108
|
+
@@ -108,6 +125,7 @@ module Phlexi
|
109
|
+
#
|
110
|
+
# @param item [Phlexi::Menu::Item] The item to render as a span
|
111
|
+
# @param depth [Integer] Current nesting depth
|
112
|
+
+ # @return [void]
|
113
|
+
def render_item_span(item, depth)
|
114
|
+
span(class: themed(:item_span, depth)) do
|
115
|
+
render_item_interior(item, depth)
|
116
|
+
@@ -118,47 +136,69 @@ module Phlexi
|
117
|
+
#
|
118
|
+
# @param item [Phlexi::Menu::Item] The item to render interior content for
|
119
|
+
# @param depth [Integer] Current nesting depth
|
120
|
+
+ # @return [void]
|
121
|
+
def render_item_interior(item, depth)
|
122
|
+
- render_leading_badge(item.leading_badge, depth) if item.leading_badge
|
123
|
+
+ render_leading_badge(item, depth) if item.leading_badge
|
124
|
+
render_icon(item.icon, depth) if item.icon
|
125
|
+
render_label(item.label, depth)
|
126
|
+
- render_trailing_badge(item.trailing_badge, depth) if item.trailing_badge
|
127
|
+
+ render_trailing_badge(item, depth) if item.trailing_badge
|
128
|
+
end
|
129
|
+
|
130
|
+
# Renders the item's label.
|
131
|
+
#
|
132
|
+
# @param label [String, Component] The label to render
|
133
|
+
# @param depth [Integer] Current nesting depth
|
134
|
+
+ # @return [void]
|
135
|
+
def render_label(label, depth)
|
136
|
+
- phlexi_render(label) {
|
137
|
+
+ phlexi_render(label) do
|
138
|
+
span(class: themed(:item_label, depth)) { label }
|
139
|
+
- }
|
140
|
+
+ end
|
141
|
+
+ end
|
142
|
+
+
|
143
|
+
+ # Renders the leading badge if present
|
144
|
+
+ #
|
145
|
+
+ # @param item [Phlexi::Menu::Item] The menu item
|
146
|
+
+ # @param depth [Integer] Current nesting depth
|
147
|
+
+ # @return [void]
|
148
|
+
+ def render_leading_badge(item, depth)
|
149
|
+
+ return unless item.leading_badge
|
150
|
+
+
|
151
|
+
+ div(class: themed(:leading_badge_wrapper, depth)) do
|
152
|
+
+ render_badge(item.leading_badge, item.leading_badge_options, :leading_badge, depth)
|
153
|
+
+ end
|
154
|
+
end
|
155
|
+
|
156
|
+
- # Renders the item's leading badge.
|
157
|
+
+ # Renders the trailing badge if present
|
158
|
+
#
|
159
|
+
- # @param badge [String, Component] The leading badge to render
|
160
|
+
+ # @param item [Phlexi::Menu::Item] The menu item
|
161
|
+
# @param depth [Integer] Current nesting depth
|
162
|
+
- def render_leading_badge(badge, depth)
|
163
|
+
- phlexi_render(badge) {
|
164
|
+
- span(class: themed(:leading_badge, depth)) { badge }
|
165
|
+
- }
|
166
|
+
+ # @return [void]
|
167
|
+
+ def render_trailing_badge(item, depth)
|
168
|
+
+ return unless item.trailing_badge
|
169
|
+
+
|
170
|
+
+ div(class: themed(:trailing_badge_wrapper, depth)) do
|
171
|
+
+ render_badge(item.trailing_badge, item.trailing_badge_options, :trailing_badge, depth)
|
172
|
+
+ end
|
173
|
+
end
|
174
|
+
|
175
|
+
- # Renders the item's trailing badge.
|
176
|
+
+ # Renders a badge with given options
|
177
|
+
#
|
178
|
+
- # @param badge [String, Component] The trailing badge to render
|
179
|
+
+ # @param badge [Object] The badge content
|
180
|
+
+ # @param options [Hash] Badge rendering options
|
181
|
+
+ # @param type [Symbol] Badge type (leading or trailing)
|
182
|
+
# @param depth [Integer] Current nesting depth
|
183
|
+
- def render_trailing_badge(badge, depth)
|
184
|
+
- phlexi_render(badge) {
|
185
|
+
- span(class: themed(:trailing_badge, depth)) { badge }
|
186
|
+
- }
|
187
|
+
+ # @return [void]
|
188
|
+
+ def render_badge(badge, options, type, depth)
|
189
|
+
+ phlexi_render(badge) do
|
190
|
+
+ render BadgeComponent.new(badge, **options)
|
191
|
+
+ end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Renders the item's icon.
|
195
|
+
#
|
196
|
+
# @param icon [Class] The icon component class to render
|
197
|
+
# @param depth [Integer] Current nesting depth
|
198
|
+
+ # @return [void]
|
199
|
+
def render_icon(icon, depth)
|
200
|
+
return unless icon
|
201
|
+
|
202
|
+
@@ -222,6 +262,7 @@ module Phlexi
|
203
|
+
# @param arg [Object] The value to render
|
204
|
+
# @yield The default rendering block
|
205
|
+
# @raise [ArgumentError] If no block is provided
|
206
|
+
+ # @return [void]
|
207
|
+
def phlexi_render(arg, &)
|
208
|
+
return unless arg
|
209
|
+
raise ArgumentError, "phlexi_render requires a default render block" unless block_given?
|
210
|
+
@@ -235,6 +276,7 @@ module Phlexi
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
+ # @return [Integer] The default maximum depth for the menu
|
215
|
+
def default_max_depth = self.class::DEFAULT_MAX_DEPTH
|
216
|
+
end
|
217
|
+
end
|
218
|
+
diff --git a/lib/phlexi/menu/item.rb b/lib/phlexi/menu/item.rb
|
219
|
+
index 4d128d6..b8b5614 100644
|
220
|
+
--- a/lib/phlexi/menu/item.rb
|
221
|
+
+++ b/lib/phlexi/menu/item.rb
|
222
|
+
@@ -20,6 +20,11 @@ module Phlexi
|
223
|
+
# admin.item "Users", url: "/admin/users"
|
224
|
+
# admin.item "Settings", url: "/admin/settings"
|
225
|
+
# end
|
226
|
+
+ #
|
227
|
+
+ # @example Custom active state logic
|
228
|
+
+ # Item.new("Dashboard", url: "/dashboard", active: -> (context) {
|
229
|
+
+ # context.controller.controller_name == "dashboards"
|
230
|
+
+ # })
|
231
|
+
class Item
|
232
|
+
# @return [String] The display text for the menu item
|
233
|
+
attr_reader :label
|
234
|
+
@@ -33,9 +38,15 @@ module Phlexi
|
235
|
+
# @return [String, Component, nil] The badge displayed before the label
|
236
|
+
attr_reader :leading_badge
|
237
|
+
|
238
|
+
+ # @return [Hash] Options for the leading badge
|
239
|
+
+ attr_reader :leading_badge_options
|
240
|
+
+
|
241
|
+
# @return [String, Component, nil] The badge displayed after the label
|
242
|
+
attr_reader :trailing_badge
|
243
|
+
|
244
|
+
+ # @return [Hash] Options for the trailing badge
|
245
|
+
+ attr_reader :trailing_badge_options
|
246
|
+
+
|
247
|
+
# @return [Array<Item>] Collection of nested menu items
|
248
|
+
attr_reader :items
|
249
|
+
|
250
|
+
@@ -55,14 +66,12 @@ module Phlexi
|
251
|
+
# @raise [ArgumentError] If the label is nil or empty
|
252
|
+
def initialize(label, url: nil, icon: nil, leading_badge: nil, trailing_badge: nil, **options, &)
|
253
|
+
raise ArgumentError, "Label cannot be nil" unless label
|
254
|
+
-
|
255
|
+
@label = label
|
256
|
+
@url = url
|
257
|
+
@icon = icon
|
258
|
+
- @leading_badge = leading_badge
|
259
|
+
- @trailing_badge = trailing_badge
|
260
|
+
- @options = options
|
261
|
+
@items = []
|
262
|
+
+ @options = options
|
263
|
+
+ setup_badges(leading_badge, trailing_badge, options)
|
264
|
+
|
265
|
+
yield self if block_given?
|
266
|
+
end
|
267
|
+
@@ -70,16 +79,44 @@ module Phlexi
|
268
|
+
# Creates and adds a nested menu item.
|
269
|
+
#
|
270
|
+
# @param label [String] The display text for the nested item
|
271
|
+
- # @param ** [Hash] Additional options passed to the Item constructor
|
272
|
+
+ # @param args [Hash] Additional options passed to the Item constructor
|
273
|
+
# @yield [item] Optional block for adding further nested items
|
274
|
+
# @yieldparam item [Item] The newly created nested item
|
275
|
+
# @return [Item] The created nested item
|
276
|
+
- def item(label, **, &)
|
277
|
+
- new_item = self.class.new(label, **, &)
|
278
|
+
+ def item(label, **args, &)
|
279
|
+
+ new_item = self.class.new(label, **args, &)
|
280
|
+
@items << new_item
|
281
|
+
new_item
|
282
|
+
end
|
283
|
+
|
284
|
+
+ # Add a leading badge to the menu item
|
285
|
+
+ #
|
286
|
+
+ # @param badge [String, Component] The badge content
|
287
|
+
+ # @param opts [Hash] Additional options for the badge
|
288
|
+
+ # @return [self] Returns self for method chaining
|
289
|
+
+ # @raise [ArgumentError] If badge is nil
|
290
|
+
+ def with_leading_badge(badge, **opts)
|
291
|
+
+ raise ArgumentError, "Badge cannot be nil" if badge.nil?
|
292
|
+
+
|
293
|
+
+ @leading_badge = badge
|
294
|
+
+ @leading_badge_options = opts
|
295
|
+
+ self
|
296
|
+
+ end
|
297
|
+
+
|
298
|
+
+ # Add a trailing badge to the menu item
|
299
|
+
+ #
|
300
|
+
+ # @param badge [String, Component] The badge content
|
301
|
+
+ # @param opts [Hash] Additional options for the badge
|
302
|
+
+ # @return [self] Returns self for method chaining
|
303
|
+
+ # @raise [ArgumentError] If badge is nil
|
304
|
+
+ def with_trailing_badge(badge, **opts)
|
305
|
+
+ raise ArgumentError, "Badge cannot be nil" if badge.nil?
|
306
|
+
+
|
307
|
+
+ @trailing_badge = badge
|
308
|
+
+ @trailing_badge_options = opts
|
309
|
+
+ self
|
310
|
+
+ end
|
311
|
+
+
|
312
|
+
# Determines if this menu item should be shown as active.
|
313
|
+
# Checks in the following order:
|
314
|
+
# 1. Custom active logic if provided in options
|
315
|
+
@@ -89,23 +126,54 @@ module Phlexi
|
316
|
+
# @param context [Object] The context object (typically a controller) for active state checking
|
317
|
+
# @return [Boolean] true if the item should be shown as active, false otherwise
|
318
|
+
def active?(context)
|
319
|
+
- # First check custom active logic if provided
|
320
|
+
- return @options[:active].call(context) if @options[:active].respond_to?(:call)
|
321
|
+
-
|
322
|
+
- # Then check if this item's URL matches current page
|
323
|
+
- if context.respond_to?(:helpers) && @url
|
324
|
+
- return true if context.helpers.current_page?(@url)
|
325
|
+
- end
|
326
|
+
-
|
327
|
+
- # Finally check if any child items are active
|
328
|
+
- @items.any? { |item| item.active?(context) }
|
329
|
+
+ check_custom_active_state(context) ||
|
330
|
+
+ check_current_page_match(context) ||
|
331
|
+
+ check_nested_items_active(context)
|
332
|
+
end
|
333
|
+
|
334
|
+
# Returns a string representation of the menu item.
|
335
|
+
#
|
336
|
+
# @return [String] A human-readable representation of the menu item
|
337
|
+
def inspect
|
338
|
+
- "#<#{self.class} label=#{@label} url=#{@url} items=#{@items.map(&:inspect)}>"
|
339
|
+
+ "#<#{self.class} label=#{@label.inspect} url=#{@url.inspect} items=#{@items.inspect}>"
|
340
|
+
+ end
|
341
|
+
+
|
342
|
+
+ private
|
343
|
+
+
|
344
|
+
+ # Sets up the badge attributes
|
345
|
+
+ #
|
346
|
+
+ # @param leading_badge [String, Component, nil] The leading badge
|
347
|
+
+ # @param trailing_badge [String, Component, nil] The trailing badge
|
348
|
+
+ # @param options [Hash] Options containing badge configurations
|
349
|
+
+ def setup_badges(leading_badge, trailing_badge, options)
|
350
|
+
+ @leading_badge = leading_badge
|
351
|
+
+ @leading_badge_options = options.delete(:leading_badge_options) || {}
|
352
|
+
+ @trailing_badge = trailing_badge
|
353
|
+
+ @trailing_badge_options = options.delete(:trailing_badge_options) || {}
|
354
|
+
+ end
|
355
|
+
+
|
356
|
+
+ # Checks if there's custom active state logic
|
357
|
+
+ #
|
358
|
+
+ # @param context [Object] The context for active state checking
|
359
|
+
+ # @return [Boolean] Result of custom active check
|
360
|
+
+ def check_custom_active_state(context)
|
361
|
+
+ @options[:active].respond_to?(:call) && @options[:active].call(context)
|
362
|
+
+ end
|
363
|
+
+
|
364
|
+
+ # Checks if the current page matches the item's URL
|
365
|
+
+ #
|
366
|
+
+ # @param context [Object] The context for URL matching
|
367
|
+
+ # @return [Boolean] Whether the current page matches
|
368
|
+
+ def check_current_page_match(context)
|
369
|
+
+ context.respond_to?(:helpers) && @url && context.helpers.current_page?(@url)
|
370
|
+
+ end
|
371
|
+
+
|
372
|
+
+ # Checks if any nested items are active
|
373
|
+
+ #
|
374
|
+
+ # @param context [Object] The context for checking nested items
|
375
|
+
+ # @return [Boolean] Whether any nested items are active
|
376
|
+
+ def check_nested_items_active(context)
|
377
|
+
+ @items.any? { |item| item.active?(context) }
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
diff --git a/lib/phlexi/menu/theme.rb b/lib/phlexi/menu/theme.rb
|
382
|
+
index 076b137..415498b 100644
|
383
|
+
--- a/lib/phlexi/menu/theme.rb
|
384
|
+
+++ b/lib/phlexi/menu/theme.rb
|
385
|
+
@@ -25,6 +25,8 @@ module Phlexi
|
386
|
+
hover: nil, # Hover state
|
387
|
+
|
388
|
+
# Badge elements
|
389
|
+
+ leading_badge_wrapper: nil, # Wrapper for leading badge
|
390
|
+
+ trailing_badge_wrapper: nil, # Wrapper for trailing badge
|
391
|
+
leading_badge: nil, # Badge before label
|
392
|
+
trailing_badge: nil, # Badge after label
|
393
|
+
|
@@ -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
|
data/lib/phlexi/menu/builder.rb
CHANGED
@@ -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
|