@sofidevo/astro-dynamic-header 3.1.0 → 4.0.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.
- package/README.md +474 -38
- package/package.json +15 -14
- package/src/ChevronIcon.astro +13 -5
- package/src/HamburgerButton.astro +100 -92
- package/src/Header.astro +103 -156
- package/src/MobileNav.astro +122 -114
- package/src/MoonIcon.astro +63 -55
- package/src/NavMenu.astro +165 -273
- package/src/SunIcon.astro +23 -15
- package/src/index.ts +6 -6
- package/src/scripts/hamburger.ts +55 -0
- package/src/scripts/nav-menu.ts +119 -0
package/src/NavMenu.astro
CHANGED
|
@@ -3,33 +3,13 @@ import type { MenuItem, SecondaryMenuItem, TertiaryMenuItem } from "./index.js";
|
|
|
3
3
|
import ChevronIcon from "./ChevronIcon.astro";
|
|
4
4
|
|
|
5
5
|
export interface Props {
|
|
6
|
-
/**
|
|
7
|
-
* Header type for styling.
|
|
8
|
-
* @example "floating"
|
|
9
|
-
*/
|
|
10
6
|
type?: "floating" | "fullscreen";
|
|
11
|
-
/** Array of menu items to display. */
|
|
12
7
|
menuItems?: MenuItem[];
|
|
13
|
-
/**
|
|
14
|
-
* Whether to show the home link in the menu.
|
|
15
|
-
* @default true
|
|
16
|
-
*/
|
|
17
8
|
showHomeLink?: boolean;
|
|
18
|
-
/**
|
|
19
|
-
* Text for the home link.
|
|
20
|
-
* @default "Home"
|
|
21
|
-
*/
|
|
22
9
|
homeText?: string;
|
|
23
|
-
/**
|
|
24
|
-
* The destination URL for the home link.
|
|
25
|
-
* @default "/"
|
|
26
|
-
*/
|
|
27
10
|
homeUrl?: string;
|
|
28
|
-
/** Extra CSS class(es) for the `<nav>` element. */
|
|
29
11
|
header__menu__class?: string;
|
|
30
|
-
/** Extra CSS class(es) for each top-level `<li>` item. */
|
|
31
12
|
header__item__class?: string;
|
|
32
|
-
/** Extra CSS class(es) for each top-level menu `<a>` link. */
|
|
33
13
|
menu__link__class?: string;
|
|
34
14
|
}
|
|
35
15
|
|
|
@@ -47,105 +27,9 @@ const pagePathname = Astro.url.pathname;
|
|
|
47
27
|
---
|
|
48
28
|
|
|
49
29
|
<script>
|
|
50
|
-
|
|
51
|
-
const subsubmenus = document.querySelectorAll(
|
|
52
|
-
".subsubmenu",
|
|
53
|
-
) as NodeListOf<HTMLElement>;
|
|
54
|
-
|
|
55
|
-
subsubmenus.forEach((subsubmenu) => {
|
|
56
|
-
const rect = subsubmenu.getBoundingClientRect();
|
|
57
|
-
const viewportWidth = window.innerWidth;
|
|
58
|
-
|
|
59
|
-
if (rect.right > viewportWidth) {
|
|
60
|
-
subsubmenu.style.right = "100%";
|
|
61
|
-
subsubmenu.style.left = "0";
|
|
62
|
-
} else {
|
|
63
|
-
subsubmenu.style.left = "100%";
|
|
64
|
-
subsubmenu.style.right = "0";
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
}
|
|
30
|
+
import { initNavMenu } from "./scripts/nav-menu.js";
|
|
68
31
|
|
|
69
|
-
|
|
70
|
-
const parentItems = document.querySelectorAll(".submenu__item--secondary");
|
|
71
|
-
|
|
72
|
-
parentItems.forEach((parentItem) => {
|
|
73
|
-
const subsubmenu = parentItem.querySelector(".subsubmenu");
|
|
74
|
-
|
|
75
|
-
if (subsubmenu) {
|
|
76
|
-
const adjustPosition = () => {
|
|
77
|
-
const viewportWidth = window.innerWidth;
|
|
78
|
-
|
|
79
|
-
const originalDisplay = (subsubmenu as HTMLElement).style.display;
|
|
80
|
-
const originalVisibility = (subsubmenu as HTMLElement).style
|
|
81
|
-
.visibility;
|
|
82
|
-
|
|
83
|
-
(subsubmenu as HTMLElement).style.display = "block";
|
|
84
|
-
(subsubmenu as HTMLElement).style.visibility = "hidden";
|
|
85
|
-
(subsubmenu as HTMLElement).style.left = "100%";
|
|
86
|
-
(subsubmenu as HTMLElement).style.transform = "auto";
|
|
87
|
-
|
|
88
|
-
const rect = subsubmenu.getBoundingClientRect();
|
|
89
|
-
|
|
90
|
-
if (rect.right > viewportWidth) {
|
|
91
|
-
(subsubmenu as HTMLElement).style.left = "-100%";
|
|
92
|
-
(subsubmenu as HTMLElement).style.right = "auto";
|
|
93
|
-
const leftRect = subsubmenu.getBoundingClientRect();
|
|
94
|
-
|
|
95
|
-
if (leftRect.left < 0) {
|
|
96
|
-
(subsubmenu as HTMLElement).style.left = "100%";
|
|
97
|
-
const overflow = rect.right - viewportWidth + 30;
|
|
98
|
-
(subsubmenu as HTMLElement).style.transform =
|
|
99
|
-
`translateX(-${overflow}px)`;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
(subsubmenu as HTMLElement).style.display = originalDisplay;
|
|
103
|
-
(subsubmenu as HTMLElement).style.visibility = originalVisibility;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const showSubsubmenu = () => {
|
|
107
|
-
adjustPosition();
|
|
108
|
-
|
|
109
|
-
(subsubmenu as HTMLElement).style.opacity = "1";
|
|
110
|
-
(subsubmenu as HTMLElement).style.pointerEvents = "all";
|
|
111
|
-
|
|
112
|
-
const isPositionedLeft =
|
|
113
|
-
(subsubmenu as HTMLElement).style.left === "-100%";
|
|
114
|
-
const transformDirection = isPositionedLeft ? "-32px" : "32px";
|
|
115
|
-
|
|
116
|
-
const currentTransform = (subsubmenu as HTMLElement).style.transform;
|
|
117
|
-
if (currentTransform && currentTransform.includes("translateX")) {
|
|
118
|
-
const existingTranslate = currentTransform.match(
|
|
119
|
-
/translateX\(([^)]+)\)/,
|
|
120
|
-
);
|
|
121
|
-
if (existingTranslate) {
|
|
122
|
-
const existingValue = existingTranslate[1];
|
|
123
|
-
(subsubmenu as HTMLElement).style.transform =
|
|
124
|
-
`translateX(calc(${existingValue} + ${transformDirection}))`;
|
|
125
|
-
}
|
|
126
|
-
} else {
|
|
127
|
-
(subsubmenu as HTMLElement).style.transform =
|
|
128
|
-
`${currentTransform} translateX(${transformDirection})`.trim();
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const hideSubsubmenu = () => {
|
|
133
|
-
(subsubmenu as HTMLElement).style.opacity = "";
|
|
134
|
-
(subsubmenu as HTMLElement).style.pointerEvents = "";
|
|
135
|
-
(subsubmenu as HTMLElement).style.transform = "";
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
parentItem.addEventListener("mouseenter", showSubsubmenu);
|
|
139
|
-
parentItem.addEventListener("mouseleave", hideSubsubmenu);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
window.addEventListener("resize", adjustSubsubmenuPositionOnResize);
|
|
145
|
-
document.addEventListener("DOMContentLoaded", () => {
|
|
146
|
-
adjustSubsubmenuPositionOnResize();
|
|
147
|
-
handleSubsubmenuInteraction();
|
|
148
|
-
});
|
|
32
|
+
initNavMenu();
|
|
149
33
|
</script>
|
|
150
34
|
|
|
151
35
|
<nav class:list={["header__menu", header__menu__class]} id="header-menu">
|
|
@@ -205,187 +89,195 @@ const pagePathname = Astro.url.pathname;
|
|
|
205
89
|
</ul>
|
|
206
90
|
</nav>
|
|
207
91
|
|
|
208
|
-
<style
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
92
|
+
<style>
|
|
93
|
+
/*
|
|
94
|
+
* base (resets) < components (ours) < utilities (consumer classNames).
|
|
95
|
+
* Order statement keeps it deterministic regardless of bundling order.
|
|
96
|
+
*/
|
|
97
|
+
@layer theme, base, components, utilities;
|
|
98
|
+
|
|
99
|
+
@layer components {
|
|
100
|
+
.header__menu {
|
|
101
|
+
@media (width < 768px) {
|
|
102
|
+
display: none;
|
|
103
|
+
}
|
|
212
104
|
}
|
|
213
|
-
}
|
|
214
105
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
106
|
+
.menu {
|
|
107
|
+
list-style: none;
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
flex-wrap: wrap;
|
|
220
111
|
|
|
221
|
-
|
|
222
|
-
|
|
112
|
+
@media (width < 768px) {
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
}
|
|
223
115
|
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
.menu__link {
|
|
227
|
-
text-decoration: none;
|
|
228
|
-
padding: 0.5em;
|
|
229
|
-
display: flex;
|
|
230
|
-
align-items: center;
|
|
231
|
-
gap: 0.8em;
|
|
232
|
-
color: color-mix(
|
|
233
|
-
in srgb,
|
|
234
|
-
var(--accent-color, #00ffff) 30%,
|
|
235
|
-
var(--text-color, #ffffff) 70%
|
|
236
|
-
);
|
|
237
|
-
position: relative;
|
|
238
|
-
}
|
|
239
116
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
117
|
+
.menu__link {
|
|
118
|
+
text-decoration: none;
|
|
119
|
+
padding: 0.5em;
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 0.8em;
|
|
123
|
+
color: color-mix(
|
|
124
|
+
in srgb,
|
|
125
|
+
var(--accent-color, #00ffff) 30%,
|
|
126
|
+
var(--text-color, #ffffff) 70%
|
|
127
|
+
);
|
|
128
|
+
position: relative;
|
|
252
129
|
}
|
|
253
130
|
|
|
254
|
-
|
|
255
|
-
|
|
131
|
+
@media screen and (min-width: 768px) {
|
|
132
|
+
.menu__link::after {
|
|
133
|
+
position: absolute;
|
|
134
|
+
top: 33px;
|
|
135
|
+
left: 0;
|
|
136
|
+
content: "";
|
|
137
|
+
width: 100%;
|
|
138
|
+
transform: scale(0);
|
|
139
|
+
transform-origin: 1;
|
|
140
|
+
height: 2px;
|
|
141
|
+
transition: transform 0.3s ease;
|
|
142
|
+
background: var(--accent-color, #00ffff);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.menu__link:hover::after {
|
|
146
|
+
transform: scale(1);
|
|
147
|
+
}
|
|
256
148
|
}
|
|
257
|
-
}
|
|
258
149
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
150
|
+
.ChevronIcon-arrow {
|
|
151
|
+
transition: transform 0.3s linear;
|
|
152
|
+
}
|
|
262
153
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
154
|
+
.menu__link:hover > .ChevronIcon-arrow {
|
|
155
|
+
transform: rotate(180deg);
|
|
156
|
+
}
|
|
266
157
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
158
|
+
.submenu,
|
|
159
|
+
.subsubmenu {
|
|
160
|
+
color: var(--text-color, #ffffff);
|
|
161
|
+
background-color: var(--bg-color-opaque, #171717);
|
|
162
|
+
border: 0.5px solid
|
|
163
|
+
color-mix(in srgb, var(--accent-color, #3e1c71) 30%, transparent);
|
|
164
|
+
position: absolute;
|
|
165
|
+
flex-direction: column;
|
|
166
|
+
gap: 0.4em;
|
|
167
|
+
padding: 1em;
|
|
168
|
+
border-radius: 10px;
|
|
169
|
+
transition: transform 0.3s ease-in-out;
|
|
170
|
+
pointer-events: none;
|
|
171
|
+
opacity: 0;
|
|
172
|
+
text-wrap: nowrap;
|
|
173
|
+
}
|
|
283
174
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
175
|
+
.menu__item {
|
|
176
|
+
padding: 6px;
|
|
177
|
+
position: relative;
|
|
178
|
+
list-style: none;
|
|
179
|
+
}
|
|
289
180
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
181
|
+
.submenu a,
|
|
182
|
+
.subsubmenu a {
|
|
183
|
+
display: flex;
|
|
184
|
+
justify-content: space-between;
|
|
185
|
+
align-items: center;
|
|
186
|
+
width: 100%;
|
|
187
|
+
height: 64px;
|
|
188
|
+
border-radius: 6px;
|
|
189
|
+
color: var(--text-color, #fff);
|
|
190
|
+
}
|
|
300
191
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
192
|
+
.submenu__item--secondary,
|
|
193
|
+
.submenu__item--tertiary {
|
|
194
|
+
position: relative;
|
|
195
|
+
}
|
|
305
196
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
197
|
+
.submenu__item--secondary a,
|
|
198
|
+
.submenu__item--tertiary a {
|
|
199
|
+
padding: 26px;
|
|
200
|
+
}
|
|
310
201
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
202
|
+
.submenu__item--secondary a:hover,
|
|
203
|
+
.submenu__item--tertiary a:hover {
|
|
204
|
+
background: color-mix(
|
|
205
|
+
in srgb,
|
|
206
|
+
var(--accent-color, #00ffff) 15%,
|
|
207
|
+
transparent
|
|
208
|
+
);
|
|
209
|
+
color: var(--text-color, white);
|
|
210
|
+
}
|
|
320
211
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
212
|
+
.submenu {
|
|
213
|
+
padding: 0.9em;
|
|
214
|
+
right: calc(50% - 80px);
|
|
215
|
+
}
|
|
325
216
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
217
|
+
.submenu::after {
|
|
218
|
+
content: "";
|
|
219
|
+
position: absolute;
|
|
220
|
+
top: -1.1em;
|
|
221
|
+
width: 100%;
|
|
222
|
+
z-index: -3;
|
|
223
|
+
pointer-events: none;
|
|
224
|
+
height: 7em;
|
|
225
|
+
right: 0;
|
|
226
|
+
}
|
|
336
227
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
228
|
+
.subsubmenu {
|
|
229
|
+
top: 0;
|
|
230
|
+
left: auto;
|
|
231
|
+
right: auto;
|
|
232
|
+
min-width: 193px;
|
|
233
|
+
}
|
|
343
234
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
235
|
+
.subsubmenu::before {
|
|
236
|
+
content: "";
|
|
237
|
+
position: absolute;
|
|
238
|
+
top: -4em;
|
|
239
|
+
z-index: -1;
|
|
240
|
+
pointer-events: none;
|
|
241
|
+
height: 10em;
|
|
242
|
+
width: 20em;
|
|
243
|
+
height: 14em;
|
|
244
|
+
}
|
|
354
245
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
246
|
+
.subsubmenu::after {
|
|
247
|
+
content: "";
|
|
248
|
+
position: absolute;
|
|
249
|
+
top: -14px;
|
|
250
|
+
right: 3em;
|
|
251
|
+
width: 100%;
|
|
252
|
+
z-index: -3;
|
|
253
|
+
height: 21em;
|
|
254
|
+
}
|
|
364
255
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
256
|
+
.submenu li {
|
|
257
|
+
width: 100%;
|
|
258
|
+
}
|
|
368
259
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
260
|
+
.menu__item:hover > .submenu {
|
|
261
|
+
opacity: 1;
|
|
262
|
+
pointer-events: all;
|
|
263
|
+
transform: translateY(1.4em);
|
|
264
|
+
}
|
|
374
265
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
266
|
+
.menu__item:hover > .submenu::after {
|
|
267
|
+
pointer-events: all;
|
|
268
|
+
}
|
|
378
269
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
270
|
+
.submenu li:hover > .subsubmenu {
|
|
271
|
+
opacity: 1;
|
|
272
|
+
pointer-events: all;
|
|
273
|
+
}
|
|
383
274
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
275
|
+
.submenu li:hover > .subsubmenu::before {
|
|
276
|
+
pointer-events: all;
|
|
277
|
+
}
|
|
387
278
|
|
|
388
|
-
|
|
389
|
-
|
|
279
|
+
.submenu li:hover > .subsubmenu::after {
|
|
280
|
+
pointer-events: all;
|
|
281
|
+
}
|
|
390
282
|
}
|
|
391
283
|
</style>
|
package/src/SunIcon.astro
CHANGED
|
@@ -15,20 +15,28 @@ const content = `<g class="gutruu"><path fill-opacity="0" class="xkoryc"><animat
|
|
|
15
15
|
/>
|
|
16
16
|
|
|
17
17
|
<style is:inline>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
/*
|
|
19
|
+
* base (resets) < components (ours) < utilities (consumer classNames).
|
|
20
|
+
* Order statement keeps it deterministic regardless of bundling order.
|
|
21
|
+
*/
|
|
22
|
+
@layer theme, base, components, utilities;
|
|
23
|
+
|
|
24
|
+
@layer components {
|
|
25
|
+
.gutruu {
|
|
26
|
+
stroke: currentColor;
|
|
27
|
+
stroke-linecap: round;
|
|
28
|
+
stroke-linejoin: round;
|
|
29
|
+
stroke-width: var(--svg-stroke-width--2px, 2px);
|
|
30
|
+
}
|
|
31
|
+
.xkoryc {
|
|
32
|
+
fill: currentColor;
|
|
33
|
+
stroke-dasharray: 34;
|
|
34
|
+
d: path(
|
|
35
|
+
"M12 7c2.76 0 5 2.24 5 5c0 2.76 -2.24 5 -5 5c-2.76 0 -5 -2.24 -5 -5c0 -2.76 2.24 -5 5 -5Z"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
.ft5dvn {
|
|
39
|
+
fill: none;
|
|
40
|
+
}
|
|
33
41
|
}
|
|
34
42
|
</style>
|
package/src/index.ts
CHANGED
|
@@ -129,8 +129,12 @@ export interface DualThemeConfig {
|
|
|
129
129
|
* Custom CSS class names for high-level layout & appearance customization.
|
|
130
130
|
*
|
|
131
131
|
* These target the structural wrapper elements of the Header. For fine-grained
|
|
132
|
-
* control over
|
|
133
|
-
*
|
|
132
|
+
* control over the nav items use the `xxx__class` props inside `navigation`,
|
|
133
|
+
* and style the logo directly on the markup you pass to the `logo` slot.
|
|
134
|
+
*
|
|
135
|
+
* The component styles live in `@layer components`, so any class passed here
|
|
136
|
+
* (Tailwind utilities included) wins over the built-in styles — no
|
|
137
|
+
* `!important` and no `twMerge()` required.
|
|
134
138
|
*
|
|
135
139
|
* @example
|
|
136
140
|
* ```astro
|
|
@@ -142,10 +146,6 @@ export interface HeaderClassNames {
|
|
|
142
146
|
container?: string;
|
|
143
147
|
/** Inner `<header>` element — best place for shadows, borders, transitions. */
|
|
144
148
|
header?: string;
|
|
145
|
-
/** Logo anchor `<a>` — add hover states or focus rings here. */
|
|
146
|
-
logo?: string;
|
|
147
|
-
/** Logo text `<span>` — override typography here. */
|
|
148
|
-
logoText?: string;
|
|
149
149
|
/** Desktop nav wrapper `<div>` — adjust spacing between logo and menu. */
|
|
150
150
|
nav?: string;
|
|
151
151
|
/** Mobile nav panel `<nav>` — add slide-in overrides or z-index tweaks. */
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behaviour for the hamburger button + mobile panel.
|
|
3
|
+
* Extracted from `Header.astro` so it can be unit tested.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class HamburgerController {
|
|
7
|
+
private readonly hamburgerBtn: HTMLElement | null;
|
|
8
|
+
private readonly mobileMenu: HTMLElement | null;
|
|
9
|
+
private readonly links: NodeListOf<HTMLElement>;
|
|
10
|
+
|
|
11
|
+
constructor(doc: Document = document) {
|
|
12
|
+
this.hamburgerBtn = doc.getElementById("hamburger-btn");
|
|
13
|
+
this.mobileMenu = doc.getElementById("mobile-header-menu");
|
|
14
|
+
this.links = doc.querySelectorAll(".mobile-menu__link");
|
|
15
|
+
this.init();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private init(): void {
|
|
19
|
+
if (!this.hamburgerBtn || !this.mobileMenu) return;
|
|
20
|
+
|
|
21
|
+
this.hamburgerBtn.addEventListener("click", () => this.toggleMenu());
|
|
22
|
+
this.links.forEach((link) => {
|
|
23
|
+
link.addEventListener("click", () => this.closeMenu());
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
closeMenu(): void {
|
|
28
|
+
this.hamburgerBtn?.classList.remove("is-active");
|
|
29
|
+
this.mobileMenu?.classList.remove("is-active");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
toggleMenu(): void {
|
|
33
|
+
this.hamburgerBtn?.classList.toggle("is-active");
|
|
34
|
+
this.mobileMenu?.classList.toggle("is-active");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Wires the hamburger as soon as the DOM is ready (module scripts may run
|
|
40
|
+
* before or after `DOMContentLoaded`, so both paths are handled).
|
|
41
|
+
*/
|
|
42
|
+
export function initHamburger(doc: Document = document): HamburgerController | null {
|
|
43
|
+
if (doc.readyState === "loading") {
|
|
44
|
+
let controller: HamburgerController | null = null;
|
|
45
|
+
doc.addEventListener(
|
|
46
|
+
"DOMContentLoaded",
|
|
47
|
+
() => {
|
|
48
|
+
controller = new HamburgerController(doc);
|
|
49
|
+
},
|
|
50
|
+
{ once: true },
|
|
51
|
+
);
|
|
52
|
+
return controller;
|
|
53
|
+
}
|
|
54
|
+
return new HamburgerController(doc);
|
|
55
|
+
}
|