@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.
Files changed (116) hide show
  1. package/README.md +425 -0
  2. package/assets/Logo_o2vend.png +0 -0
  3. package/assets/favicon.png +0 -0
  4. package/assets/logo-white.png +0 -0
  5. package/bin/o2vend +42 -0
  6. package/config/widget-map.json +50 -0
  7. package/lib/commands/check.js +201 -0
  8. package/lib/commands/generate.js +33 -0
  9. package/lib/commands/init.js +214 -0
  10. package/lib/commands/optimize.js +216 -0
  11. package/lib/commands/package.js +208 -0
  12. package/lib/commands/serve.js +105 -0
  13. package/lib/commands/validate.js +191 -0
  14. package/lib/lib/api-client.js +357 -0
  15. package/lib/lib/dev-server.js +2618 -0
  16. package/lib/lib/file-watcher.js +80 -0
  17. package/lib/lib/hot-reload.js +106 -0
  18. package/lib/lib/liquid-engine.js +822 -0
  19. package/lib/lib/liquid-filters.js +671 -0
  20. package/lib/lib/mock-api-server.js +989 -0
  21. package/lib/lib/mock-data.js +1468 -0
  22. package/lib/lib/widget-service.js +321 -0
  23. package/package.json +70 -0
  24. package/test-theme/README.md +27 -0
  25. package/test-theme/assets/async-sections.js +446 -0
  26. package/test-theme/assets/cart-drawer.js +463 -0
  27. package/test-theme/assets/cart-manager.js +223 -0
  28. package/test-theme/assets/checkout-price-handler.js +368 -0
  29. package/test-theme/assets/components.css +4629 -0
  30. package/test-theme/assets/delivery-zone.css +299 -0
  31. package/test-theme/assets/delivery-zone.js +396 -0
  32. package/test-theme/assets/logo.png +0 -0
  33. package/test-theme/assets/sections.css +48 -0
  34. package/test-theme/assets/theme.css +3500 -0
  35. package/test-theme/assets/theme.js +3745 -0
  36. package/test-theme/config/settings_data.json +292 -0
  37. package/test-theme/config/settings_schema.json +1050 -0
  38. package/test-theme/layout/theme.liquid +195 -0
  39. package/test-theme/locales/en.default.json +260 -0
  40. package/test-theme/sections/content-fallback.liquid +53 -0
  41. package/test-theme/sections/content.liquid +57 -0
  42. package/test-theme/sections/footer-fallback.liquid +328 -0
  43. package/test-theme/sections/footer.liquid +278 -0
  44. package/test-theme/sections/header-fallback.liquid +1805 -0
  45. package/test-theme/sections/header.liquid +1145 -0
  46. package/test-theme/sections/hero-fallback.liquid +212 -0
  47. package/test-theme/sections/hero.liquid +136 -0
  48. package/test-theme/snippets/account-sidebar.liquid +200 -0
  49. package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
  50. package/test-theme/snippets/breadcrumbs.liquid +134 -0
  51. package/test-theme/snippets/cart-drawer.liquid +467 -0
  52. package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
  53. package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
  54. package/test-theme/snippets/delivery-zone-search.liquid +78 -0
  55. package/test-theme/snippets/icon.liquid +105 -0
  56. package/test-theme/snippets/login-modal.liquid +346 -0
  57. package/test-theme/snippets/mega-menu.liquid +812 -0
  58. package/test-theme/snippets/news-thumbnail.liquid +187 -0
  59. package/test-theme/snippets/pagination.liquid +120 -0
  60. package/test-theme/snippets/price.liquid +92 -0
  61. package/test-theme/snippets/product-card-related.liquid +78 -0
  62. package/test-theme/snippets/product-card-simple.liquid +41 -0
  63. package/test-theme/snippets/product-card.liquid +697 -0
  64. package/test-theme/snippets/rating.liquid +85 -0
  65. package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
  66. package/test-theme/snippets/skeleton-product-card.liquid +124 -0
  67. package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
  68. package/test-theme/snippets/social-sharing.liquid +185 -0
  69. package/test-theme/templates/account/dashboard.liquid +401 -0
  70. package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
  71. package/test-theme/templates/account/loyalty.liquid +588 -0
  72. package/test-theme/templates/account/order-detail.liquid +230 -0
  73. package/test-theme/templates/account/orders.liquid +349 -0
  74. package/test-theme/templates/account/profile.liquid +758 -0
  75. package/test-theme/templates/account/register.liquid +232 -0
  76. package/test-theme/templates/account/return-orders.liquid +348 -0
  77. package/test-theme/templates/account/store-credit.liquid +464 -0
  78. package/test-theme/templates/account/subscriptions.liquid +601 -0
  79. package/test-theme/templates/account/wishlist.liquid +419 -0
  80. package/test-theme/templates/address-book.liquid +1092 -0
  81. package/test-theme/templates/categories.liquid +452 -0
  82. package/test-theme/templates/checkout.liquid +4511 -0
  83. package/test-theme/templates/error.liquid +384 -0
  84. package/test-theme/templates/index.liquid +11 -0
  85. package/test-theme/templates/login.liquid +185 -0
  86. package/test-theme/templates/order-confirmation.liquid +720 -0
  87. package/test-theme/templates/page.liquid +297 -0
  88. package/test-theme/templates/product-detail.liquid +4363 -0
  89. package/test-theme/templates/products.liquid +518 -0
  90. package/test-theme/templates/search.liquid +922 -0
  91. package/test-theme/theme.json.example +19 -0
  92. package/test-theme/widgets/brand-carousel.liquid +676 -0
  93. package/test-theme/widgets/brand.liquid +245 -0
  94. package/test-theme/widgets/carousel.liquid +843 -0
  95. package/test-theme/widgets/category-list-carousel.liquid +656 -0
  96. package/test-theme/widgets/category-list.liquid +340 -0
  97. package/test-theme/widgets/category.liquid +475 -0
  98. package/test-theme/widgets/discount-time.liquid +176 -0
  99. package/test-theme/widgets/footer-menu.liquid +695 -0
  100. package/test-theme/widgets/footer.liquid +179 -0
  101. package/test-theme/widgets/gallery.liquid +271 -0
  102. package/test-theme/widgets/header-menu.liquid +932 -0
  103. package/test-theme/widgets/header.liquid +159 -0
  104. package/test-theme/widgets/html.liquid +214 -0
  105. package/test-theme/widgets/news.liquid +217 -0
  106. package/test-theme/widgets/product-canvas.liquid +235 -0
  107. package/test-theme/widgets/product-carousel.liquid +502 -0
  108. package/test-theme/widgets/product.liquid +45 -0
  109. package/test-theme/widgets/recently-viewed.liquid +26 -0
  110. package/test-theme/widgets/shared/product-grid.liquid +339 -0
  111. package/test-theme/widgets/simple-product.liquid +42 -0
  112. package/test-theme/widgets/single-product.liquid +610 -0
  113. package/test-theme/widgets/spacebar-carousel.liquid +663 -0
  114. package/test-theme/widgets/spacebar.liquid +279 -0
  115. package/test-theme/widgets/splash.liquid +378 -0
  116. 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>