spree_admin 5.2.0.rc2 → 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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/spree/admin/application.scss +1 -1
- data/app/assets/stylesheets/spree/admin/components/_alerts.scss +1 -1
- data/app/assets/stylesheets/spree/admin/components/_buttons.scss +5 -4
- data/app/assets/stylesheets/spree/admin/components/_dialogs.scss +0 -1
- data/app/assets/stylesheets/spree/admin/components/_dropdowns.scss +6 -0
- data/app/assets/stylesheets/spree/admin/components/_main.scss +2 -233
- data/app/assets/stylesheets/spree/admin/components/_sidebar.scss +693 -0
- data/app/assets/stylesheets/spree/admin/components/_tables.scss +2 -2
- data/app/assets/stylesheets/spree/admin/components/_variants_form.scss +1 -2
- data/app/assets/stylesheets/spree/admin/global/_variables.scss +15 -12
- data/app/assets/stylesheets/spree/admin/shared/_base.scss +9 -3
- data/app/assets/stylesheets/spree/admin/shared/_forms.scss +5 -6
- data/app/assets/stylesheets/spree/admin/views/_dashboard.scss +14 -0
- data/app/controllers/spree/admin/admin_users_controller.rb +0 -2
- data/app/controllers/spree/admin/checkouts_controller.rb +1 -4
- data/app/controllers/spree/admin/coupon_codes_controller.rb +0 -14
- data/app/controllers/spree/admin/customer_returns_controller.rb +0 -13
- data/app/controllers/spree/admin/digital_assets_controller.rb +2 -2
- data/app/controllers/spree/admin/exports_controller.rb +2 -9
- data/app/controllers/spree/admin/gift_cards_controller.rb +7 -14
- data/app/controllers/spree/admin/integrations_controller.rb +1 -1
- data/app/controllers/spree/admin/invitations_controller.rb +0 -2
- data/app/controllers/spree/admin/metafields_controller.rb +1 -1
- data/app/controllers/spree/admin/oauth_applications_controller.rb +0 -10
- data/app/controllers/spree/admin/option_types_controller.rb +0 -10
- data/app/controllers/spree/admin/orders_controller.rb +1 -1
- data/app/controllers/spree/admin/page_blocks_controller.rb +1 -1
- data/app/controllers/spree/admin/pages_controller.rb +1 -1
- data/app/controllers/spree/admin/payment_methods_controller.rb +1 -11
- data/app/controllers/spree/admin/policies_controller.rb +4 -0
- data/app/controllers/spree/admin/posts_controller.rb +2 -10
- data/app/controllers/spree/admin/promotion_actions_controller.rb +1 -1
- data/app/controllers/spree/admin/promotion_rules_controller.rb +1 -1
- data/app/controllers/spree/admin/promotions_controller.rb +1 -1
- data/app/controllers/spree/admin/properties_controller.rb +0 -12
- data/app/controllers/spree/admin/reports_controller.rb +1 -1
- data/app/controllers/spree/admin/resource_controller.rb +27 -17
- data/app/controllers/spree/admin/return_authorizations_controller.rb +0 -10
- data/app/controllers/spree/admin/shipping_methods_controller.rb +4 -0
- data/app/controllers/spree/admin/stock_items_controller.rb +8 -11
- data/app/controllers/spree/admin/stock_locations_controller.rb +1 -1
- data/app/controllers/spree/admin/stock_transfers_controller.rb +0 -10
- data/app/controllers/spree/admin/store_credits_controller.rb +35 -35
- data/app/controllers/spree/admin/taxonomies_controller.rb +0 -10
- data/app/controllers/spree/admin/taxons_controller.rb +1 -1
- data/app/controllers/spree/admin/themes_controller.rb +6 -2
- data/app/controllers/spree/admin/translations_controller.rb +1 -1
- data/app/controllers/spree/admin/users_controller.rb +7 -17
- data/app/controllers/spree/admin/webhooks_subscribers_controller.rb +0 -10
- data/app/controllers/spree/admin/zones_controller.rb +0 -7
- data/app/helpers/spree/admin/base_helper.rb +1 -1
- data/app/helpers/spree/admin/drawer_helper.rb +6 -6
- data/app/helpers/spree/admin/dropdown_helper.rb +26 -16
- data/app/helpers/spree/admin/modal_helper.rb +2 -0
- data/app/helpers/spree/admin/navigation_helper.rb +47 -4
- data/app/helpers/spree/admin/orders_filters_helper.rb +3 -0
- data/app/helpers/spree/admin/promotion_actions_helper.rb +1 -1
- data/app/helpers/spree/admin/promotion_rules_helper.rb +1 -1
- data/app/helpers/spree/admin/translations_helper.rb +1 -1
- data/app/javascript/spree/admin/application.js +2 -1
- data/app/javascript/spree/admin/controllers/dropdown_controller.js +85 -14
- data/app/javascript/spree/admin/controllers/sidebar_controller.js +231 -0
- data/app/javascript/spree/admin/controllers/tooltip_controller.js +84 -31
- data/app/models/spree/admin/form_builder.rb +76 -17
- data/app/models/spree/admin/navigation/builder.rb +82 -0
- data/app/models/spree/admin/navigation/item.rb +177 -0
- data/app/models/spree/admin/navigation.rb +193 -0
- data/app/views/layouts/spree/admin.html.erb +1 -1
- data/app/views/spree/admin/assets/edit.html.erb +1 -1
- data/app/views/spree/admin/custom_domains/_custom_domains.html.erb +1 -1
- data/app/views/spree/admin/custom_domains/_form.html.erb +2 -14
- data/app/views/spree/admin/digital_assets/_table.html.erb +1 -1
- data/app/views/spree/admin/gift_cards/_filters.html.erb +25 -16
- data/app/views/spree/admin/gift_cards/index.html.erb +1 -1
- data/app/views/spree/admin/integrations/index.html.erb +20 -8
- data/app/views/spree/admin/invitations/new.html.erb +2 -1
- data/app/views/spree/admin/metafield_definitions/_filters.html.erb +1 -1
- data/app/views/spree/admin/newsletter_subscribers/_filters.html.erb +1 -1
- data/app/views/spree/admin/newsletter_subscribers/_table_header.html.erb +2 -2
- data/app/views/spree/admin/oauth_applications/_table_header.html.erb +1 -1
- data/app/views/spree/admin/orders/_customer.html.erb +3 -3
- data/app/views/spree/admin/orders/_filters.html.erb +33 -25
- data/app/views/spree/admin/orders/_header.html.erb +0 -5
- data/app/views/spree/admin/orders/_list.html.erb +3 -3
- data/app/views/spree/admin/orders/_table_filter_dropdown.html.erb +1 -1
- data/app/views/spree/admin/page_blocks/edit.html.erb +3 -3
- data/app/views/spree/admin/page_blocks/forms/_image.html.erb +2 -5
- data/app/views/spree/admin/page_builder/_add_block.html.erb +1 -1
- data/app/views/spree/admin/page_builder/_header.html.erb +1 -1
- data/app/views/spree/admin/page_builder/_pages_dropdown.html.erb +2 -2
- data/app/views/spree/admin/page_builder/_sidebar_block.html.erb +1 -1
- data/app/views/spree/admin/page_builder/_sidebar_colors.html.erb +2 -2
- data/app/views/spree/admin/page_builder/_sidebar_fonts.html.erb +3 -3
- data/app/views/spree/admin/page_builder/_sidebar_section.html.erb +1 -1
- data/app/views/spree/admin/page_links/_form.html.erb +4 -13
- data/app/views/spree/admin/page_links/_list.html.erb +1 -1
- data/app/views/spree/admin/page_links/edit.html.erb +1 -1
- data/app/views/spree/admin/page_sections/edit.html.erb +3 -3
- data/app/views/spree/admin/page_sections/forms/_header.html.erb +0 -2
- data/app/views/spree/admin/page_sections/new.html.erb +1 -1
- data/app/views/spree/admin/pages/_table_header.html.erb +3 -3
- data/app/views/spree/admin/payment_methods/index.html.erb +5 -1
- data/app/views/spree/admin/payments/_payment.html.erb +7 -0
- data/app/views/spree/admin/policies/_filters.html.erb +1 -1
- data/app/views/spree/admin/posts/_form.html.erb +1 -4
- data/app/views/spree/admin/posts/filters.html.erb +18 -8
- data/app/views/spree/admin/products/_bulk_operations.html.erb +2 -2
- data/app/views/spree/admin/products/_filters.html.erb +17 -6
- data/app/views/spree/admin/products/_table_filter_dropdown.html.erb +1 -1
- data/app/views/spree/admin/products/edit.html.erb +0 -2
- data/app/views/spree/admin/products/form/_status.html.erb +0 -3
- data/app/views/spree/admin/products/form/_variants.html.erb +1 -1
- data/app/views/spree/admin/profile/edit.html.erb +9 -61
- data/app/views/spree/admin/promotions/_filters.html.erb +23 -13
- data/app/views/spree/admin/promotions/_table_filter_dropdown.html.erb +1 -1
- data/app/views/spree/admin/promotions/_table_header.html.erb +1 -1
- data/app/views/spree/admin/promotions/form/_kind.html.erb +4 -4
- data/app/views/spree/admin/promotions/form/_settings.html.erb +2 -13
- data/app/views/spree/admin/refund_reasons/_table_header.html.erb +1 -1
- data/app/views/spree/admin/refunds/_form.html.erb +1 -9
- data/app/views/spree/admin/reimbursement_types/_table_header.html.erb +1 -1
- data/app/views/spree/admin/return_authorization_reasons/_table_header.html.erb +1 -1
- data/app/views/spree/admin/return_authorizations/filters.html.erb +1 -1
- data/app/views/spree/admin/roles/index.html.erb +1 -1
- data/app/views/spree/admin/shared/_audit_nav.html.erb +2 -0
- data/app/views/spree/admin/shared/_calendar_range_picker.html.erb +2 -2
- data/app/views/spree/admin/shared/_content_header.html.erb +5 -2
- data/app/views/spree/admin/shared/_developers_nav.html.erb +2 -4
- data/app/views/spree/admin/shared/_header.html.erb +5 -7
- data/app/views/spree/admin/shared/_index_table.html.erb +5 -4
- data/app/views/spree/admin/shared/_index_table_options.html.erb +1 -1
- data/app/views/spree/admin/shared/_navigation.html.erb +5 -0
- data/app/views/spree/admin/shared/_navigation_item.html.erb +64 -0
- data/app/views/spree/admin/shared/_new_item_dropdown.html.erb +2 -2
- data/app/views/spree/admin/shared/_page_section_image.html.erb +2 -5
- data/app/views/spree/admin/shared/_page_section_logo.html.erb +1 -1
- data/app/views/spree/admin/shared/_returns_and_refunds_nav.html.erb +2 -3
- data/app/views/spree/admin/shared/_shipping_nav.html.erb +3 -2
- data/app/views/spree/admin/shared/_sidebar.html.erb +33 -7
- data/app/views/spree/admin/shared/_stock_nav.html.erb +6 -3
- data/app/views/spree/admin/shared/_tax_nav.html.erb +1 -2
- data/app/views/spree/admin/shared/_team_nav.html.erb +2 -3
- data/app/views/spree/admin/shared/_user_dropdown.html.erb +29 -19
- data/app/views/spree/admin/shared/sidebar/_customers_nav.html.erb +7 -0
- data/app/views/spree/admin/shared/sidebar/_orders_nav.html.erb +22 -2
- data/app/views/spree/admin/shared/sidebar/_products_nav.html.erb +21 -0
- data/app/views/spree/admin/shared/sidebar/_promotions_nav.html.erb +8 -0
- data/app/views/spree/admin/shared/sidebar/_returns_nav.html.erb +12 -0
- data/app/views/spree/admin/shared/sidebar/_store_dropdown.html.erb +4 -2
- data/app/views/spree/admin/shared/sidebar/_store_nav.html.erb +15 -1
- data/app/views/spree/admin/shared/sidebar/_storefront_nav.html.erb +25 -3
- data/app/views/spree/admin/shared/sortable_tree/_taxonomy.html.erb +1 -1
- data/app/views/spree/admin/shipping_categories/_table_header.html.erb +1 -1
- data/app/views/spree/admin/shipping_methods/_table_header.html.erb +1 -1
- data/app/views/spree/admin/stock_items/_filters.html.erb +18 -8
- data/app/views/spree/admin/stock_locations/_table_header.html.erb +2 -2
- data/app/views/spree/admin/stock_locations/_table_row.html.erb +1 -1
- data/app/views/spree/admin/stock_transfers/_filters.html.erb +19 -9
- data/app/views/spree/admin/store_credit_categories/index.html.erb +1 -1
- data/app/views/spree/admin/store_credits/_list.html.erb +3 -3
- data/app/views/spree/admin/storefront/edit.html.erb +2 -14
- data/app/views/spree/admin/stores/form/_basic.html.erb +2 -8
- data/app/views/spree/admin/stores/form/_checkout.html.erb +2 -2
- data/app/views/spree/admin/stores/form/_checkout_links.html.erb +1 -1
- data/app/views/spree/admin/stores/form/_emails.html.erb +1 -1
- data/app/views/spree/admin/tax_categories/_table_header.html.erb +2 -2
- data/app/views/spree/admin/tax_rates/_form.html.erb +1 -10
- data/app/views/spree/admin/tax_rates/_table_header.html.erb +2 -2
- data/app/views/spree/admin/taxonomies/_table_header.html.erb +1 -1
- data/app/views/spree/admin/taxons/_form.html.erb +2 -9
- data/app/views/spree/admin/themes/_theme.html.erb +1 -1
- data/app/views/spree/admin/translations/translation_rows/_permalink_field_row.html.erb +1 -12
- data/app/views/spree/admin/users/_filters.html.erb +26 -17
- data/app/views/spree/admin/users/index.html.erb +1 -1
- data/config/initializers/spree_admin_navigation.rb +510 -0
- data/config/locales/en.yml +6 -0
- data/lib/generators/spree/admin/scaffold/templates/controller.rb.tt +3 -1
- data/lib/generators/spree/admin/scaffold/templates/views/_filters.html.erb.tt +1 -1
- data/lib/generators/spree/admin/scaffold/templates/views/_table_header.html.erb.tt +2 -2
- data/lib/generators/spree/admin/scaffold/templates/views/_table_row.html.erb.tt +8 -6
- data/lib/spree/admin/engine.rb +64 -2
- data/lib/spree/admin/runtime_configuration.rb +1 -0
- data/lib/spree/admin.rb +20 -0
- metadata +17 -15
- data/app/assets/stylesheets/spree/admin/components/_offcanvas.scss +0 -26
- data/app/javascript/spree/admin/helpers/canvas.js +0 -29
- data/app/views/spree/admin/shared/_offcanvas_nav.html.erb +0 -12
|
@@ -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)
|
|
64
|
+
if (!this.menu || this._isOpen) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
45
67
|
|
|
46
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
123
|
-
|
|
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.
|
|
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
|
-
//
|
|
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: `${
|
|
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.
|
|
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
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
import {
|
|
3
|
+
computePosition,
|
|
4
|
+
flip,
|
|
5
|
+
shift,
|
|
6
|
+
offset,
|
|
7
|
+
autoUpdate,
|
|
8
|
+
} from "@floating-ui/dom";
|
|
9
|
+
|
|
10
|
+
export default class extends Controller {
|
|
11
|
+
static targets = ["desktop", "mobile"];
|
|
12
|
+
|
|
13
|
+
static values = {
|
|
14
|
+
storageKey: { type: String, default: "spree_admin_sidebar_open" },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
connect() {
|
|
18
|
+
// Initialize dropdown tracking
|
|
19
|
+
this.activeDropdown = null
|
|
20
|
+
this.dropdownCleanup = null
|
|
21
|
+
this.submenuHoverHandlers = new Map()
|
|
22
|
+
this.hideTimeout = null
|
|
23
|
+
|
|
24
|
+
// Disable transitions during initial load to prevent animation flash
|
|
25
|
+
this.element.classList.add("sidebar-no-transition");
|
|
26
|
+
|
|
27
|
+
// Restore saved state for desktop sidebar (default is open/expanded)
|
|
28
|
+
const savedState = localStorage.getItem(this.storageKeyValue);
|
|
29
|
+
|
|
30
|
+
// Only collapse if explicitly saved as false
|
|
31
|
+
if (savedState === "false") {
|
|
32
|
+
this.element.classList.add("sidebar-collapsed");
|
|
33
|
+
this.setupCollapsedSubmenuHandlers();
|
|
34
|
+
} else {
|
|
35
|
+
// Explicitly ensure it's not collapsed (default is expanded)
|
|
36
|
+
this.element.classList.remove("sidebar-collapsed");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Re-enable transitions after initial state is set
|
|
40
|
+
requestAnimationFrame(() => {
|
|
41
|
+
this.element.classList.remove("sidebar-no-transition");
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
disconnect() {
|
|
46
|
+
// Clear any pending hide timeout
|
|
47
|
+
if (this.hideTimeout) {
|
|
48
|
+
clearTimeout(this.hideTimeout);
|
|
49
|
+
this.hideTimeout = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.cleanupSubmenuHandlers();
|
|
53
|
+
if (this.dropdownCleanup) {
|
|
54
|
+
this.dropdownCleanup();
|
|
55
|
+
this.dropdownCleanup = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
toggle() {
|
|
60
|
+
this.element.classList.toggle("sidebar-collapsed");
|
|
61
|
+
const isCollapsed = this.element.classList.contains("sidebar-collapsed");
|
|
62
|
+
localStorage.setItem(this.storageKeyValue, String(!isCollapsed));
|
|
63
|
+
|
|
64
|
+
// Setup or cleanup submenu handlers based on state
|
|
65
|
+
if (isCollapsed) {
|
|
66
|
+
this.setupCollapsedSubmenuHandlers();
|
|
67
|
+
} else {
|
|
68
|
+
this.cleanupSubmenuHandlers();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
openMobile() {
|
|
73
|
+
if (this.hasMobileTarget) {
|
|
74
|
+
this.mobileTarget.classList.add("sidebar-mobile-open");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
closeMobile() {
|
|
79
|
+
if (this.hasMobileTarget) {
|
|
80
|
+
this.mobileTarget.classList.remove("sidebar-mobile-open");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setupCollapsedSubmenuHandlers() {
|
|
85
|
+
const sidebar = document.querySelector('#main-sidebar');
|
|
86
|
+
if (!sidebar) return;
|
|
87
|
+
|
|
88
|
+
// Find all dropdown submenus
|
|
89
|
+
const dropdowns = sidebar.querySelectorAll('.nav-submenu-dropdown');
|
|
90
|
+
|
|
91
|
+
dropdowns.forEach(dropdown => {
|
|
92
|
+
// Find the associated nav-item (previous sibling, skipping the regular nav-submenu)
|
|
93
|
+
let navItem = dropdown.previousElementSibling;
|
|
94
|
+
|
|
95
|
+
// Skip over the regular .nav-submenu to get to the .nav-item
|
|
96
|
+
while (navItem && navItem.classList.contains('nav-submenu')) {
|
|
97
|
+
navItem = navItem.previousElementSibling;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!navItem || !navItem.classList.contains('nav-item')) return;
|
|
101
|
+
|
|
102
|
+
const showHandler = () => this.showSubmenuFloating(navItem, dropdown);
|
|
103
|
+
const hideHandler = () => this.scheduleHideSubmenu();
|
|
104
|
+
|
|
105
|
+
navItem.addEventListener('mouseenter', showHandler);
|
|
106
|
+
navItem.addEventListener('mouseleave', hideHandler);
|
|
107
|
+
dropdown.addEventListener('mouseenter', showHandler);
|
|
108
|
+
dropdown.addEventListener('mouseleave', hideHandler);
|
|
109
|
+
|
|
110
|
+
this.submenuHoverHandlers.set(navItem, { showHandler, hideHandler, dropdown });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
cleanupSubmenuHandlers() {
|
|
115
|
+
this.submenuHoverHandlers.forEach(({ showHandler, hideHandler, dropdown }, navItem) => {
|
|
116
|
+
navItem.removeEventListener('mouseenter', showHandler);
|
|
117
|
+
navItem.removeEventListener('mouseleave', hideHandler);
|
|
118
|
+
dropdown.removeEventListener('mouseenter', showHandler);
|
|
119
|
+
dropdown.removeEventListener('mouseleave', hideHandler);
|
|
120
|
+
});
|
|
121
|
+
this.submenuHoverHandlers.clear();
|
|
122
|
+
|
|
123
|
+
// Hide active dropdown if any
|
|
124
|
+
if (this.activeDropdown) {
|
|
125
|
+
this.hideSubmenuFloating();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
showSubmenuFloating(navItem, dropdown) {
|
|
130
|
+
// Cancel any pending hide
|
|
131
|
+
if (this.hideTimeout) {
|
|
132
|
+
clearTimeout(this.hideTimeout);
|
|
133
|
+
this.hideTimeout = null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// If showing same dropdown, return
|
|
137
|
+
if (this.activeDropdown === dropdown) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Hide any currently active dropdown
|
|
142
|
+
if (this.activeDropdown) {
|
|
143
|
+
this.hideSubmenuFloating();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Store reference
|
|
147
|
+
this.activeDropdown = dropdown;
|
|
148
|
+
|
|
149
|
+
// Move dropdown to body for proper positioning (to avoid sidebar overflow clipping)
|
|
150
|
+
if (dropdown.parentNode !== document.body) {
|
|
151
|
+
dropdown._originalParent = dropdown.parentNode;
|
|
152
|
+
dropdown._originalNextSibling = dropdown.nextSibling;
|
|
153
|
+
document.body.appendChild(dropdown);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Show dropdown
|
|
157
|
+
dropdown.classList.remove('d-none');
|
|
158
|
+
|
|
159
|
+
// Style dropdown items
|
|
160
|
+
dropdown.querySelectorAll('.nav-link').forEach((item, index) => {
|
|
161
|
+
item.classList.add('dropdown-item');
|
|
162
|
+
if (index > 0) {
|
|
163
|
+
item.classList.add('mt-1');
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Position using Floating UI
|
|
168
|
+
if (this.dropdownCleanup) {
|
|
169
|
+
this.dropdownCleanup();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.dropdownCleanup = autoUpdate(navItem, dropdown, () => {
|
|
173
|
+
computePosition(navItem, dropdown, {
|
|
174
|
+
placement: 'right-start',
|
|
175
|
+
middleware: [
|
|
176
|
+
offset(8),
|
|
177
|
+
flip(),
|
|
178
|
+
shift({ padding: 8 }),
|
|
179
|
+
],
|
|
180
|
+
}).then(({ x, y }) => {
|
|
181
|
+
Object.assign(dropdown.style, {
|
|
182
|
+
left: `${x}px`,
|
|
183
|
+
top: `${y}px`,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
scheduleHideSubmenu() {
|
|
190
|
+
// Clear any existing timeout
|
|
191
|
+
if (this.hideTimeout) {
|
|
192
|
+
clearTimeout(this.hideTimeout);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Schedule hide with delay to allow mouse to move to submenu
|
|
196
|
+
this.hideTimeout = setTimeout(() => {
|
|
197
|
+
this.hideSubmenuFloating();
|
|
198
|
+
this.hideTimeout = null;
|
|
199
|
+
}, 150); // 150ms grace period
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
hideSubmenuFloating() {
|
|
203
|
+
if (!this.activeDropdown) return;
|
|
204
|
+
|
|
205
|
+
// Stop auto-update
|
|
206
|
+
if (this.dropdownCleanup) {
|
|
207
|
+
this.dropdownCleanup();
|
|
208
|
+
this.dropdownCleanup = null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Hide dropdown
|
|
212
|
+
this.activeDropdown.classList.add('d-none');
|
|
213
|
+
|
|
214
|
+
// Restore dropdown to original position in sidebar
|
|
215
|
+
if (this.activeDropdown._originalParent) {
|
|
216
|
+
if (this.activeDropdown._originalNextSibling) {
|
|
217
|
+
this.activeDropdown._originalParent.insertBefore(
|
|
218
|
+
this.activeDropdown,
|
|
219
|
+
this.activeDropdown._originalNextSibling
|
|
220
|
+
);
|
|
221
|
+
} else {
|
|
222
|
+
this.activeDropdown._originalParent.appendChild(this.activeDropdown);
|
|
223
|
+
}
|
|
224
|
+
this.activeDropdown._originalParent = null;
|
|
225
|
+
this.activeDropdown._originalNextSibling = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Clear reference
|
|
229
|
+
this.activeDropdown = null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
import { Controller } from "@hotwired/stimulus"
|
|
9
9
|
|
|
10
10
|
export default class extends Controller {
|
|
11
|
-
static targets = ["tooltip"]
|
|
12
11
|
static values = {
|
|
13
12
|
placement: { type: String, default: "top" },
|
|
14
13
|
offset: { type: Number, default: 10 },
|
|
@@ -17,18 +16,34 @@ export default class extends Controller {
|
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
connect() {
|
|
19
|
+
// Find tooltip element by CSS class instead of Stimulus target
|
|
20
|
+
this.tooltip = this.element.querySelector('.tooltip-container')
|
|
21
|
+
|
|
22
|
+
// Early return if no tooltip element exists
|
|
23
|
+
if (!this.tooltip) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
this._cleanup = null
|
|
21
28
|
this.boundUpdate = this.update.bind(this)
|
|
22
29
|
this._originalWidth = null
|
|
23
30
|
this._isShown = false
|
|
31
|
+
this._originalParent = null
|
|
32
|
+
this._originalNextSibling = null
|
|
33
|
+
this._movedToBody = false
|
|
24
34
|
this.startAutoUpdate()
|
|
25
35
|
this.addEventListeners()
|
|
26
36
|
this.prepareTooltip()
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
disconnect() {
|
|
40
|
+
if (!this.tooltip) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
this.removeEventListeners()
|
|
31
45
|
this.stopAutoUpdate()
|
|
46
|
+
this.restoreTooltipPosition()
|
|
32
47
|
this.resetTooltip()
|
|
33
48
|
}
|
|
34
49
|
|
|
@@ -44,43 +59,75 @@ export default class extends Controller {
|
|
|
44
59
|
|
|
45
60
|
prepareTooltip() {
|
|
46
61
|
// Ensure tooltip is rendered offscreen but measurable
|
|
47
|
-
if (this.
|
|
62
|
+
if (this.tooltip) {
|
|
48
63
|
// Save original display
|
|
49
|
-
this._originalDisplay = this.
|
|
64
|
+
this._originalDisplay = this.tooltip.style.display
|
|
50
65
|
// Temporarily show tooltip offscreen to measure size
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
66
|
+
this.tooltip.style.visibility = "hidden"
|
|
67
|
+
this.tooltip.style.display = "block"
|
|
68
|
+
this.tooltip.style.left = "-9999px"
|
|
69
|
+
this.tooltip.style.top = "-9999px"
|
|
55
70
|
// Force reflow and measure
|
|
56
|
-
this._originalWidth = this.
|
|
71
|
+
this._originalWidth = this.tooltip.offsetWidth + 10
|
|
57
72
|
// Height is now dynamic, so we do not set or store it
|
|
58
|
-
this.
|
|
59
|
-
this.
|
|
73
|
+
this.tooltip.style.width = `${this._originalWidth}px`
|
|
74
|
+
this.tooltip.style.height = "" // Remove any fixed height
|
|
60
75
|
// Hide again
|
|
61
|
-
this.
|
|
62
|
-
this.
|
|
63
|
-
this.
|
|
64
|
-
this.
|
|
76
|
+
this.tooltip.style.display = "none"
|
|
77
|
+
this.tooltip.style.visibility = ""
|
|
78
|
+
this.tooltip.style.left = ""
|
|
79
|
+
this.tooltip.style.top = ""
|
|
65
80
|
}
|
|
66
81
|
}
|
|
67
82
|
|
|
68
83
|
resetTooltip() {
|
|
69
|
-
if (this.
|
|
70
|
-
this.
|
|
71
|
-
this.
|
|
72
|
-
this.
|
|
84
|
+
if (this.tooltip) {
|
|
85
|
+
this.tooltip.style.width = ""
|
|
86
|
+
this.tooltip.style.height = ""
|
|
87
|
+
this.tooltip.style.display = this._originalDisplay || ""
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
moveTooltipToBody() {
|
|
92
|
+
if (this.tooltip && this.tooltip.parentNode !== document.body) {
|
|
93
|
+
// Save original position for restoration
|
|
94
|
+
this._originalParent = this.tooltip.parentNode
|
|
95
|
+
this._originalNextSibling = this.tooltip.nextSibling
|
|
96
|
+
|
|
97
|
+
// Move tooltip to body to prevent clipping by sidebar overflow
|
|
98
|
+
document.body.appendChild(this.tooltip)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
restoreTooltipPosition() {
|
|
103
|
+
if (this.tooltip && this._originalParent) {
|
|
104
|
+
// Restore tooltip to original position
|
|
105
|
+
if (this._originalNextSibling) {
|
|
106
|
+
this._originalParent.insertBefore(this.tooltip, this._originalNextSibling)
|
|
107
|
+
} else {
|
|
108
|
+
this._originalParent.appendChild(this.tooltip)
|
|
109
|
+
}
|
|
110
|
+
this._originalParent = null
|
|
111
|
+
this._originalNextSibling = null
|
|
73
112
|
}
|
|
74
113
|
}
|
|
75
114
|
|
|
76
115
|
show = () => {
|
|
116
|
+
if (!this.tooltip) return
|
|
117
|
+
|
|
77
118
|
if (!this._isShown) {
|
|
78
|
-
|
|
79
|
-
this.
|
|
119
|
+
// Move to body on first show
|
|
120
|
+
if (!this._movedToBody) {
|
|
121
|
+
this.moveTooltipToBody()
|
|
122
|
+
this._movedToBody = true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.tooltip.style.display = "block"
|
|
126
|
+
this.tooltip.style.visibility = "visible"
|
|
80
127
|
// Set explicit width, but let height be dynamic
|
|
81
128
|
if (this._originalWidth) {
|
|
82
|
-
this.
|
|
83
|
-
this.
|
|
129
|
+
this.tooltip.style.width = `${this._originalWidth}px`
|
|
130
|
+
this.tooltip.style.height = ""
|
|
84
131
|
}
|
|
85
132
|
this.update() // Ensure immediate update when shown
|
|
86
133
|
this._isShown = true
|
|
@@ -88,19 +135,21 @@ export default class extends Controller {
|
|
|
88
135
|
}
|
|
89
136
|
|
|
90
137
|
hide = () => {
|
|
138
|
+
if (!this.tooltip) return
|
|
139
|
+
|
|
91
140
|
if (this._isShown) {
|
|
92
|
-
this.
|
|
93
|
-
this.
|
|
141
|
+
this.tooltip.style.display = "none"
|
|
142
|
+
this.tooltip.style.visibility = ""
|
|
94
143
|
// Keep width set to prevent flicker if quickly re-hovered
|
|
95
144
|
this._isShown = false
|
|
96
145
|
}
|
|
97
146
|
}
|
|
98
147
|
|
|
99
148
|
startAutoUpdate() {
|
|
100
|
-
if (!this._cleanup) {
|
|
149
|
+
if (!this._cleanup && this.tooltip) {
|
|
101
150
|
this._cleanup = autoUpdate(
|
|
102
151
|
this.element,
|
|
103
|
-
this.
|
|
152
|
+
this.tooltip,
|
|
104
153
|
this.boundUpdate,
|
|
105
154
|
)
|
|
106
155
|
}
|
|
@@ -114,8 +163,10 @@ export default class extends Controller {
|
|
|
114
163
|
}
|
|
115
164
|
|
|
116
165
|
update() {
|
|
166
|
+
if (!this.tooltip) return
|
|
167
|
+
|
|
117
168
|
// Update position even if not visible, to ensure correct positioning when shown
|
|
118
|
-
computePosition(this.element, this.
|
|
169
|
+
computePosition(this.element, this.tooltip, {
|
|
119
170
|
placement: this.placementValue,
|
|
120
171
|
middleware: [
|
|
121
172
|
offset({
|
|
@@ -127,10 +178,12 @@ export default class extends Controller {
|
|
|
127
178
|
shift({ padding: 5 }),
|
|
128
179
|
],
|
|
129
180
|
}).then(({ x, y }) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
181
|
+
if (this.tooltip) {
|
|
182
|
+
Object.assign(this.tooltip.style, {
|
|
183
|
+
left: `${x}px`,
|
|
184
|
+
top: `${y}px`,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
134
187
|
})
|
|
135
188
|
}
|
|
136
189
|
}
|