@o2vend/theme-cli 1.0.32
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.
- package/README.md +425 -0
- package/assets/Logo_o2vend.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-white.png +0 -0
- package/bin/o2vend +42 -0
- package/config/widget-map.json +50 -0
- package/lib/commands/check.js +201 -0
- package/lib/commands/generate.js +33 -0
- package/lib/commands/init.js +214 -0
- package/lib/commands/optimize.js +216 -0
- package/lib/commands/package.js +208 -0
- package/lib/commands/serve.js +105 -0
- package/lib/commands/validate.js +191 -0
- package/lib/lib/api-client.js +357 -0
- package/lib/lib/dev-server.js +2618 -0
- package/lib/lib/file-watcher.js +80 -0
- package/lib/lib/hot-reload.js +106 -0
- package/lib/lib/liquid-engine.js +822 -0
- package/lib/lib/liquid-filters.js +671 -0
- package/lib/lib/mock-api-server.js +989 -0
- package/lib/lib/mock-data.js +1468 -0
- package/lib/lib/widget-service.js +321 -0
- package/package.json +70 -0
- package/test-theme/README.md +27 -0
- package/test-theme/assets/async-sections.js +446 -0
- package/test-theme/assets/cart-drawer.js +463 -0
- package/test-theme/assets/cart-manager.js +223 -0
- package/test-theme/assets/checkout-price-handler.js +368 -0
- package/test-theme/assets/components.css +4629 -0
- package/test-theme/assets/delivery-zone.css +299 -0
- package/test-theme/assets/delivery-zone.js +396 -0
- package/test-theme/assets/logo.png +0 -0
- package/test-theme/assets/sections.css +48 -0
- package/test-theme/assets/theme.css +3500 -0
- package/test-theme/assets/theme.js +3745 -0
- package/test-theme/config/settings_data.json +292 -0
- package/test-theme/config/settings_schema.json +1050 -0
- package/test-theme/layout/theme.liquid +195 -0
- package/test-theme/locales/en.default.json +260 -0
- package/test-theme/sections/content-fallback.liquid +53 -0
- package/test-theme/sections/content.liquid +57 -0
- package/test-theme/sections/footer-fallback.liquid +328 -0
- package/test-theme/sections/footer.liquid +278 -0
- package/test-theme/sections/header-fallback.liquid +1805 -0
- package/test-theme/sections/header.liquid +1145 -0
- package/test-theme/sections/hero-fallback.liquid +212 -0
- package/test-theme/sections/hero.liquid +136 -0
- package/test-theme/snippets/account-sidebar.liquid +200 -0
- package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
- package/test-theme/snippets/breadcrumbs.liquid +134 -0
- package/test-theme/snippets/cart-drawer.liquid +467 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
- package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
- package/test-theme/snippets/delivery-zone-search.liquid +78 -0
- package/test-theme/snippets/icon.liquid +105 -0
- package/test-theme/snippets/login-modal.liquid +346 -0
- package/test-theme/snippets/mega-menu.liquid +812 -0
- package/test-theme/snippets/news-thumbnail.liquid +187 -0
- package/test-theme/snippets/pagination.liquid +120 -0
- package/test-theme/snippets/price.liquid +92 -0
- package/test-theme/snippets/product-card-related.liquid +78 -0
- package/test-theme/snippets/product-card-simple.liquid +41 -0
- package/test-theme/snippets/product-card.liquid +697 -0
- package/test-theme/snippets/rating.liquid +85 -0
- package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
- package/test-theme/snippets/skeleton-product-card.liquid +124 -0
- package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
- package/test-theme/snippets/social-sharing.liquid +185 -0
- package/test-theme/templates/account/dashboard.liquid +401 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
- package/test-theme/templates/account/loyalty.liquid +588 -0
- package/test-theme/templates/account/order-detail.liquid +230 -0
- package/test-theme/templates/account/orders.liquid +349 -0
- package/test-theme/templates/account/profile.liquid +758 -0
- package/test-theme/templates/account/register.liquid +232 -0
- package/test-theme/templates/account/return-orders.liquid +348 -0
- package/test-theme/templates/account/store-credit.liquid +464 -0
- package/test-theme/templates/account/subscriptions.liquid +601 -0
- package/test-theme/templates/account/wishlist.liquid +419 -0
- package/test-theme/templates/address-book.liquid +1092 -0
- package/test-theme/templates/categories.liquid +452 -0
- package/test-theme/templates/checkout.liquid +4511 -0
- package/test-theme/templates/error.liquid +384 -0
- package/test-theme/templates/index.liquid +11 -0
- package/test-theme/templates/login.liquid +185 -0
- package/test-theme/templates/order-confirmation.liquid +720 -0
- package/test-theme/templates/page.liquid +297 -0
- package/test-theme/templates/product-detail.liquid +4363 -0
- package/test-theme/templates/products.liquid +518 -0
- package/test-theme/templates/search.liquid +922 -0
- package/test-theme/theme.json.example +19 -0
- package/test-theme/widgets/brand-carousel.liquid +676 -0
- package/test-theme/widgets/brand.liquid +245 -0
- package/test-theme/widgets/carousel.liquid +843 -0
- package/test-theme/widgets/category-list-carousel.liquid +656 -0
- package/test-theme/widgets/category-list.liquid +340 -0
- package/test-theme/widgets/category.liquid +475 -0
- package/test-theme/widgets/discount-time.liquid +176 -0
- package/test-theme/widgets/footer-menu.liquid +695 -0
- package/test-theme/widgets/footer.liquid +179 -0
- package/test-theme/widgets/gallery.liquid +271 -0
- package/test-theme/widgets/header-menu.liquid +932 -0
- package/test-theme/widgets/header.liquid +159 -0
- package/test-theme/widgets/html.liquid +214 -0
- package/test-theme/widgets/news.liquid +217 -0
- package/test-theme/widgets/product-canvas.liquid +235 -0
- package/test-theme/widgets/product-carousel.liquid +502 -0
- package/test-theme/widgets/product.liquid +45 -0
- package/test-theme/widgets/recently-viewed.liquid +26 -0
- package/test-theme/widgets/shared/product-grid.liquid +339 -0
- package/test-theme/widgets/simple-product.liquid +42 -0
- package/test-theme/widgets/single-product.liquid +610 -0
- package/test-theme/widgets/spacebar-carousel.liquid +663 -0
- package/test-theme/widgets/spacebar.liquid +279 -0
- package/test-theme/widgets/splash.liquid +378 -0
- package/test-theme/widgets/testimonial-carousel.liquid +709 -0
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Header Menu Widget
|
|
3
|
+
|
|
4
|
+
This widget ONLY handles menu navigation logic:
|
|
5
|
+
- Desktop navigation menu (bottom row)
|
|
6
|
+
- Mobile drawer menu
|
|
7
|
+
- Menu fetching and rendering from API
|
|
8
|
+
{% endcomment %}
|
|
9
|
+
|
|
10
|
+
{% liquid
|
|
11
|
+
assign widget_settings = widget.settings
|
|
12
|
+
assign widget_data = widget.data
|
|
13
|
+
%}
|
|
14
|
+
|
|
15
|
+
{% comment %}Desktop Navigation Menu{% endcomment %}
|
|
16
|
+
<nav class="header-menu-nav" data-widget-id="{{ widget.id }}" data-header-menu-nav role="navigation" aria-label="Main navigation">
|
|
17
|
+
<div class="header-menu-nav__loading" data-menu-loading data-menu-loading-hidden>
|
|
18
|
+
<span>Loading menu...</span>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="header-menu-nav__error" data-menu-error data-menu-error-hidden>
|
|
21
|
+
<span>Menu items unavailable</span>
|
|
22
|
+
</div>
|
|
23
|
+
<ul class="header-menu-nav__list" data-menu-list>
|
|
24
|
+
<!-- Menu items will be dynamically inserted here -->
|
|
25
|
+
</ul>
|
|
26
|
+
</nav>
|
|
27
|
+
|
|
28
|
+
{% comment %}Mobile Navigation Drawer{% endcomment %}
|
|
29
|
+
<div class="header-menu-nav__mobile-drawer" id="header-menu-mobile-drawer-{{ widget.id }}" aria-hidden="true">
|
|
30
|
+
<div class="header-menu-nav__mobile-drawer-content">
|
|
31
|
+
<div class="header-menu-nav__mobile-drawer-header">
|
|
32
|
+
<h2 class="header-menu-nav__mobile-drawer-title">Menu</h2>
|
|
33
|
+
<button type="button" class="header-menu-nav__mobile-drawer-close" aria-label="Close menu" data-header-menu-close>
|
|
34
|
+
<svg class="header-menu-nav__drawer-close-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
35
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
36
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
37
|
+
</svg>
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
<nav class="header-menu-nav__mobile-drawer-nav" role="navigation" aria-label="Mobile navigation">
|
|
41
|
+
<ul class="header-menu-nav__mobile-drawer-list" data-mobile-menu-list>
|
|
42
|
+
<!-- Mobile menu items will be dynamically inserted here -->
|
|
43
|
+
<li class="header-menu-nav__mobile-drawer-item" data-mobile-menu-loading data-mobile-menu-loading-hidden>
|
|
44
|
+
<span class="header-menu-nav__mobile-drawer-link">Loading menu...</span>
|
|
45
|
+
</li>
|
|
46
|
+
<li class="header-menu-nav__mobile-drawer-item" data-mobile-menu-error data-mobile-menu-error-hidden>
|
|
47
|
+
<span class="header-menu-nav__mobile-drawer-link">Menu items unavailable</span>
|
|
48
|
+
</li>
|
|
49
|
+
</ul>
|
|
50
|
+
</nav>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<style>
|
|
55
|
+
/* Menu Navigation Styles */
|
|
56
|
+
.header-menu-nav {
|
|
57
|
+
flex: 1 1 auto;
|
|
58
|
+
display: flex;
|
|
59
|
+
justify-content: flex-start;
|
|
60
|
+
align-items: center;
|
|
61
|
+
min-width: 0;
|
|
62
|
+
width: 100%;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@media (max-width: 768px) {
|
|
66
|
+
.header-menu-nav {
|
|
67
|
+
display: none !important;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.header-menu-nav__loading,
|
|
72
|
+
.header-menu-nav__error {
|
|
73
|
+
padding: var(--space-2, 0.5rem) 0;
|
|
74
|
+
color: var(--menu-text, var(--header-text, #000));
|
|
75
|
+
font-size: var(--text-base, 1rem);
|
|
76
|
+
opacity: 0.6;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.header-menu-nav__error {
|
|
80
|
+
opacity: 0.5;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.header-menu-nav__list {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: var(--space-6, 1.5rem);
|
|
87
|
+
list-style: none;
|
|
88
|
+
margin: 0;
|
|
89
|
+
padding: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.header-menu-nav__item {
|
|
93
|
+
position: relative;
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.header-menu-nav__link {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: var(--space-1, 0.25rem);
|
|
102
|
+
color: var(--menu-text, var(--header-text, #000));
|
|
103
|
+
text-decoration: none;
|
|
104
|
+
font-size: var(--text-base, 1rem);
|
|
105
|
+
font-weight: var(--font-weight-medium, 500);
|
|
106
|
+
line-height: var(--leading-normal, 1.5);
|
|
107
|
+
padding: var(--space-2, 0.5rem) 0;
|
|
108
|
+
transition: color var(--transition-fast, 150ms ease);
|
|
109
|
+
position: relative;
|
|
110
|
+
white-space: nowrap;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.header-menu-nav__link:hover {
|
|
114
|
+
color: var(--menu-text-hover, var(--header-text, #333));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Dropdown arrow icon */
|
|
118
|
+
.header-menu-nav__item.has-children .header-menu-nav__link::after {
|
|
119
|
+
content: '';
|
|
120
|
+
display: inline-block;
|
|
121
|
+
width: 0;
|
|
122
|
+
height: 0;
|
|
123
|
+
border-left: var(--space-1, 0.25rem) solid transparent;
|
|
124
|
+
border-right: var(--space-1, 0.25rem) solid transparent;
|
|
125
|
+
border-top: var(--space-1-25, 0.3125rem) solid var(--menu-text, var(--header-text, #000));
|
|
126
|
+
margin-left: var(--space-1, 0.25rem);
|
|
127
|
+
transition: border-top-color var(--transition-fast, 150ms ease);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.header-menu-nav__item.has-children:hover .header-menu-nav__link::after {
|
|
131
|
+
border-top-color: var(--menu-text-hover, var(--header-text, #333));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Submenu */
|
|
135
|
+
.header-menu-nav__item.has-children {
|
|
136
|
+
position: relative;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.header-menu-nav__submenu {
|
|
140
|
+
position: absolute;
|
|
141
|
+
top: calc(100% + var(--space-2, 0.5rem));
|
|
142
|
+
left: 0;
|
|
143
|
+
display: none;
|
|
144
|
+
flex-direction: column;
|
|
145
|
+
gap: 0;
|
|
146
|
+
background: var(--dropdown-bg, #fff);
|
|
147
|
+
padding: var(--space-2, 0.5rem) 0;
|
|
148
|
+
border-radius: var(--border-radius-medium, 8px);
|
|
149
|
+
box-shadow: 0 var(--space-4, 1rem) var(--space-6, 1.5rem) rgba(0, 0, 0, 0.1);
|
|
150
|
+
min-width: var(--space-50, 12.5rem);
|
|
151
|
+
list-style: none;
|
|
152
|
+
margin: 0;
|
|
153
|
+
z-index: 100;
|
|
154
|
+
border: var(--space-0-5, 0.125rem) solid var(--header-divider, rgba(0, 0, 0, 0.08));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.header-menu-nav__item.has-children:hover .header-menu-nav__submenu {
|
|
158
|
+
display: flex;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.header-menu-nav__submenu-item {
|
|
162
|
+
margin: 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.header-menu-nav__submenu-link {
|
|
166
|
+
display: block;
|
|
167
|
+
padding: var(--space-2-5, 0.625rem) var(--space-5, 1.25rem);
|
|
168
|
+
color: var(--menu-text, var(--header-text, #000));
|
|
169
|
+
text-decoration: none;
|
|
170
|
+
font-size: var(--text-sm, 0.875rem);
|
|
171
|
+
line-height: var(--leading-normal, 1.5);
|
|
172
|
+
transition: background-color var(--transition-fast, 150ms ease);
|
|
173
|
+
font-weight: var(--font-weight-normal, 400);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.header-menu-nav__submenu-link:hover {
|
|
177
|
+
background-color: var(--submenu-hover, rgba(0, 0, 0, 0.04));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Mobile Drawer */
|
|
181
|
+
@media (max-width: 768px) {
|
|
182
|
+
.header-menu-nav__mobile-drawer {
|
|
183
|
+
position: fixed !important;
|
|
184
|
+
top: 0 !important;
|
|
185
|
+
left: 0 !important;
|
|
186
|
+
right: 0 !important;
|
|
187
|
+
bottom: 0 !important;
|
|
188
|
+
width: 100% !important;
|
|
189
|
+
height: 100vh !important;
|
|
190
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
191
|
+
backdrop-filter: blur(2px);
|
|
192
|
+
z-index: 9999 !important;
|
|
193
|
+
pointer-events: none;
|
|
194
|
+
display: block !important;
|
|
195
|
+
opacity: 0;
|
|
196
|
+
visibility: hidden;
|
|
197
|
+
transition: opacity 300ms ease, visibility 300ms ease;
|
|
198
|
+
margin: 0 !important;
|
|
199
|
+
padding: 0 !important;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.header-menu-nav__mobile-drawer.active {
|
|
203
|
+
pointer-events: auto !important;
|
|
204
|
+
opacity: 1 !important;
|
|
205
|
+
visibility: visible !important;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.header-menu-nav__mobile-drawer-content {
|
|
209
|
+
position: absolute !important;
|
|
210
|
+
top: 0 !important;
|
|
211
|
+
left: 0 !important;
|
|
212
|
+
width: 280px !important;
|
|
213
|
+
max-width: 85vw !important;
|
|
214
|
+
height: 100% !important;
|
|
215
|
+
background-color: var(--header-bg, #fff) !important;
|
|
216
|
+
padding: var(--space-6, 1.5rem) !important;
|
|
217
|
+
overflow-y: auto !important;
|
|
218
|
+
overflow-x: hidden;
|
|
219
|
+
-webkit-overflow-scrolling: touch;
|
|
220
|
+
transform: translateX(-100%);
|
|
221
|
+
transition: transform var(--transition-slow, var(--duration-300, 300ms) var(--ease-out, ease));
|
|
222
|
+
box-shadow: var(--space-0-5, 0.125rem) 0 var(--space-2, 0.5rem) rgba(0, 0, 0, 0.15);
|
|
223
|
+
display: flex !important;
|
|
224
|
+
flex-direction: column !important;
|
|
225
|
+
margin: 0 !important;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.header-menu-nav__mobile-drawer.active .header-menu-nav__mobile-drawer-content {
|
|
229
|
+
transform: translateX(0) !important;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.header-menu-nav__mobile-drawer-header {
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
justify-content: space-between;
|
|
236
|
+
margin-bottom: var(--space-6, 1.5rem);
|
|
237
|
+
padding-bottom: var(--space-3, 0.75rem);
|
|
238
|
+
border-bottom: var(--space-0-5, 0.125rem) solid var(--header-divider, rgba(0, 0, 0, 0.08));
|
|
239
|
+
flex-shrink: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.header-menu-nav__mobile-drawer-title {
|
|
243
|
+
font-size: var(--text-lg, 1.125rem);
|
|
244
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
245
|
+
line-height: var(--leading-normal, 1.5);
|
|
246
|
+
color: var(--header-text, #000);
|
|
247
|
+
margin: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.header-menu-nav__mobile-drawer-close {
|
|
251
|
+
display: flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
justify-content: center;
|
|
254
|
+
width: var(--space-10, 2.5rem);
|
|
255
|
+
height: var(--space-10, 2.5rem);
|
|
256
|
+
background: none;
|
|
257
|
+
border: none;
|
|
258
|
+
color: var(--header-text, #000);
|
|
259
|
+
cursor: pointer;
|
|
260
|
+
border-radius: var(--border-radius-small, var(--space-1, 0.25rem));
|
|
261
|
+
transition: background-color var(--transition-fast, var(--duration-150, 150ms) var(--ease-out, ease));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.header-menu-nav__mobile-drawer-close:hover,
|
|
265
|
+
.header-menu-nav__mobile-drawer-close:focus {
|
|
266
|
+
background-color: var(--submenu-hover, rgba(0, 0, 0, 0.04));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.header-menu-nav__drawer-close-icon {
|
|
270
|
+
width: var(--icon-size-lg, var(--space-6, 1.5rem));
|
|
271
|
+
height: var(--icon-size-lg, var(--space-6, 1.5rem));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.header-menu-nav__mobile-drawer-nav {
|
|
275
|
+
flex: 1;
|
|
276
|
+
overflow-y: auto;
|
|
277
|
+
-webkit-overflow-scrolling: touch;
|
|
278
|
+
min-height: 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.header-menu-nav__mobile-drawer-list {
|
|
282
|
+
list-style: none;
|
|
283
|
+
margin: 0;
|
|
284
|
+
padding: 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.header-menu-nav__mobile-drawer-item {
|
|
288
|
+
border-bottom: var(--space-0-5, 0.125rem) solid var(--header-divider, rgba(0, 0, 0, 0.08));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.header-menu-nav__mobile-drawer-item:last-child {
|
|
292
|
+
border-bottom: none;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.header-menu-nav__mobile-drawer-link {
|
|
296
|
+
display: block;
|
|
297
|
+
padding: var(--space-4, 1rem) 0;
|
|
298
|
+
color: var(--header-text, #000);
|
|
299
|
+
text-decoration: none;
|
|
300
|
+
font-weight: var(--font-weight-medium, 500);
|
|
301
|
+
font-size: var(--text-base, 1rem);
|
|
302
|
+
line-height: var(--leading-normal, 1.5);
|
|
303
|
+
transition: color var(--transition-fast, 150ms ease);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.header-menu-nav__mobile-drawer-link:hover,
|
|
307
|
+
.header-menu-nav__mobile-drawer-link:focus {
|
|
308
|
+
color: var(--menu-text-hover, var(--header-text, #333));
|
|
309
|
+
opacity: 0.7;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.header-menu-nav__mobile-drawer-sublist {
|
|
313
|
+
list-style: none;
|
|
314
|
+
margin: 0;
|
|
315
|
+
padding: 0 0 0 var(--space-4, 1rem);
|
|
316
|
+
border-left: var(--space-0-5, 0.125rem) solid var(--header-divider, rgba(0, 0, 0, 0.08));
|
|
317
|
+
display: none;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.header-menu-nav__mobile-drawer-item.open .header-menu-nav__mobile-drawer-sublist {
|
|
321
|
+
display: block;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.header-menu-nav__mobile-drawer-subitem {
|
|
325
|
+
border-bottom: var(--space-0-5, 0.125rem) solid rgba(0, 0, 0, 0.05);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.header-menu-nav__mobile-drawer-sublink {
|
|
329
|
+
display: block;
|
|
330
|
+
padding: var(--space-3, 0.75rem) 0;
|
|
331
|
+
color: var(--header-text, #000);
|
|
332
|
+
text-decoration: none;
|
|
333
|
+
font-weight: var(--font-weight-normal, 400);
|
|
334
|
+
font-size: var(--text-sm, 0.875rem);
|
|
335
|
+
line-height: var(--leading-normal, 1.5);
|
|
336
|
+
transition: color var(--transition-fast, 150ms ease);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.header-menu-nav__mobile-drawer-sublink:hover,
|
|
340
|
+
.header-menu-nav__mobile-drawer-sublink:focus {
|
|
341
|
+
color: var(--menu-text-hover, var(--header-text, #333));
|
|
342
|
+
opacity: 0.6;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* Hide mobile drawer on desktop */
|
|
347
|
+
@media (min-width: 769px) {
|
|
348
|
+
.header-menu-nav__mobile-drawer {
|
|
349
|
+
display: none !important;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
</style>
|
|
353
|
+
|
|
354
|
+
<script>
|
|
355
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
356
|
+
const menuNav = document.querySelector('[data-widget-id="{{ widget.id }}"][data-header-menu-nav]');
|
|
357
|
+
if (!menuNav) return;
|
|
358
|
+
|
|
359
|
+
// Move drawer to body level to avoid parent display:none issues
|
|
360
|
+
const drawerId = 'header-menu-mobile-drawer-{{ widget.id }}';
|
|
361
|
+
const drawer = document.getElementById(drawerId);
|
|
362
|
+
if (drawer && drawer.parentElement && drawer.parentElement !== document.body) {
|
|
363
|
+
console.log('[HeaderMenu Widget] Moving drawer to body level');
|
|
364
|
+
document.body.appendChild(drawer);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Shared menu data storage
|
|
368
|
+
let sharedMenuData = null;
|
|
369
|
+
|
|
370
|
+
// Load Main Menu from API (for both desktop and mobile)
|
|
371
|
+
loadMainMenu(menuNav);
|
|
372
|
+
|
|
373
|
+
// Setup hamburger menu with retry logic (only on mobile)
|
|
374
|
+
if (window.innerWidth <= 768) {
|
|
375
|
+
setupHamburgerMenu();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Re-setup on window resize
|
|
379
|
+
let resizeTimeout;
|
|
380
|
+
window.addEventListener('resize', function() {
|
|
381
|
+
clearTimeout(resizeTimeout);
|
|
382
|
+
resizeTimeout = setTimeout(function() {
|
|
383
|
+
if (window.innerWidth <= 768) {
|
|
384
|
+
setupHamburgerMenu();
|
|
385
|
+
}
|
|
386
|
+
}, 250);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
function setupHamburgerMenu() {
|
|
390
|
+
const drawerId = 'header-menu-mobile-drawer-{{ widget.id }}';
|
|
391
|
+
let attempts = 0;
|
|
392
|
+
const maxAttempts = 15;
|
|
393
|
+
|
|
394
|
+
function trySetup() {
|
|
395
|
+
// Only setup on mobile
|
|
396
|
+
if (window.innerWidth > 768) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const drawer = document.getElementById(drawerId);
|
|
401
|
+
const drawerClose = document.querySelector('[data-header-menu-close]');
|
|
402
|
+
const toggle = document.querySelector('[data-header-menu-toggle]');
|
|
403
|
+
|
|
404
|
+
if (toggle && drawer) {
|
|
405
|
+
console.log('[HeaderMenu Widget] Setting up hamburger menu', {
|
|
406
|
+
widgetId: '{{ widget.id }}',
|
|
407
|
+
toggleFound: !!toggle,
|
|
408
|
+
drawerFound: !!drawer,
|
|
409
|
+
drawerCloseFound: !!drawerClose,
|
|
410
|
+
windowWidth: window.innerWidth
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Use a flag to prevent duplicate listeners
|
|
414
|
+
if (toggle.dataset.menuListenerAttached === 'true') {
|
|
415
|
+
console.log('[HeaderMenu Widget] Listener already attached, skipping');
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
toggle.dataset.menuListenerAttached = 'true';
|
|
419
|
+
|
|
420
|
+
// Mobile menu toggle - Open drawer (only on mobile)
|
|
421
|
+
toggle.addEventListener('click', (e) => {
|
|
422
|
+
// Double check we're on mobile
|
|
423
|
+
if (window.innerWidth > 768) {
|
|
424
|
+
console.log('[HeaderMenu Widget] Ignoring toggle click on desktop');
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
e.preventDefault();
|
|
428
|
+
e.stopPropagation();
|
|
429
|
+
const currentDrawer = document.getElementById(drawerId);
|
|
430
|
+
if (!currentDrawer) {
|
|
431
|
+
console.error('[HeaderMenu Widget] Drawer not found when toggling');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const isOpen = currentDrawer.classList.contains('active');
|
|
435
|
+
console.log('[HeaderMenu Widget] Toggle clicked, drawer state:', isOpen ? 'open' : 'closed');
|
|
436
|
+
if (isOpen) {
|
|
437
|
+
closeDrawer(currentDrawer, toggle);
|
|
438
|
+
} else {
|
|
439
|
+
openDrawer(currentDrawer, toggle);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
function openDrawer(currentDrawer, currentToggle) {
|
|
444
|
+
if (!currentDrawer) {
|
|
445
|
+
console.error('[HeaderMenu Widget] Cannot open drawer - drawer not found');
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (window.innerWidth > 768) {
|
|
449
|
+
console.log('[HeaderMenu Widget] Ignoring open on desktop');
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
console.log('[HeaderMenu Widget] Opening drawer');
|
|
453
|
+
currentDrawer.classList.add('active');
|
|
454
|
+
currentDrawer.setAttribute('aria-hidden', 'false');
|
|
455
|
+
if (currentToggle) {
|
|
456
|
+
currentToggle.setAttribute('aria-expanded', 'true');
|
|
457
|
+
currentToggle.classList.add('is-open');
|
|
458
|
+
}
|
|
459
|
+
// Prevent body scroll
|
|
460
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
461
|
+
document.body.style.paddingRight = scrollbarWidth + 'px';
|
|
462
|
+
document.body.style.overflow = 'hidden';
|
|
463
|
+
document.documentElement.style.overflow = 'hidden';
|
|
464
|
+
console.log('[HeaderMenu Widget] Drawer opened successfully');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function closeDrawer(currentDrawer, currentToggle) {
|
|
468
|
+
if (!currentDrawer) {
|
|
469
|
+
console.error('[HeaderMenu Widget] Cannot close drawer - drawer not found');
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
console.log('[HeaderMenu Widget] Closing drawer');
|
|
473
|
+
currentDrawer.classList.remove('active');
|
|
474
|
+
currentDrawer.setAttribute('aria-hidden', 'true');
|
|
475
|
+
if (currentToggle) {
|
|
476
|
+
currentToggle.setAttribute('aria-expanded', 'false');
|
|
477
|
+
currentToggle.classList.remove('is-open');
|
|
478
|
+
}
|
|
479
|
+
// Restore body scroll
|
|
480
|
+
document.body.style.paddingRight = '';
|
|
481
|
+
document.body.style.overflow = '';
|
|
482
|
+
document.documentElement.style.overflow = '';
|
|
483
|
+
console.log('[HeaderMenu Widget] Drawer closed successfully');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Close drawer button
|
|
487
|
+
if (drawerClose && drawer) {
|
|
488
|
+
drawerClose.addEventListener('click', (e) => {
|
|
489
|
+
e.preventDefault();
|
|
490
|
+
e.stopPropagation();
|
|
491
|
+
console.log('[HeaderMenu Widget] Close button clicked');
|
|
492
|
+
const currentDrawer = document.getElementById(drawerId);
|
|
493
|
+
if (currentDrawer) {
|
|
494
|
+
closeDrawer(currentDrawer, toggle);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Close on Escape key (only add once)
|
|
500
|
+
if (!window.__headerMenuWidgetEscapeHandler) {
|
|
501
|
+
window.__headerMenuWidgetEscapeHandler = function(e) {
|
|
502
|
+
if (e.key === 'Escape' && window.innerWidth <= 768) {
|
|
503
|
+
const currentDrawer = document.getElementById(drawerId);
|
|
504
|
+
if (currentDrawer && currentDrawer.classList.contains('active')) {
|
|
505
|
+
console.log('[HeaderMenu Widget] Escape key pressed');
|
|
506
|
+
closeDrawer(currentDrawer, toggle);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
document.addEventListener('keydown', window.__headerMenuWidgetEscapeHandler);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Close on outside click (only add once)
|
|
514
|
+
if (!window.__headerMenuWidgetOverlayHandler) {
|
|
515
|
+
window.__headerMenuWidgetOverlayHandler = function(e) {
|
|
516
|
+
if (window.innerWidth > 768) return;
|
|
517
|
+
const currentDrawer = document.getElementById(drawerId);
|
|
518
|
+
if (currentDrawer && currentDrawer.classList.contains('active')) {
|
|
519
|
+
const drawerContent = currentDrawer.querySelector('.header-menu-nav__mobile-drawer-content');
|
|
520
|
+
// Close if clicking the backdrop (drawer itself) but not the content
|
|
521
|
+
if (e.target === currentDrawer || (drawerContent && !drawerContent.contains(e.target) && currentDrawer.contains(e.target))) {
|
|
522
|
+
console.log('[HeaderMenu Widget] Backdrop clicked - closing drawer');
|
|
523
|
+
closeDrawer(currentDrawer, toggle);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
document.addEventListener('click', window.__headerMenuWidgetOverlayHandler);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Close on outside click
|
|
531
|
+
const overlayClickHandler = (e) => {
|
|
532
|
+
const currentDrawer = document.getElementById(drawerId);
|
|
533
|
+
if (currentDrawer && currentDrawer.classList.contains('active')) {
|
|
534
|
+
if (e.target === currentDrawer) {
|
|
535
|
+
closeDrawer(currentDrawer, newToggle);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
document.addEventListener('click', overlayClickHandler);
|
|
540
|
+
|
|
541
|
+
console.log('[HeaderMenu Widget] Hamburger menu setup complete');
|
|
542
|
+
return true;
|
|
543
|
+
} else {
|
|
544
|
+
attempts++;
|
|
545
|
+
if (attempts < maxAttempts) {
|
|
546
|
+
setTimeout(trySetup, 100);
|
|
547
|
+
} else {
|
|
548
|
+
console.warn('[HeaderMenu Widget] Could not find hamburger menu elements after', maxAttempts, 'attempts', {
|
|
549
|
+
toggleFound: !!toggle,
|
|
550
|
+
drawerFound: !!drawer,
|
|
551
|
+
drawerId: drawerId,
|
|
552
|
+
windowWidth: window.innerWidth
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
trySetup();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Submenu toggles for mobile
|
|
563
|
+
function setupSubmenuToggles() {
|
|
564
|
+
// Find all mobile drawer items with children (both in menuNav and in the drawer)
|
|
565
|
+
const drawerId = 'header-menu-mobile-drawer-{{ widget.id }}';
|
|
566
|
+
const drawer = document.getElementById(drawerId);
|
|
567
|
+
const drawerContainer = drawer ? drawer.querySelector('[data-mobile-menu-list]') : null;
|
|
568
|
+
|
|
569
|
+
// Get items from both locations
|
|
570
|
+
const itemsFromNav = menuNav ? menuNav.querySelectorAll('.header-menu-nav__mobile-drawer-item.has-children') : [];
|
|
571
|
+
const itemsFromDrawer = drawerContainer ? drawerContainer.querySelectorAll('.header-menu-nav__mobile-drawer-item.has-children') : [];
|
|
572
|
+
|
|
573
|
+
// Combine and deduplicate
|
|
574
|
+
const allItems = new Set([...itemsFromNav, ...itemsFromDrawer]);
|
|
575
|
+
|
|
576
|
+
console.log('[HeaderMenu Widget] Setting up submenu toggles, found', allItems.size, 'items with children');
|
|
577
|
+
|
|
578
|
+
allItems.forEach(item => {
|
|
579
|
+
const link = item.querySelector('.header-menu-nav__mobile-drawer-link');
|
|
580
|
+
if (link) {
|
|
581
|
+
// Remove existing listener if any
|
|
582
|
+
const newLink = link.cloneNode(true);
|
|
583
|
+
link.parentNode.replaceChild(newLink, link);
|
|
584
|
+
|
|
585
|
+
newLink.addEventListener('click', (e) => {
|
|
586
|
+
const submenu = item.querySelector('.header-menu-nav__mobile-drawer-sublist');
|
|
587
|
+
if (submenu) {
|
|
588
|
+
e.preventDefault();
|
|
589
|
+
e.stopPropagation();
|
|
590
|
+
const isOpen = item.classList.contains('open');
|
|
591
|
+
item.classList.toggle('open');
|
|
592
|
+
console.log('[HeaderMenu Widget] Submenu toggled:', item.classList.contains('open') ? 'opened' : 'closed');
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Load Main Menu from API
|
|
601
|
+
* Fetches menus, filters for "Main Menu" type, and renders menu items
|
|
602
|
+
*/
|
|
603
|
+
async function loadMainMenu(menuNav) {
|
|
604
|
+
const menuList = menuNav.querySelector('[data-menu-list]');
|
|
605
|
+
const loadingEl = menuNav.querySelector('[data-menu-loading]');
|
|
606
|
+
const errorEl = menuNav.querySelector('[data-menu-error]');
|
|
607
|
+
const mobileMenuList = document.querySelector('[data-mobile-menu-list]');
|
|
608
|
+
const mobileLoading = document.querySelector('[data-mobile-menu-loading]');
|
|
609
|
+
const mobileError = document.querySelector('[data-mobile-menu-error]');
|
|
610
|
+
|
|
611
|
+
if (!menuList) {
|
|
612
|
+
console.error('[HeaderMenu] Menu list not found');
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
// Show loading state
|
|
618
|
+
if (loadingEl) loadingEl.style.display = 'block';
|
|
619
|
+
if (errorEl) errorEl.style.display = 'none';
|
|
620
|
+
if (mobileLoading) mobileLoading.style.display = 'block';
|
|
621
|
+
if (mobileError) mobileError.style.display = 'none';
|
|
622
|
+
menuList.innerHTML = '';
|
|
623
|
+
|
|
624
|
+
// Step 1: Fetch all menus
|
|
625
|
+
const menusResponse = await fetch('/webstoreapi/menus', {
|
|
626
|
+
headers: {
|
|
627
|
+
'Accept': 'application/json',
|
|
628
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
if (!menusResponse.ok) {
|
|
633
|
+
throw new Error(`Failed to fetch menus: ${menusResponse.status}`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const menus = await menusResponse.json();
|
|
637
|
+
if (!Array.isArray(menus)) {
|
|
638
|
+
throw new Error('Invalid menus response format');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Step 2: Filter for "Main Menu" type (case-insensitive)
|
|
642
|
+
const mainMenu = menus.find(menu => {
|
|
643
|
+
const menuType = menu.type || '';
|
|
644
|
+
return menuType.toLowerCase().trim() === 'main menu';
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
if (!mainMenu || !mainMenu.id) {
|
|
648
|
+
throw new Error('Main Menu not found');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Step 3: Fetch full menu details with items
|
|
652
|
+
const menuResponse = await fetch(`/webstoreapi/menus/${mainMenu.id}`, {
|
|
653
|
+
method: 'GET',
|
|
654
|
+
headers: {
|
|
655
|
+
'Accept': 'application/json',
|
|
656
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
if (!menuResponse.ok) {
|
|
661
|
+
throw new Error(`Failed to fetch menu details: ${menuResponse.status}`);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const menuData = await menuResponse.json();
|
|
665
|
+
|
|
666
|
+
if (!menuData || !menuData.items || !Array.isArray(menuData.items)) {
|
|
667
|
+
throw new Error('Menu items not found');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Step 4: Render menu items
|
|
671
|
+
if (menuData.items.length === 0) {
|
|
672
|
+
throw new Error('No menu items available');
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Store menu data for sharing between desktop and mobile
|
|
676
|
+
sharedMenuData = menuData.items;
|
|
677
|
+
|
|
678
|
+
// Render desktop menu
|
|
679
|
+
renderMenuItems(menuList, menuData.items);
|
|
680
|
+
|
|
681
|
+
// Render mobile menu
|
|
682
|
+
if (mobileMenuList) {
|
|
683
|
+
renderMobileMenuItems(mobileMenuList, menuData.items);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Setup mobile submenu toggles
|
|
687
|
+
setupSubmenuToggles();
|
|
688
|
+
|
|
689
|
+
// Hide loading state
|
|
690
|
+
if (loadingEl) {
|
|
691
|
+
loadingEl.setAttribute('data-menu-loading-hidden', '');
|
|
692
|
+
loadingEl.style.display = 'none';
|
|
693
|
+
}
|
|
694
|
+
if (mobileLoading) {
|
|
695
|
+
mobileLoading.setAttribute('data-mobile-menu-loading-hidden', '');
|
|
696
|
+
mobileLoading.style.display = 'none';
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
} catch (error) {
|
|
700
|
+
console.error('[HeaderMenu] Error loading menu:', error);
|
|
701
|
+
|
|
702
|
+
// Hide loading, show error
|
|
703
|
+
if (loadingEl) {
|
|
704
|
+
loadingEl.setAttribute('data-menu-loading-hidden', '');
|
|
705
|
+
loadingEl.style.display = 'none';
|
|
706
|
+
}
|
|
707
|
+
if (mobileLoading) {
|
|
708
|
+
mobileLoading.setAttribute('data-mobile-menu-loading-hidden', '');
|
|
709
|
+
mobileLoading.style.display = 'none';
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (errorEl) {
|
|
713
|
+
errorEl.removeAttribute('data-menu-error-hidden');
|
|
714
|
+
errorEl.style.display = 'block';
|
|
715
|
+
} else {
|
|
716
|
+
menuList.innerHTML = '<li class="header-menu-nav__item"><span class="header-menu-nav__link">Menu items unavailable</span></li>';
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Show error in mobile menu too
|
|
720
|
+
if (mobileError) {
|
|
721
|
+
mobileError.removeAttribute('data-mobile-menu-error-hidden');
|
|
722
|
+
mobileError.style.display = 'block';
|
|
723
|
+
} else if (mobileMenuList) {
|
|
724
|
+
mobileMenuList.innerHTML = '<li class="header-menu-nav__mobile-drawer-item"><span class="header-menu-nav__mobile-drawer-link">Menu items unavailable</span></li>';
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Normalize menu link to ensure it's an absolute URL
|
|
731
|
+
* @param {string} link - Menu link from API
|
|
732
|
+
* @returns {string} Normalized absolute URL
|
|
733
|
+
*/
|
|
734
|
+
function normalizeMenuLink(link) {
|
|
735
|
+
if (!link || link === '#' || link.trim() === '') {
|
|
736
|
+
return '#';
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const trimmedLink = link.trim();
|
|
740
|
+
|
|
741
|
+
// If already an absolute URL (http:// or https://), return as-is
|
|
742
|
+
if (trimmedLink.startsWith('http://') || trimmedLink.startsWith('https://')) {
|
|
743
|
+
return trimmedLink;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// If already starts with /, it's absolute from root, return as-is
|
|
747
|
+
if (trimmedLink.startsWith('/')) {
|
|
748
|
+
return trimmedLink;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Otherwise, prepend base URL to make it absolute
|
|
752
|
+
const baseUrl = window.location.origin;
|
|
753
|
+
// Remove leading slash from link if present, then add it back with baseUrl
|
|
754
|
+
const cleanLink = trimmedLink.startsWith('/') ? trimmedLink : '/' + trimmedLink;
|
|
755
|
+
return baseUrl + cleanLink;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Render menu items recursively
|
|
760
|
+
* @param {HTMLElement} container - Container element to append menu items
|
|
761
|
+
* @param {Array} items - Array of menu items
|
|
762
|
+
*/
|
|
763
|
+
function renderMenuItems(container, items) {
|
|
764
|
+
if (!items || items.length === 0) return;
|
|
765
|
+
|
|
766
|
+
// Sort items by displayOrder if available
|
|
767
|
+
const sortedItems = [...items].sort((a, b) => {
|
|
768
|
+
const orderA = a.displayOrder !== undefined ? a.displayOrder : 999;
|
|
769
|
+
const orderB = b.displayOrder !== undefined ? b.displayOrder : 999;
|
|
770
|
+
return orderA - orderB;
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
sortedItems.forEach(item => {
|
|
774
|
+
const hasChildren = item.childItems && Array.isArray(item.childItems) && item.childItems.length > 0;
|
|
775
|
+
|
|
776
|
+
// Create list item
|
|
777
|
+
const li = document.createElement('li');
|
|
778
|
+
li.className = 'header-menu-nav__item';
|
|
779
|
+
if (hasChildren) {
|
|
780
|
+
li.classList.add('has-children');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Create link
|
|
784
|
+
const link = document.createElement('a');
|
|
785
|
+
link.href = normalizeMenuLink(item.link);
|
|
786
|
+
link.className = 'header-menu-nav__link';
|
|
787
|
+
|
|
788
|
+
// Escape HTML to prevent XSS
|
|
789
|
+
const textNode = document.createTextNode(item.name || item.title || '');
|
|
790
|
+
link.appendChild(textNode);
|
|
791
|
+
link.setAttribute('aria-label', item.name || item.title || 'Menu item');
|
|
792
|
+
|
|
793
|
+
li.appendChild(link);
|
|
794
|
+
|
|
795
|
+
// Create submenu for items with children
|
|
796
|
+
if (hasChildren) {
|
|
797
|
+
const submenu = document.createElement('ul');
|
|
798
|
+
submenu.className = 'header-menu-nav__submenu';
|
|
799
|
+
|
|
800
|
+
// Recursively render child items
|
|
801
|
+
renderSubmenuItems(submenu, item.childItems);
|
|
802
|
+
li.appendChild(submenu);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
container.appendChild(li);
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Render submenu items recursively
|
|
811
|
+
* @param {HTMLElement} container - Container element for submenu
|
|
812
|
+
* @param {Array} items - Array of child menu items
|
|
813
|
+
*/
|
|
814
|
+
function renderSubmenuItems(container, items) {
|
|
815
|
+
if (!items || items.length === 0) return;
|
|
816
|
+
|
|
817
|
+
// Sort items by displayOrder if available
|
|
818
|
+
const sortedItems = [...items].sort((a, b) => {
|
|
819
|
+
const orderA = a.displayOrder !== undefined ? a.displayOrder : 999;
|
|
820
|
+
const orderB = b.displayOrder !== undefined ? b.displayOrder : 999;
|
|
821
|
+
return orderA - orderB;
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
sortedItems.forEach(item => {
|
|
825
|
+
const li = document.createElement('li');
|
|
826
|
+
li.className = 'header-menu-nav__submenu-item';
|
|
827
|
+
|
|
828
|
+
const link = document.createElement('a');
|
|
829
|
+
link.href = normalizeMenuLink(item.link);
|
|
830
|
+
link.className = 'header-menu-nav__submenu-link';
|
|
831
|
+
|
|
832
|
+
// Escape HTML to prevent XSS
|
|
833
|
+
const textNode = document.createTextNode(item.name || item.title || '');
|
|
834
|
+
link.appendChild(textNode);
|
|
835
|
+
link.setAttribute('aria-label', item.name || item.title || 'Submenu item');
|
|
836
|
+
|
|
837
|
+
li.appendChild(link);
|
|
838
|
+
container.appendChild(li);
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Render mobile menu items recursively
|
|
844
|
+
* @param {HTMLElement} container - Container element to append menu items
|
|
845
|
+
* @param {Array} items - Array of menu items
|
|
846
|
+
*/
|
|
847
|
+
function renderMobileMenuItems(container, items) {
|
|
848
|
+
if (!items || items.length === 0) {
|
|
849
|
+
container.innerHTML = '<li class="header-menu-nav__mobile-drawer-item"><span class="header-menu-nav__mobile-drawer-link">No menu items</span></li>';
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Clear existing content
|
|
854
|
+
container.innerHTML = '';
|
|
855
|
+
|
|
856
|
+
// Sort items by displayOrder if available
|
|
857
|
+
const sortedItems = [...items].sort((a, b) => {
|
|
858
|
+
const orderA = a.displayOrder !== undefined ? a.displayOrder : 999;
|
|
859
|
+
const orderB = b.displayOrder !== undefined ? b.displayOrder : 999;
|
|
860
|
+
return orderA - orderB;
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
sortedItems.forEach(item => {
|
|
864
|
+
const hasChildren = item.childItems && Array.isArray(item.childItems) && item.childItems.length > 0;
|
|
865
|
+
|
|
866
|
+
// Create list item
|
|
867
|
+
const li = document.createElement('li');
|
|
868
|
+
li.className = 'header-menu-nav__mobile-drawer-item';
|
|
869
|
+
if (hasChildren) {
|
|
870
|
+
li.classList.add('has-children');
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Create link
|
|
874
|
+
const link = document.createElement('a');
|
|
875
|
+
link.href = normalizeMenuLink(item.link);
|
|
876
|
+
link.className = 'header-menu-nav__mobile-drawer-link';
|
|
877
|
+
|
|
878
|
+
// Escape HTML to prevent XSS
|
|
879
|
+
const textNode = document.createTextNode(item.name || item.title || '');
|
|
880
|
+
link.appendChild(textNode);
|
|
881
|
+
link.setAttribute('aria-label', item.name || item.title || 'Menu item');
|
|
882
|
+
|
|
883
|
+
li.appendChild(link);
|
|
884
|
+
|
|
885
|
+
// Create submenu for items with children
|
|
886
|
+
if (hasChildren) {
|
|
887
|
+
const submenu = document.createElement('ul');
|
|
888
|
+
submenu.className = 'header-menu-nav__mobile-drawer-sublist';
|
|
889
|
+
|
|
890
|
+
// Recursively render child items
|
|
891
|
+
renderMobileSubmenuItems(submenu, item.childItems);
|
|
892
|
+
li.appendChild(submenu);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
container.appendChild(li);
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Render mobile submenu items recursively
|
|
901
|
+
* @param {HTMLElement} container - Container element for submenu
|
|
902
|
+
* @param {Array} items - Array of child menu items
|
|
903
|
+
*/
|
|
904
|
+
function renderMobileSubmenuItems(container, items) {
|
|
905
|
+
if (!items || items.length === 0) return;
|
|
906
|
+
|
|
907
|
+
// Sort items by displayOrder if available
|
|
908
|
+
const sortedItems = [...items].sort((a, b) => {
|
|
909
|
+
const orderA = a.displayOrder !== undefined ? a.displayOrder : 999;
|
|
910
|
+
const orderB = b.displayOrder !== undefined ? b.displayOrder : 999;
|
|
911
|
+
return orderA - orderB;
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
sortedItems.forEach(item => {
|
|
915
|
+
const li = document.createElement('li');
|
|
916
|
+
li.className = 'header-menu-nav__mobile-drawer-subitem';
|
|
917
|
+
|
|
918
|
+
const link = document.createElement('a');
|
|
919
|
+
link.href = normalizeMenuLink(item.link);
|
|
920
|
+
link.className = 'header-menu-nav__mobile-drawer-sublink';
|
|
921
|
+
|
|
922
|
+
// Escape HTML to prevent XSS
|
|
923
|
+
const textNode = document.createTextNode(item.name || item.title || '');
|
|
924
|
+
link.appendChild(textNode);
|
|
925
|
+
link.setAttribute('aria-label', item.name || item.title || 'Submenu item');
|
|
926
|
+
|
|
927
|
+
li.appendChild(link);
|
|
928
|
+
container.appendChild(li);
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
</script>
|