spree_admin 5.2.0.rc3 → 5.2.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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/spree/admin/application.scss +1 -1
  3. data/app/assets/stylesheets/spree/admin/components/_dropdowns.scss +2 -0
  4. data/app/assets/stylesheets/spree/admin/components/_main.scss +2 -233
  5. data/app/assets/stylesheets/spree/admin/components/_sidebar.scss +693 -0
  6. data/app/assets/stylesheets/spree/admin/global/_variables.scss +1 -0
  7. data/app/assets/stylesheets/spree/admin/shared/_base.scss +7 -1
  8. data/app/assets/stylesheets/spree/admin/views/_dashboard.scss +14 -0
  9. data/app/controllers/spree/admin/integrations_controller.rb +1 -1
  10. data/app/controllers/spree/admin/metafields_controller.rb +1 -1
  11. data/app/controllers/spree/admin/page_blocks_controller.rb +1 -1
  12. data/app/controllers/spree/admin/payment_methods_controller.rb +1 -1
  13. data/app/controllers/spree/admin/promotion_actions_controller.rb +1 -1
  14. data/app/controllers/spree/admin/promotion_rules_controller.rb +1 -1
  15. data/app/controllers/spree/admin/promotions_controller.rb +1 -1
  16. data/app/controllers/spree/admin/reports_controller.rb +1 -1
  17. data/app/controllers/spree/admin/taxons_controller.rb +1 -1
  18. data/app/controllers/spree/admin/translations_controller.rb +1 -1
  19. data/app/helpers/spree/admin/base_helper.rb +1 -1
  20. data/app/helpers/spree/admin/drawer_helper.rb +6 -6
  21. data/app/helpers/spree/admin/dropdown_helper.rb +10 -2
  22. data/app/helpers/spree/admin/modal_helper.rb +2 -0
  23. data/app/helpers/spree/admin/navigation_helper.rb +46 -3
  24. data/app/helpers/spree/admin/orders_filters_helper.rb +1 -0
  25. data/app/helpers/spree/admin/promotion_actions_helper.rb +1 -1
  26. data/app/helpers/spree/admin/promotion_rules_helper.rb +1 -1
  27. data/app/helpers/spree/admin/translations_helper.rb +1 -1
  28. data/app/javascript/spree/admin/application.js +2 -1
  29. data/app/javascript/spree/admin/controllers/dropdown_controller.js +85 -14
  30. data/app/javascript/spree/admin/controllers/sidebar_controller.js +231 -0
  31. data/app/javascript/spree/admin/controllers/tooltip_controller.js +84 -31
  32. data/app/models/spree/admin/form_builder.rb +74 -16
  33. data/app/models/spree/admin/navigation/builder.rb +82 -0
  34. data/app/models/spree/admin/navigation/item.rb +177 -0
  35. data/app/models/spree/admin/navigation.rb +193 -0
  36. data/app/views/layouts/spree/admin.html.erb +1 -1
  37. data/app/views/spree/admin/assets/edit.html.erb +1 -1
  38. data/app/views/spree/admin/custom_domains/_form.html.erb +2 -14
  39. data/app/views/spree/admin/gift_cards/_filters.html.erb +26 -18
  40. data/app/views/spree/admin/gift_cards/index.html.erb +1 -1
  41. data/app/views/spree/admin/orders/_customer.html.erb +2 -2
  42. data/app/views/spree/admin/orders/_filters.html.erb +34 -25
  43. data/app/views/spree/admin/orders/_table_filter_dropdown.html.erb +1 -1
  44. data/app/views/spree/admin/page_blocks/forms/_image.html.erb +2 -5
  45. data/app/views/spree/admin/page_links/_form.html.erb +4 -13
  46. data/app/views/spree/admin/page_sections/forms/_header.html.erb +0 -2
  47. data/app/views/spree/admin/payments/_payment.html.erb +7 -0
  48. data/app/views/spree/admin/posts/_form.html.erb +1 -4
  49. data/app/views/spree/admin/posts/filters.html.erb +18 -8
  50. data/app/views/spree/admin/products/_bulk_operations.html.erb +1 -1
  51. data/app/views/spree/admin/products/_filters.html.erb +17 -6
  52. data/app/views/spree/admin/products/_table_filter_dropdown.html.erb +1 -1
  53. data/app/views/spree/admin/profile/edit.html.erb +9 -61
  54. data/app/views/spree/admin/promotions/_filters.html.erb +23 -13
  55. data/app/views/spree/admin/promotions/_table_filter_dropdown.html.erb +1 -1
  56. data/app/views/spree/admin/promotions/form/_settings.html.erb +2 -13
  57. data/app/views/spree/admin/refunds/_form.html.erb +1 -9
  58. data/app/views/spree/admin/return_authorizations/filters.html.erb +1 -1
  59. data/app/views/spree/admin/shared/_audit_nav.html.erb +2 -0
  60. data/app/views/spree/admin/shared/_calendar_range_picker.html.erb +2 -2
  61. data/app/views/spree/admin/shared/_content_header.html.erb +1 -1
  62. data/app/views/spree/admin/shared/_developers_nav.html.erb +2 -4
  63. data/app/views/spree/admin/shared/_header.html.erb +5 -7
  64. data/app/views/spree/admin/shared/_navigation.html.erb +5 -0
  65. data/app/views/spree/admin/shared/_navigation_item.html.erb +64 -0
  66. data/app/views/spree/admin/shared/_new_item_dropdown.html.erb +1 -1
  67. data/app/views/spree/admin/shared/_page_section_image.html.erb +2 -5
  68. data/app/views/spree/admin/shared/_page_section_logo.html.erb +1 -1
  69. data/app/views/spree/admin/shared/_returns_and_refunds_nav.html.erb +2 -3
  70. data/app/views/spree/admin/shared/_shipping_nav.html.erb +3 -2
  71. data/app/views/spree/admin/shared/_sidebar.html.erb +33 -7
  72. data/app/views/spree/admin/shared/_stock_nav.html.erb +6 -3
  73. data/app/views/spree/admin/shared/_tax_nav.html.erb +1 -2
  74. data/app/views/spree/admin/shared/_team_nav.html.erb +2 -3
  75. data/app/views/spree/admin/shared/_user_dropdown.html.erb +26 -17
  76. data/app/views/spree/admin/shared/sidebar/_customers_nav.html.erb +7 -0
  77. data/app/views/spree/admin/shared/sidebar/_orders_nav.html.erb +22 -2
  78. data/app/views/spree/admin/shared/sidebar/_products_nav.html.erb +21 -0
  79. data/app/views/spree/admin/shared/sidebar/_promotions_nav.html.erb +8 -0
  80. data/app/views/spree/admin/shared/sidebar/_returns_nav.html.erb +12 -0
  81. data/app/views/spree/admin/shared/sidebar/_store_dropdown.html.erb +3 -1
  82. data/app/views/spree/admin/shared/sidebar/_store_nav.html.erb +15 -1
  83. data/app/views/spree/admin/shared/sidebar/_storefront_nav.html.erb +25 -3
  84. data/app/views/spree/admin/shared/sortable_tree/_taxonomy.html.erb +1 -1
  85. data/app/views/spree/admin/stock_items/_filters.html.erb +18 -8
  86. data/app/views/spree/admin/stock_locations/_table_row.html.erb +1 -1
  87. data/app/views/spree/admin/stock_transfers/_filters.html.erb +19 -9
  88. data/app/views/spree/admin/storefront/edit.html.erb +2 -14
  89. data/app/views/spree/admin/stores/form/_basic.html.erb +2 -8
  90. data/app/views/spree/admin/stores/form/_emails.html.erb +1 -1
  91. data/app/views/spree/admin/tax_rates/_form.html.erb +1 -10
  92. data/app/views/spree/admin/taxons/_form.html.erb +2 -9
  93. data/app/views/spree/admin/themes/_theme.html.erb +1 -1
  94. data/app/views/spree/admin/translations/translation_rows/_permalink_field_row.html.erb +1 -12
  95. data/app/views/spree/admin/users/_filters.html.erb +23 -13
  96. data/config/initializers/spree_admin_navigation.rb +510 -0
  97. data/config/locales/en.yml +4 -0
  98. data/lib/generators/spree/admin/scaffold/templates/controller.rb.tt +3 -1
  99. data/lib/generators/spree/admin/scaffold/templates/views/_table_row.html.erb.tt +8 -6
  100. data/lib/spree/admin/engine.rb +64 -2
  101. data/lib/spree/admin/runtime_configuration.rb +1 -0
  102. data/lib/spree/admin.rb +20 -0
  103. metadata +17 -15
  104. data/app/assets/stylesheets/spree/admin/components/_offcanvas.scss +0 -26
  105. data/app/javascript/spree/admin/helpers/canvas.js +0 -29
  106. data/app/views/spree/admin/shared/_offcanvas_nav.html.erb +0 -12
@@ -31,4 +31,18 @@
31
31
  display: flex;
32
32
  }
33
33
  }
34
+ }
35
+
36
+ #updater-notice {
37
+ background-color: $white;
38
+ width: 100%;
39
+ display: flex;
40
+ flex-direction: row;
41
+ gap: 1rem;
42
+ justify-content: flex-start;
43
+ align-items: center;
44
+ padding: 0.5rem 0.5rem;
45
+ border: 1px solid $border-color;
46
+ border-radius: $border-radius-lg;
47
+ box-shadow: $box-shadow-xs;
34
48
  }
@@ -16,7 +16,7 @@ module Spree
16
16
  private
17
17
 
18
18
  def allowed_integration_types
19
- @allowed_integration_types ||= Rails.application.config.spree.integrations.map { |klass| [klass.to_s, klass] }.to_h
19
+ @allowed_integration_types ||= Spree.integrations.map { |klass| [klass.to_s, klass] }.to_h
20
20
  end
21
21
 
22
22
  def require_integration_type
@@ -28,7 +28,7 @@ module Spree
28
28
  end
29
29
 
30
30
  def allowed_model_classes
31
- @allowed_model_classes ||= Rails.application.config.spree.metafield_enabled_resources
31
+ @allowed_model_classes ||= Spree.metafields.enabled_resources
32
32
  end
33
33
 
34
34
  def update_turbo_stream_enabled?
@@ -40,7 +40,7 @@ module Spree
40
40
 
41
41
  def allowed_types
42
42
  [
43
- *Rails.application.config.spree.page_blocks,
43
+ *Spree.page_builder.page_blocks,
44
44
  *parent&.available_blocks_to_add
45
45
  ].uniq.sort_by(&:name)
46
46
  end
@@ -33,7 +33,7 @@ module Spree
33
33
  def allowed_payment_types
34
34
  # We need to map to strings, otherwise some weird things happen with STI
35
35
  # where Rails can't find the ancestor class when we try to save the payment method.
36
- Rails.application.config.spree.payment_methods.map(&:to_s)
36
+ Spree.payment_methods.map(&:to_s)
37
37
  end
38
38
 
39
39
  def permitted_resource_params
@@ -30,7 +30,7 @@ module Spree
30
30
  end
31
31
 
32
32
  def allowed_action_types
33
- Rails.application.config.spree.promotions.actions
33
+ Spree.promotions.actions
34
34
  end
35
35
 
36
36
  def location_after_save
@@ -28,7 +28,7 @@ module Spree
28
28
  end
29
29
 
30
30
  def allowed_rule_types
31
- Rails.application.config.spree.promotions.rules
31
+ Spree.promotions.rules
32
32
  end
33
33
 
34
34
  def location_after_save
@@ -28,7 +28,7 @@ module Spree
28
28
  end
29
29
 
30
30
  def load_form_data
31
- @promotion_rules = Rails.application.config.spree.promotions.rules
31
+ @promotion_rules = Spree.promotions.rules
32
32
  @rule_types = @promotion_rules.map do |promotion_rule|
33
33
  [Spree.t("promotion_rule_types.#{promotion_rule.to_s.demodulize.underscore}.name"), promotion_rule.to_s]
34
34
  end
@@ -54,7 +54,7 @@ module Spree
54
54
  end
55
55
 
56
56
  def allowed_report_types
57
- Rails.application.config.spree.reports.map(&:to_s)
57
+ Spree.reports.map(&:to_s)
58
58
  end
59
59
 
60
60
  def permitted_resource_params
@@ -97,7 +97,7 @@ module Spree
97
97
  end
98
98
 
99
99
  def load_form_data
100
- @taxon_rules = Rails.application.config.spree.taxon_rules
100
+ @taxon_rules = Spree.taxon_rules
101
101
  @rule_types = @taxon_rules.map do |taxon_rule|
102
102
  [Spree.t("admin.taxon_rules.#{taxon_rule.to_s.demodulize.underscore}"), taxon_rule.to_s]
103
103
  end
@@ -42,7 +42,7 @@ module Spree
42
42
 
43
43
  # Allowed translatable resources configured in Spree
44
44
  def allowed_model_classes
45
- Rails.application.config.spree.translatable_resources
45
+ Spree.translatable_resources
46
46
  end
47
47
 
48
48
  # Determine the translation locale to use
@@ -4,7 +4,7 @@ module Spree
4
4
  include Spree::ImagesHelper
5
5
 
6
6
  def render_admin_partials(section, options = {})
7
- Rails.application.config.spree_admin.send(section).map do |partial|
7
+ Spree.admin.partials.send(section.to_s.gsub('_partials', '').to_sym).map do |partial|
8
8
  render partial, options
9
9
  end.join.html_safe
10
10
  end
@@ -1,18 +1,18 @@
1
1
  module Spree
2
2
  module Admin
3
3
  module DrawerHelper
4
- def drawer_header(title)
4
+ def drawer_header(title, controller_name = 'drawer')
5
5
  content_tag(:div, class: 'drawer-header') do
6
- content_tag(:h5, title, class: 'drawer-title') + drawer_close_button
6
+ content_tag(:h5, title, class: 'drawer-title') + drawer_close_button(controller_name)
7
7
  end.html_safe
8
8
  end
9
9
 
10
- def drawer_close_button
11
- button_tag('', type: 'button', class: 'btn-close', data: { action: 'drawer#close', dismiss: 'drawer', aria_label: Spree.t(:close) }).html_safe
10
+ def drawer_close_button(controller_name = 'drawer')
11
+ button_tag('', type: 'button', class: 'btn-close', data: { action: "#{controller_name}#close", dismiss: controller_name, aria_label: Spree.t(:close) }).html_safe
12
12
  end
13
13
 
14
- def drawer_discard_button
15
- button_tag(type: 'button', class: 'btn btn-light', data: { action: 'drawer#close', dismiss: 'drawer' }) do
14
+ def drawer_discard_button(controller_name = 'drawer')
15
+ button_tag(type: 'button', class: 'btn btn-light', data: { action: "#{controller_name}#close", dismiss: controller_name }) do
16
16
  Spree.t('actions.discard')
17
17
  end.html_safe
18
18
  end
@@ -16,10 +16,18 @@ module Spree
16
16
  options[:placement] || 'bottom-start'
17
17
  end
18
18
 
19
- options[:data] = {
19
+ # Extract portal option
20
+ portal = options.delete(:portal)
21
+
22
+ data_attrs = {
20
23
  controller: 'dropdown',
21
24
  dropdown_placement_value: placement
22
- }.merge(options[:data] || {})
25
+ }
26
+
27
+ # Add portal value if explicitly set
28
+ data_attrs[:dropdown_portal_value] = portal unless portal.nil?
29
+
30
+ options[:data] = data_attrs.merge(options[:data] || {})
23
31
  content_tag(:div, options, &block)
24
32
  end
25
33
 
@@ -5,6 +5,8 @@ module Spree
5
5
  # @param title [String, Proc] the title of the modal
6
6
  # @return [String]
7
7
  def modal_header(title)
8
+ Spree::Deprecation.warn('Bootstrap modals are deprecated and will be removed in Spree 6. Please use native dialogs with `dialog_header` helper.')
9
+
8
10
  title = capture(&title) if block_given?
9
11
  content_tag(:div, class: 'modal-header') do
10
12
  content_tag(:h5, title, class: 'modal-title') + modal_close_button
@@ -7,15 +7,15 @@ module Spree
7
7
  # @param [String, nil] icon Optional icon name to prepend to the label
8
8
  # @param [Boolean, nil] active Whether the link should be marked as active
9
9
  # @return [SafeBuffer] The navigation item HTML
10
- def nav_item(label = nil, url, icon: nil, active: nil, data: {})
10
+ def nav_item(label = nil, url, icon: nil, active: nil, data: {}, **options)
11
11
  content_tag :li, class: 'nav-item', role: 'presentation' do
12
12
  if block_given?
13
- active_link_to url, class: 'nav-link', active: active, data: data do
13
+ active_link_to url, class: 'nav-link', active: active, data: data, **options do
14
14
  yield
15
15
  end
16
16
  else
17
17
  label = icon(icon) + label if icon.present? && label.present?
18
- active_link_to label, url, class: 'nav-link', active: active, data: data
18
+ active_link_to label, url, class: 'nav-link', active: active, data: data, **options
19
19
  end
20
20
  end
21
21
  end
@@ -312,6 +312,49 @@ module Spree
312
312
  icon(@breadcrumb_icon)
313
313
  end
314
314
  end
315
+
316
+ # Renders the navigation for the given context
317
+ # @param context [Symbol] the navigation context (:sidebar, :settings, etc.)
318
+ # @param options [Hash] additional options for rendering
319
+ # @return [String] the rendered navigation HTML
320
+ def render_navigation(context = :sidebar, **options)
321
+ return '' if Spree::Admin::RuntimeConfig.legacy_sidebar_navigation
322
+
323
+ items = navigation_items(context)
324
+ return '' if items.empty?
325
+
326
+ render 'spree/admin/shared/navigation',
327
+ items: items,
328
+ context: context,
329
+ **options
330
+ end
331
+
332
+ # Get navigation items for the given context
333
+ # @param context [Symbol] the navigation context
334
+ # @return [Array<Spree::Admin::Navigation::Item>] the visible navigation items
335
+ def navigation_items(context = :sidebar)
336
+ # Pass the view context (self) so that can? and other helpers are available
337
+ Spree.admin.navigation.send(context)&.visible_items(self) || []
338
+ end
339
+
340
+ # Renders page tab navigation for the given context
341
+ # @param context [Symbol] the navigation context (:tax_tabs, :shipping_tabs, etc.)
342
+ # @param options [Hash] additional options for rendering
343
+ # @return [String] the rendered tab navigation HTML wrapped in content_for(:page_tabs)
344
+ def render_tab_navigation(context, **options)
345
+ items = navigation_items(context)
346
+ return '' if items.empty?
347
+
348
+ content_for :page_tabs do
349
+ items.map do |item|
350
+ item_url = item.resolve_url(self)
351
+ item_label = item.resolve_label
352
+ is_active = item.active?(request.path, self)
353
+
354
+ nav_item(item_label, item_url, active: is_active)
355
+ end.join.html_safe
356
+ end
357
+ end
315
358
  end
316
359
  end
317
360
  end
@@ -70,6 +70,7 @@ module Spree
70
70
  # e.g. SELECT DISTINCT DISTINCT "spree_orders".id, "spree_orders"."created_at" AS alias_0 FROM "spree_orders"
71
71
  # see https://github.com/spree/spree/pull/3919
72
72
  @orders = @search.result(distinct: true).page(params[:page]).per(params[:per_page] || Spree::Admin::RuntimeConfig.admin_orders_per_page)
73
+ @collection = @orders
73
74
  end
74
75
 
75
76
  def load_user
@@ -3,7 +3,7 @@ module Spree
3
3
  module PromotionActionsHelper
4
4
  def options_for_promotion_action_types(promotion)
5
5
  existing = promotion.actions.pluck(:type)
6
- Rails.application.config.spree.promotions.actions.map(&:name).reject { |r| existing.include? r }
6
+ Spree.promotions.actions.map(&:name).reject { |r| existing.include? r }
7
7
  end
8
8
  end
9
9
  end
@@ -3,7 +3,7 @@ module Spree
3
3
  module PromotionRulesHelper
4
4
  def options_for_promotion_rule_types(promotion)
5
5
  existing = promotion.rules.pluck(:type)
6
- Rails.application.config.spree.promotions.rules.map(&:name).reject { |r| existing.include? r }
6
+ Spree.promotions.rules.map(&:name).reject { |r| existing.include? r }
7
7
  end
8
8
 
9
9
  def active_options_for_option_value_promotion_rule(promotion_rule)
@@ -2,7 +2,7 @@ module Spree
2
2
  module Admin
3
3
  module TranslationsHelper
4
4
  def link_to_edit_translations(resource, options = {})
5
- return unless Rails.application.config.spree.translatable_resources.map(&:name).include?(resource.class.name)
5
+ return unless Spree.translatable_resources.map(&:name).include?(resource.class.name)
6
6
  return unless can?(:update, resource)
7
7
  return unless can?(:update, :translations)
8
8
 
@@ -15,7 +15,6 @@ import "mapkick/bundle"
15
15
 
16
16
  // Helpers
17
17
  import 'spree/admin/helpers/tinymce'
18
- import 'spree/admin/helpers/canvas'
19
18
  import 'spree/admin/helpers/trix/video_embed'
20
19
  import 'spree/admin/helpers/bootstrap'
21
20
 
@@ -76,6 +75,7 @@ import SearchPickerController from 'spree/admin/controllers/search_picker_contro
76
75
  import SectionFormController from 'spree/admin/controllers/section_form_controller'
77
76
  import SelectController from 'spree/admin/controllers/select_controller'
78
77
  import SeoFormController from 'spree/admin/controllers/seo_form_controller'
78
+ import SidebarController from 'spree/admin/controllers/sidebar_controller'
79
79
  import SlugFormController from 'spree/admin/controllers/slug_form_controller'
80
80
  import StickyController from 'spree/admin/controllers/sticky_controller'
81
81
  import SortableAutoSubmit from 'spree/admin/controllers/sortable_auto_submit_controller'
@@ -141,6 +141,7 @@ application.register('search-picker', SearchPickerController)
141
141
  application.register('section-form', SectionFormController)
142
142
  application.register('select', SelectController)
143
143
  application.register('seo-form', SeoFormController)
144
+ application.register('sidebar', SidebarController)
144
145
  application.register('slug-form', SlugFormController)
145
146
  application.register('sticky', StickyController)
146
147
  application.register('sortable', Sortable)
@@ -9,24 +9,44 @@ import {
9
9
  import { Controller } from "@hotwired/stimulus"
10
10
 
11
11
  export default class extends Controller {
12
- static targets = ["menu", "toggle"]
13
12
  static values = {
14
13
  placement: { type: String, default: "bottom-start" },
15
14
  offset: { type: Number, default: 4 },
15
+ portal: { type: Boolean, default: true },
16
16
  }
17
17
 
18
18
  connect() {
19
+ // Find menu and toggle elements by CSS class instead of Stimulus target
20
+ // Try both .dropdown-menu and .dropdown-container for backward compatibility
21
+ this.menu = this.element.querySelector('.dropdown-menu') ||
22
+ this.element.querySelector('.dropdown-container')
23
+ this.toggleBtn = this.element.querySelector('.dropdown-toggle')
24
+
25
+ // Early return if no menu element exists
26
+ if (!this.menu) {
27
+ return
28
+ }
29
+
19
30
  this._cleanup = null
20
31
  this.boundUpdate = this.update.bind(this)
21
32
  this._isOpen = false
22
33
  this._toggleElement = null
34
+ this._originalParent = null
35
+ this._originalNextSibling = null
36
+ this._movedToBody = false
23
37
  }
24
38
 
25
39
  disconnect() {
40
+ if (!this.menu) return
26
41
  this.stopAutoUpdate()
42
+ this.restoreMenuPosition()
27
43
  }
28
44
 
29
45
  toggle(event) {
46
+ if (!this.menu) {
47
+ return
48
+ }
49
+
30
50
  event.preventDefault()
31
51
  event.stopPropagation()
32
52
 
@@ -41,9 +61,19 @@ export default class extends Controller {
41
61
  }
42
62
 
43
63
  open() {
44
- if (this._isOpen) return
64
+ if (!this.menu || this._isOpen) {
65
+ return
66
+ }
45
67
 
46
- this.menuTarget.classList.remove("hidden")
68
+ // Move menu to body on first open to prevent clipping by sidebar overflow
69
+ // Skip if portal is disabled or if inside bulk panel (to preserve Stimulus controller context)
70
+ if (!this._movedToBody && this.shouldPortalToBody()) {
71
+ this.moveMenuToBody()
72
+ this._movedToBody = true
73
+ }
74
+
75
+ this.menu.classList.remove("hidden")
76
+ this.menu.style.display = "block"
47
77
  this._isOpen = true
48
78
 
49
79
  // Start automatic positioning
@@ -59,9 +89,10 @@ export default class extends Controller {
59
89
  }
60
90
 
61
91
  close() {
62
- if (!this._isOpen) return
92
+ if (!this.menu || !this._isOpen) return
63
93
 
64
- this.menuTarget.classList.add("hidden")
94
+ this.menu.classList.add("hidden")
95
+ this.menu.style.display = ""
65
96
  this._isOpen = false
66
97
 
67
98
  // Stop automatic positioning
@@ -101,11 +132,11 @@ export default class extends Controller {
101
132
  }
102
133
 
103
134
  startAutoUpdate() {
104
- if (!this._cleanup) {
105
- const referenceElement = this.hasToggleTarget ? this.toggleTarget : (this._toggleElement || this.element)
135
+ if (!this._cleanup && this.menu) {
136
+ const referenceElement = this.toggleBtn || this._toggleElement || this.element
106
137
  this._cleanup = autoUpdate(
107
138
  referenceElement,
108
- this.menuTarget,
139
+ this.menu,
109
140
  this.boundUpdate,
110
141
  )
111
142
  }
@@ -119,10 +150,12 @@ export default class extends Controller {
119
150
  }
120
151
 
121
152
  update() {
122
- // Use the toggle target if available, or the stored toggle element, or fall back to the controller element
123
- const referenceElement = this.hasToggleTarget ? this.toggleTarget : (this._toggleElement || this.element)
153
+ if (!this.menu) return
154
+
155
+ // Use the toggle button if available, or the stored toggle element, or fall back to the controller element
156
+ const referenceElement = this.toggleBtn || this._toggleElement || this.element
124
157
 
125
- computePosition(referenceElement, this.menuTarget, {
158
+ computePosition(referenceElement, this.menu, {
126
159
  placement: this.placementValue,
127
160
  middleware: [
128
161
  offset(this.offsetValue),
@@ -133,9 +166,18 @@ export default class extends Controller {
133
166
  shift({ padding: 8 }),
134
167
  size({
135
168
  apply({ availableWidth, availableHeight, elements }) {
136
- // Ensure dropdown doesn't exceed viewport
169
+ // Get the element's computed max-width
170
+ const computedStyle = window.getComputedStyle(elements.floating)
171
+ const originalMaxWidth = parseFloat(computedStyle.maxWidth)
172
+
173
+ // Use the smaller of availableWidth or original max-width
174
+ const maxWidth = originalMaxWidth && !isNaN(originalMaxWidth)
175
+ ? Math.min(availableWidth, originalMaxWidth)
176
+ : availableWidth
177
+
178
+ // Ensure dropdown doesn't exceed viewport or original constraints
137
179
  Object.assign(elements.floating.style, {
138
- maxWidth: `${availableWidth}px`,
180
+ maxWidth: `${maxWidth}px`,
139
181
  maxHeight: `${availableHeight}px`,
140
182
  overflow: "auto",
141
183
  })
@@ -144,11 +186,40 @@ export default class extends Controller {
144
186
  }),
145
187
  ],
146
188
  }).then(({ x, y }) => {
147
- Object.assign(this.menuTarget.style, {
189
+ Object.assign(this.menu.style, {
148
190
  left: `${x}px`,
149
191
  top: `${y}px`,
150
192
  position: "absolute",
151
193
  })
152
194
  })
153
195
  }
196
+
197
+ shouldPortalToBody() {
198
+ // Don't portal if explicitly disabled via data attribute
199
+ return this.portalValue
200
+ }
201
+
202
+ moveMenuToBody() {
203
+ if (this.menu && this.menu.parentNode !== document.body) {
204
+ // Save original position for restoration
205
+ this._originalParent = this.menu.parentNode
206
+ this._originalNextSibling = this.menu.nextSibling
207
+
208
+ // Move menu to body to prevent clipping by sidebar overflow
209
+ document.body.appendChild(this.menu)
210
+ }
211
+ }
212
+
213
+ restoreMenuPosition() {
214
+ if (this.menu && this._originalParent) {
215
+ // Restore menu to original position
216
+ if (this._originalNextSibling) {
217
+ this._originalParent.insertBefore(this.menu, this._originalNextSibling)
218
+ } else {
219
+ this._originalParent.appendChild(this.menu)
220
+ }
221
+ this._originalParent = null
222
+ this._originalNextSibling = null
223
+ }
224
+ }
154
225
  }