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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/spree/admin/application.scss +1 -1
- data/app/assets/stylesheets/spree/admin/components/_dropdowns.scss +2 -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/global/_variables.scss +1 -0
- data/app/assets/stylesheets/spree/admin/shared/_base.scss +7 -1
- data/app/assets/stylesheets/spree/admin/views/_dashboard.scss +14 -0
- data/app/controllers/spree/admin/integrations_controller.rb +1 -1
- data/app/controllers/spree/admin/metafields_controller.rb +1 -1
- data/app/controllers/spree/admin/page_blocks_controller.rb +1 -1
- data/app/controllers/spree/admin/payment_methods_controller.rb +1 -1
- 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/reports_controller.rb +1 -1
- data/app/controllers/spree/admin/taxons_controller.rb +1 -1
- data/app/controllers/spree/admin/translations_controller.rb +1 -1
- 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 +10 -2
- data/app/helpers/spree/admin/modal_helper.rb +2 -0
- data/app/helpers/spree/admin/navigation_helper.rb +46 -3
- data/app/helpers/spree/admin/orders_filters_helper.rb +1 -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 +74 -16
- 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/_form.html.erb +2 -14
- data/app/views/spree/admin/gift_cards/_filters.html.erb +26 -18
- data/app/views/spree/admin/gift_cards/index.html.erb +1 -1
- data/app/views/spree/admin/orders/_customer.html.erb +2 -2
- data/app/views/spree/admin/orders/_filters.html.erb +34 -25
- data/app/views/spree/admin/orders/_table_filter_dropdown.html.erb +1 -1
- data/app/views/spree/admin/page_blocks/forms/_image.html.erb +2 -5
- data/app/views/spree/admin/page_links/_form.html.erb +4 -13
- data/app/views/spree/admin/page_sections/forms/_header.html.erb +0 -2
- data/app/views/spree/admin/payments/_payment.html.erb +7 -0
- 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 +1 -1
- 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/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/form/_settings.html.erb +2 -13
- data/app/views/spree/admin/refunds/_form.html.erb +1 -9
- data/app/views/spree/admin/return_authorizations/filters.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 +1 -1
- 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/_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 +1 -1
- 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 +26 -17
- 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 +3 -1
- 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/stock_items/_filters.html.erb +18 -8
- 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/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/_emails.html.erb +1 -1
- data/app/views/spree/admin/tax_rates/_form.html.erb +1 -10
- 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 +23 -13
- data/config/initializers/spree_admin_navigation.rb +510 -0
- data/config/locales/en.yml +4 -0
- data/lib/generators/spree/admin/scaffold/templates/controller.rb.tt +3 -1
- 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
|
@@ -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
|
}
|
|
@@ -24,13 +24,18 @@ module Spree
|
|
|
24
24
|
# - :required [Boolean] whether field is required
|
|
25
25
|
# - :help [String] help text to display below the field
|
|
26
26
|
# - :help_bubble [String] help bubble text
|
|
27
|
+
# - :prepend [String] text to prepend before the input field
|
|
28
|
+
# - :append [String] text to append after the input field
|
|
27
29
|
# @return [String] HTML string containing the complete form group
|
|
28
30
|
def spree_text_field(method, options = {})
|
|
29
31
|
options[:class] ||= 'form-control'
|
|
32
|
+
prepend = options.delete(:prepend)
|
|
33
|
+
append = options.delete(:append)
|
|
34
|
+
|
|
30
35
|
@template.content_tag(:div, class: 'form-group') do
|
|
31
36
|
@template.label(@object_name, method, get_label(method, options)) +
|
|
32
|
-
@template.text_field(@object_name, method, objectify_options(options)) +
|
|
33
|
-
@template.error_message_on(@object_name, method) +
|
|
37
|
+
wrap_with_input_group(@template.text_field(@object_name, method, objectify_options(options)), prepend, append) +
|
|
38
|
+
@template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
|
|
34
39
|
end
|
|
35
40
|
end
|
|
36
41
|
|
|
@@ -41,10 +46,13 @@ module Spree
|
|
|
41
46
|
# @return [String] HTML string containing the complete form group
|
|
42
47
|
def spree_number_field(method, options = {})
|
|
43
48
|
options[:class] ||= 'form-control'
|
|
49
|
+
prepend = options.delete(:prepend)
|
|
50
|
+
append = options.delete(:append)
|
|
51
|
+
|
|
44
52
|
@template.content_tag(:div, class: 'form-group') do
|
|
45
53
|
@template.label(@object_name, method, get_label(method, options)) +
|
|
46
|
-
@template.number_field(@object_name, method, objectify_options(options)) +
|
|
47
|
-
@template.error_message_on(@object_name, method) +
|
|
54
|
+
wrap_with_input_group(@template.number_field(@object_name, method, objectify_options(options)), prepend, append) +
|
|
55
|
+
@template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
|
|
48
56
|
end
|
|
49
57
|
end
|
|
50
58
|
|
|
@@ -58,7 +66,7 @@ module Spree
|
|
|
58
66
|
@template.content_tag(:div, class: 'form-group') do
|
|
59
67
|
@template.label(@object_name, method, get_label(method, options)) +
|
|
60
68
|
@template.email_field(@object_name, method, objectify_options(options)) +
|
|
61
|
-
@template.error_message_on(@object_name, method) +
|
|
69
|
+
@template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
|
|
62
70
|
end
|
|
63
71
|
end
|
|
64
72
|
|
|
@@ -72,7 +80,7 @@ module Spree
|
|
|
72
80
|
@template.content_tag(:div, class: 'form-group') do
|
|
73
81
|
@template.label(@object_name, method, get_label(method, options)) +
|
|
74
82
|
@template.date_field(@object_name, method, objectify_options(options)) +
|
|
75
|
-
@template.error_message_on(@object_name, method) +
|
|
83
|
+
@template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
|
|
76
84
|
end
|
|
77
85
|
end
|
|
78
86
|
|
|
@@ -86,7 +94,7 @@ module Spree
|
|
|
86
94
|
@template.content_tag(:div, class: 'form-group') do
|
|
87
95
|
@template.label(@object_name, method, get_label(method, options)) +
|
|
88
96
|
@template.datetime_field(@object_name, method, objectify_options(options)) +
|
|
89
|
-
@template.error_message_on(@object_name, method) +
|
|
97
|
+
@template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
|
|
90
98
|
end
|
|
91
99
|
end
|
|
92
100
|
|
|
@@ -107,7 +115,7 @@ module Spree
|
|
|
107
115
|
|
|
108
116
|
@template.label(@object_name, method, get_label(method, options)) +
|
|
109
117
|
@template.text_area(@object_name, method, objectify_options(options)) +
|
|
110
|
-
@template.error_message_on(@object_name, method) +
|
|
118
|
+
@template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
|
|
111
119
|
end
|
|
112
120
|
end
|
|
113
121
|
|
|
@@ -122,7 +130,7 @@ module Spree
|
|
|
122
130
|
@template.content_tag(:div, class: 'trix-container') do
|
|
123
131
|
@template.rich_text_area(@object_name, method, objectify_options(options))
|
|
124
132
|
end +
|
|
125
|
-
@template.error_message_on(@object_name, method) +
|
|
133
|
+
@template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
|
|
126
134
|
end
|
|
127
135
|
end
|
|
128
136
|
|
|
@@ -147,7 +155,7 @@ module Spree
|
|
|
147
155
|
@template.content_tag(:div, class: 'form-group') do
|
|
148
156
|
@template.label(@object_name, method, get_label(method, options)) +
|
|
149
157
|
@template.select(@object_name, method, choices, objectify_options(options), html_options, &block) +
|
|
150
|
-
@template.error_message_on(@object_name, method) +
|
|
158
|
+
@template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
|
|
151
159
|
end
|
|
152
160
|
end
|
|
153
161
|
|
|
@@ -173,7 +181,7 @@ module Spree
|
|
|
173
181
|
@template.content_tag(:div, class: 'form-group') do
|
|
174
182
|
@template.label(@object_name, method, get_label(method, options)) +
|
|
175
183
|
@template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), html_options) +
|
|
176
|
-
@template.error_message_on(@object_name, method) +
|
|
184
|
+
@template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
|
|
177
185
|
end
|
|
178
186
|
end
|
|
179
187
|
|
|
@@ -187,7 +195,7 @@ module Spree
|
|
|
187
195
|
@template.content_tag(:div, class: 'custom-control custom-checkbox') do
|
|
188
196
|
@template.check_box(@object_name, method, objectify_options(options.merge(class: 'custom-control-input'))) +
|
|
189
197
|
@template.label(@object_name, method, get_label(method, options), class: 'custom-control-label')
|
|
190
|
-
end + @template.error_message_on(@object_name, method) +
|
|
198
|
+
end + @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge!(class: 'form-text mt-2 ml-4'))
|
|
191
199
|
end
|
|
192
200
|
end
|
|
193
201
|
|
|
@@ -204,23 +212,43 @@ module Spree
|
|
|
204
212
|
@template.content_tag(:div, class: 'custom-control custom-radio') do
|
|
205
213
|
@template.radio_button(@object_name, method, tag_value, objectify_options(options.merge(class: 'custom-control-input'))) +
|
|
206
214
|
@template.label(@object_name, method, get_label(method, options), class: 'custom-control-label', for: options[:id])
|
|
207
|
-
end + @template.error_message_on(@object_name, method) +
|
|
215
|
+
end + @template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
|
|
208
216
|
end
|
|
209
217
|
end
|
|
210
218
|
|
|
219
|
+
# Create a direct file upload field with Spree form styling
|
|
220
|
+
#
|
|
221
|
+
# @param method [Symbol] the field name
|
|
222
|
+
# @param options [Hash] field options
|
|
223
|
+
# @option options [Boolean] :crop whether to crop the image
|
|
224
|
+
# @option options [Boolean] :auto_submit whether to auto-submit the form when the file is uploaded
|
|
225
|
+
# @option options [Boolean] :can_delete whether to show the delete button
|
|
226
|
+
# @option options [Boolean] :inline whether to display the uploader inline
|
|
227
|
+
# @option options [Integer] :height the height of the uploader
|
|
228
|
+
# @option options [Integer] :width the width of the uploader
|
|
229
|
+
# @option options [Array] :allowed_file_types the allowed file types, defaults to image types
|
|
230
|
+
# @return [String] HTML string containing the complete form group with direct file upload field
|
|
231
|
+
def spree_file_field(method, options = {})
|
|
232
|
+
@template.content_tag(:div, class: 'form-group') do
|
|
233
|
+
@template.label(@object_name, method, get_label(method, options)) +
|
|
234
|
+
@template.render('active_storage/upload_form', form: self, field_name: method, **options) +
|
|
235
|
+
@template.error_message_on(@object_name, method) + spree_field_help(method, options.merge(class: 'form-text mt-2'))
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
|
|
211
241
|
# Generate help text for a field
|
|
212
242
|
#
|
|
213
243
|
# @param _method [Symbol] the field name (unused but kept for consistency)
|
|
214
244
|
# @param options [Hash] field options
|
|
215
245
|
# @option options [String] :help help text to display
|
|
216
246
|
# @return [String] HTML string containing the help text or empty string
|
|
217
|
-
def
|
|
247
|
+
def spree_field_help(_method, options = {})
|
|
218
248
|
options[:class] ||= 'form-text mt-2'
|
|
219
249
|
@template.content_tag(:span, options[:help], class: options[:class])
|
|
220
250
|
end
|
|
221
251
|
|
|
222
|
-
private
|
|
223
|
-
|
|
224
252
|
# Generate the label for a field with required indicator and help bubble
|
|
225
253
|
#
|
|
226
254
|
# @param method [Symbol] the field name
|
|
@@ -246,6 +274,36 @@ module Spree
|
|
|
246
274
|
|
|
247
275
|
@template.raw(translated_label + required_label + help_bubble)
|
|
248
276
|
end
|
|
277
|
+
|
|
278
|
+
# Wrap a field with an input group if prepend or append is specified
|
|
279
|
+
#
|
|
280
|
+
# @param field_html [String] the HTML for the field
|
|
281
|
+
# @param prepend [String, nil] text to prepend before the input field
|
|
282
|
+
# @param append [String, nil] text to append after the input field
|
|
283
|
+
# @return [String] HTML string with input group wrapper or the original field
|
|
284
|
+
def wrap_with_input_group(field_html, prepend = nil, append = nil)
|
|
285
|
+
return field_html if prepend.nil? && append.nil?
|
|
286
|
+
|
|
287
|
+
@template.content_tag(:div, class: 'input-group') do
|
|
288
|
+
prepend_html = if prepend.present?
|
|
289
|
+
@template.content_tag(:div, class: 'input-group-prepend') do
|
|
290
|
+
@template.content_tag(:span, prepend, class: 'input-group-text')
|
|
291
|
+
end
|
|
292
|
+
else
|
|
293
|
+
''.html_safe
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
append_html = if append.present?
|
|
297
|
+
@template.content_tag(:div, class: 'input-group-append') do
|
|
298
|
+
@template.content_tag(:span, append, class: 'input-group-text')
|
|
299
|
+
end
|
|
300
|
+
else
|
|
301
|
+
''.html_safe
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
prepend_html + field_html + append_html
|
|
305
|
+
end
|
|
306
|
+
end
|
|
249
307
|
end
|
|
250
308
|
end
|
|
251
309
|
end
|