@ecl/site-header 5.0.0-alpha.1
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/LICENSE +296 -0
- package/README.md +123 -0
- package/_site-header-language-switcher.scss +315 -0
- package/package.json +43 -0
- package/site-header-ec.scss +514 -0
- package/site-header-eu.scss +505 -0
- package/site-header-language-switcher.html.twig +181 -0
- package/site-header-print.scss +48 -0
- package/site-header.html.twig +434 -0
- package/site-header.js +683 -0
package/site-header.js
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { queryOne, queryAll } from '@ecl/dom-utils';
|
|
2
|
+
import { createFocusTrap } from 'focus-trap';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {HTMLElement} element DOM element for component instantiation and scope
|
|
6
|
+
* @param {Object} options
|
|
7
|
+
* @param {String} options.languageLinkSelector
|
|
8
|
+
* @param {String} options.languageListOverlaySelector
|
|
9
|
+
* @param {String} options.languageListEuSelector
|
|
10
|
+
* @param {String} options.languageListNonEuSelector
|
|
11
|
+
* @param {String} options.closeOverlaySelector
|
|
12
|
+
* @param {String} options.searchToggleSelector
|
|
13
|
+
* @param {String} options.searchFormSelector
|
|
14
|
+
* @param {String} options.loginToggleSelector
|
|
15
|
+
* @param {String} options.loginBoxSelector
|
|
16
|
+
* @param {integer} options.tabletBreakpoint
|
|
17
|
+
* @param {Boolean} options.attachClickListener Whether or not to bind click events
|
|
18
|
+
* @param {Boolean} options.attachKeyListener Whether or not to bind keyboard events
|
|
19
|
+
* @param {Boolean} options.attachResizeListener Whether or not to bind resize events
|
|
20
|
+
*/
|
|
21
|
+
export class SiteHeader {
|
|
22
|
+
/**
|
|
23
|
+
* @static
|
|
24
|
+
* Shorthand for instance creation and initialisation.
|
|
25
|
+
*
|
|
26
|
+
* @param {HTMLElement} root DOM element for component instantiation and scope
|
|
27
|
+
*
|
|
28
|
+
* @return {SiteHeader} An instance of SiteHeader.
|
|
29
|
+
*/
|
|
30
|
+
static autoInit(root, { SITE_HEADER_CORE: defaultOptions = {} } = {}) {
|
|
31
|
+
const siteHeader = new SiteHeader(root, defaultOptions);
|
|
32
|
+
siteHeader.init();
|
|
33
|
+
root.ECLSiteHeader = siteHeader;
|
|
34
|
+
return siteHeader;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
element,
|
|
39
|
+
{
|
|
40
|
+
containerSelector = '[data-ecl-site-header-top]',
|
|
41
|
+
languageLinkSelector = '[data-ecl-language-selector]',
|
|
42
|
+
languageListOverlaySelector = '[data-ecl-language-list-overlay]',
|
|
43
|
+
languageListEuSelector = '[data-ecl-language-list-eu]',
|
|
44
|
+
languageListNonEuSelector = '[data-ecl-language-list-non-eu]',
|
|
45
|
+
languageListContentSelector = '[data-ecl-language-list-content]',
|
|
46
|
+
closeOverlaySelector = '[data-ecl-language-list-close]',
|
|
47
|
+
searchToggleSelector = '[data-ecl-search-toggle]',
|
|
48
|
+
searchFormSelector = '[data-ecl-search-form]',
|
|
49
|
+
loginToggleSelector = '[data-ecl-login-toggle]',
|
|
50
|
+
loginBoxSelector = '[data-ecl-login-box]',
|
|
51
|
+
notificationSelector = '[data-ecl-site-header-notification]',
|
|
52
|
+
attachClickListener = true,
|
|
53
|
+
attachKeyListener = true,
|
|
54
|
+
attachResizeListener = true,
|
|
55
|
+
tabletBreakpoint = 768,
|
|
56
|
+
} = {},
|
|
57
|
+
) {
|
|
58
|
+
// Check element
|
|
59
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
60
|
+
throw new TypeError(
|
|
61
|
+
'DOM element should be given to initialize this widget.',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.element = element;
|
|
66
|
+
|
|
67
|
+
// Options
|
|
68
|
+
this.containerSelector = containerSelector;
|
|
69
|
+
this.languageLinkSelector = languageLinkSelector;
|
|
70
|
+
this.languageListOverlaySelector = languageListOverlaySelector;
|
|
71
|
+
this.languageListEuSelector = languageListEuSelector;
|
|
72
|
+
this.languageListNonEuSelector = languageListNonEuSelector;
|
|
73
|
+
this.languageListContentSelector = languageListContentSelector;
|
|
74
|
+
this.closeOverlaySelector = closeOverlaySelector;
|
|
75
|
+
this.searchToggleSelector = searchToggleSelector;
|
|
76
|
+
this.searchFormSelector = searchFormSelector;
|
|
77
|
+
this.loginToggleSelector = loginToggleSelector;
|
|
78
|
+
this.notificationSelector = notificationSelector;
|
|
79
|
+
this.loginBoxSelector = loginBoxSelector;
|
|
80
|
+
this.attachClickListener = attachClickListener;
|
|
81
|
+
this.attachKeyListener = attachKeyListener;
|
|
82
|
+
this.attachResizeListener = attachResizeListener;
|
|
83
|
+
this.tabletBreakpoint = tabletBreakpoint;
|
|
84
|
+
|
|
85
|
+
// Private variables
|
|
86
|
+
this.languageMaxColumnItems = 8;
|
|
87
|
+
this.languageLink = null;
|
|
88
|
+
this.languageListOverlay = null;
|
|
89
|
+
this.languageListEu = null;
|
|
90
|
+
this.languageListNonEu = null;
|
|
91
|
+
this.languageListContent = null;
|
|
92
|
+
this.close = null;
|
|
93
|
+
this.focusTrap = null;
|
|
94
|
+
this.searchToggle = null;
|
|
95
|
+
this.searchForm = null;
|
|
96
|
+
this.loginToggle = null;
|
|
97
|
+
this.loginBox = null;
|
|
98
|
+
this.resizeTimer = null;
|
|
99
|
+
this.direction = null;
|
|
100
|
+
this.notificationContainer = null;
|
|
101
|
+
|
|
102
|
+
// Bind `this` for use in callbacks
|
|
103
|
+
this.openOverlay = this.openOverlay.bind(this);
|
|
104
|
+
this.closeOverlay = this.closeOverlay.bind(this);
|
|
105
|
+
this.toggleOverlay = this.toggleOverlay.bind(this);
|
|
106
|
+
this.toggleSearch = this.toggleSearch.bind(this);
|
|
107
|
+
this.toggleLogin = this.toggleLogin.bind(this);
|
|
108
|
+
this.setLoginArrow = this.setLoginArrow.bind(this);
|
|
109
|
+
this.setSearchArrow = this.setSearchArrow.bind(this);
|
|
110
|
+
this.handleKeyboardLanguage = this.handleKeyboardLanguage.bind(this);
|
|
111
|
+
this.handleKeyboardGlobal = this.handleKeyboardGlobal.bind(this);
|
|
112
|
+
this.handleClickGlobal = this.handleClickGlobal.bind(this);
|
|
113
|
+
this.handleResize = this.handleResize.bind(this);
|
|
114
|
+
this.setLanguageListHeight = this.setLanguageListHeight.bind(this);
|
|
115
|
+
this.handleNotificationClose = this.handleNotificationClose.bind(this);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Initialise component.
|
|
120
|
+
*/
|
|
121
|
+
init() {
|
|
122
|
+
if (!ECL) {
|
|
123
|
+
throw new TypeError('Called init but ECL is not present');
|
|
124
|
+
}
|
|
125
|
+
ECL.components = ECL.components || new Map();
|
|
126
|
+
this.arrowSize = '0.5rem';
|
|
127
|
+
// Bind global events
|
|
128
|
+
if (this.attachKeyListener) {
|
|
129
|
+
document.addEventListener('keyup', this.handleKeyboardGlobal);
|
|
130
|
+
}
|
|
131
|
+
if (this.attachClickListener) {
|
|
132
|
+
document.addEventListener('click', this.handleClickGlobal);
|
|
133
|
+
}
|
|
134
|
+
if (this.attachResizeListener) {
|
|
135
|
+
window.addEventListener('resize', this.handleResize);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Site header elements
|
|
139
|
+
this.container = queryOne(this.containerSelector);
|
|
140
|
+
|
|
141
|
+
// Language list management
|
|
142
|
+
this.languageLink = queryOne(this.languageLinkSelector);
|
|
143
|
+
this.languageListOverlay = queryOne(this.languageListOverlaySelector);
|
|
144
|
+
this.languageListEu = queryOne(this.languageListEuSelector);
|
|
145
|
+
this.languageListNonEu = queryOne(this.languageListNonEuSelector);
|
|
146
|
+
this.languageListContent = queryOne(this.languageListContentSelector);
|
|
147
|
+
this.close = queryOne(this.closeOverlaySelector);
|
|
148
|
+
this.notification = queryOne(this.notificationSelector);
|
|
149
|
+
|
|
150
|
+
// direction
|
|
151
|
+
this.direction = getComputedStyle(this.element).direction;
|
|
152
|
+
if (this.direction === 'rtl') {
|
|
153
|
+
this.element.classList.add('ecl-site-header--rtl');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Create focus trap
|
|
157
|
+
this.focusTrap = createFocusTrap(this.languageListOverlay, {
|
|
158
|
+
onDeactivate: this.closeOverlay,
|
|
159
|
+
allowOutsideClick: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (this.attachClickListener && this.languageLink) {
|
|
163
|
+
this.languageLink.addEventListener('click', this.toggleOverlay);
|
|
164
|
+
}
|
|
165
|
+
if (this.attachClickListener && this.close) {
|
|
166
|
+
this.close.addEventListener('click', this.toggleOverlay);
|
|
167
|
+
}
|
|
168
|
+
if (this.attachKeyListener && this.languageLink) {
|
|
169
|
+
this.languageLink.addEventListener(
|
|
170
|
+
'keydown',
|
|
171
|
+
this.handleKeyboardLanguage,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Search form management
|
|
176
|
+
this.searchToggle = queryOne(this.searchToggleSelector);
|
|
177
|
+
this.searchForm = queryOne(this.searchFormSelector);
|
|
178
|
+
|
|
179
|
+
if (this.attachClickListener && this.searchToggle) {
|
|
180
|
+
this.searchToggle.addEventListener('click', this.toggleSearch);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Login management
|
|
184
|
+
this.loginToggle = queryOne(this.loginToggleSelector);
|
|
185
|
+
this.loginBox = queryOne(this.loginBoxSelector);
|
|
186
|
+
|
|
187
|
+
if (this.attachClickListener && this.loginToggle) {
|
|
188
|
+
this.loginToggle.addEventListener('click', this.toggleLogin);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Set ecl initialized attribute
|
|
192
|
+
this.element.setAttribute('data-ecl-auto-initialized', 'true');
|
|
193
|
+
ECL.components.set(this.element, this);
|
|
194
|
+
|
|
195
|
+
if (this.notification) {
|
|
196
|
+
this.notificationContainer = this.notification.closest(
|
|
197
|
+
'.ecl-site-header__notification',
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
const eclNotification = ECL.components.get(this.notification);
|
|
202
|
+
|
|
203
|
+
if (eclNotification) {
|
|
204
|
+
eclNotification.on('onClose', this.handleNotificationClose);
|
|
205
|
+
}
|
|
206
|
+
}, 0);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Destroy component.
|
|
212
|
+
*/
|
|
213
|
+
destroy() {
|
|
214
|
+
if (this.attachClickListener && this.languageLink) {
|
|
215
|
+
this.languageLink.removeEventListener('click', this.toggleOverlay);
|
|
216
|
+
}
|
|
217
|
+
if (this.focusTrap) {
|
|
218
|
+
this.focusTrap.deactivate();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (this.attachKeyListener && this.languageLink) {
|
|
222
|
+
this.languageLink.removeEventListener(
|
|
223
|
+
'keydown',
|
|
224
|
+
this.handleKeyboardLanguage,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (this.attachClickListener && this.close) {
|
|
229
|
+
this.close.removeEventListener('click', this.toggleOverlay);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (this.attachClickListener && this.searchToggle) {
|
|
233
|
+
this.searchToggle.removeEventListener('click', this.toggleSearch);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (this.attachClickListener && this.loginToggle) {
|
|
237
|
+
this.loginToggle.removeEventListener('click', this.toggleLogin);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (this.attachKeyListener) {
|
|
241
|
+
document.removeEventListener('keyup', this.handleKeyboardGlobal);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (this.attachClickListener) {
|
|
245
|
+
document.removeEventListener('click', this.handleClickGlobal);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (this.attachResizeListener) {
|
|
249
|
+
window.removeEventListener('resize', this.handleResize);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (this.element) {
|
|
253
|
+
this.element.removeAttribute('data-ecl-auto-initialized');
|
|
254
|
+
this.element.classList.remove('ecl-site-header--rtl');
|
|
255
|
+
ECL.components.delete(this.element);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Update display of the modal language list overlay.
|
|
261
|
+
*/
|
|
262
|
+
updateOverlay() {
|
|
263
|
+
// Check number of items and adapt display
|
|
264
|
+
let columnsEu = 1;
|
|
265
|
+
let columnsNonEu = 1;
|
|
266
|
+
if (this.languageListEu) {
|
|
267
|
+
// Get all Eu languages
|
|
268
|
+
const itemsEu = queryAll(
|
|
269
|
+
'.ecl-site-header__language-item',
|
|
270
|
+
this.languageListEu,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Calculate number of columns
|
|
274
|
+
columnsEu = Math.ceil(itemsEu.length / this.languageMaxColumnItems);
|
|
275
|
+
|
|
276
|
+
// Apply column display
|
|
277
|
+
if (columnsEu > 1) {
|
|
278
|
+
this.languageListEu.classList.add(
|
|
279
|
+
`ecl-site-header__language-category--${columnsEu}-col`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (this.languageListNonEu) {
|
|
284
|
+
// Get all non-Eu languages
|
|
285
|
+
const itemsNonEu = queryAll(
|
|
286
|
+
'.ecl-site-header__language-item',
|
|
287
|
+
this.languageListNonEu,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Calculate number of columns
|
|
291
|
+
columnsNonEu = Math.ceil(itemsNonEu.length / this.languageMaxColumnItems);
|
|
292
|
+
|
|
293
|
+
// Apply column display
|
|
294
|
+
if (columnsNonEu > 1) {
|
|
295
|
+
this.languageListNonEu.classList.add(
|
|
296
|
+
`ecl-site-header__language-category--${columnsNonEu}-col`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check total width, and change display if needed
|
|
302
|
+
if (this.languageListEu) {
|
|
303
|
+
this.languageListEu.parentNode.classList.remove(
|
|
304
|
+
'ecl-site-header__language-content--stack',
|
|
305
|
+
);
|
|
306
|
+
} else if (this.languageListNonEu) {
|
|
307
|
+
this.languageListNonEu.parentNode.classList.remove(
|
|
308
|
+
'ecl-site-header__language-content--stack',
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
let popoverRect = this.languageListOverlay.getBoundingClientRect();
|
|
312
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
313
|
+
|
|
314
|
+
if (popoverRect.width > containerRect.width) {
|
|
315
|
+
// Stack elements
|
|
316
|
+
if (this.languageListEu) {
|
|
317
|
+
this.languageListEu.parentNode.classList.add(
|
|
318
|
+
'ecl-site-header__language-content--stack',
|
|
319
|
+
);
|
|
320
|
+
} else if (this.languageListNonEu) {
|
|
321
|
+
this.languageListNonEu.parentNode.classList.add(
|
|
322
|
+
'ecl-site-header__language-content--stack',
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Adapt column display
|
|
327
|
+
if (this.languageListNonEu) {
|
|
328
|
+
this.languageListNonEu.classList.remove(
|
|
329
|
+
`ecl-site-header__language-category--${columnsNonEu}-col`,
|
|
330
|
+
);
|
|
331
|
+
this.languageListNonEu.classList.add(
|
|
332
|
+
`ecl-site-header__language-category--${Math.max(
|
|
333
|
+
columnsEu,
|
|
334
|
+
columnsNonEu,
|
|
335
|
+
)}-col`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check available space
|
|
341
|
+
this.languageListOverlay.classList.remove(
|
|
342
|
+
'ecl-site-header__language-container--push-right',
|
|
343
|
+
'ecl-site-header__language-container--push-left',
|
|
344
|
+
);
|
|
345
|
+
this.languageListOverlay.classList.remove(
|
|
346
|
+
'ecl-site-header__language-container--full',
|
|
347
|
+
);
|
|
348
|
+
this.languageListOverlay.style.removeProperty(
|
|
349
|
+
'--ecl-language-arrow-position',
|
|
350
|
+
);
|
|
351
|
+
this.languageListOverlay.style.removeProperty('right');
|
|
352
|
+
this.languageListOverlay.style.removeProperty('left');
|
|
353
|
+
|
|
354
|
+
popoverRect = this.languageListOverlay.getBoundingClientRect();
|
|
355
|
+
const screenWidth = window.innerWidth;
|
|
356
|
+
const linkRect = this.languageLink.getBoundingClientRect();
|
|
357
|
+
// Popover too large
|
|
358
|
+
if (this.direction === 'ltr' && popoverRect.right > screenWidth) {
|
|
359
|
+
// Push the popover to the right
|
|
360
|
+
this.languageListOverlay.classList.add(
|
|
361
|
+
'ecl-site-header__language-container--push-right',
|
|
362
|
+
);
|
|
363
|
+
this.languageListOverlay.style.setProperty(
|
|
364
|
+
'right',
|
|
365
|
+
`calc(-${containerRect.right}px + ${linkRect.right}px)`,
|
|
366
|
+
);
|
|
367
|
+
// Adapt arrow position
|
|
368
|
+
const arrowPosition =
|
|
369
|
+
containerRect.right - linkRect.right + linkRect.width / 2;
|
|
370
|
+
this.languageListOverlay.style.setProperty(
|
|
371
|
+
'--ecl-language-arrow-position',
|
|
372
|
+
`calc(${arrowPosition}px - ${this.arrowSize})`,
|
|
373
|
+
);
|
|
374
|
+
} else if (this.direction === 'rtl' && popoverRect.left < 0) {
|
|
375
|
+
this.languageListOverlay.classList.add(
|
|
376
|
+
'ecl-site-header__language-container--push-left',
|
|
377
|
+
);
|
|
378
|
+
this.languageListOverlay.style.setProperty(
|
|
379
|
+
'left',
|
|
380
|
+
`calc(-${linkRect.left}px + ${containerRect.left}px)`,
|
|
381
|
+
);
|
|
382
|
+
// Adapt arrow position
|
|
383
|
+
const arrowPosition =
|
|
384
|
+
linkRect.right - containerRect.left - linkRect.width / 2;
|
|
385
|
+
this.languageListOverlay.style.setProperty(
|
|
386
|
+
'--ecl-language-arrow-position',
|
|
387
|
+
`${arrowPosition}px`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Mobile popover (full width)
|
|
392
|
+
if (window.innerWidth < this.tabletBreakpoint) {
|
|
393
|
+
// Push the popover to the right
|
|
394
|
+
this.languageListOverlay.classList.add(
|
|
395
|
+
'ecl-site-header__language-container--full',
|
|
396
|
+
);
|
|
397
|
+
this.languageListOverlay.style.removeProperty('right');
|
|
398
|
+
|
|
399
|
+
// Adapt arrow position
|
|
400
|
+
const arrowPosition =
|
|
401
|
+
popoverRect.right - linkRect.right + linkRect.width / 2;
|
|
402
|
+
this.languageListOverlay.style.setProperty(
|
|
403
|
+
'--ecl-language-arrow-position',
|
|
404
|
+
`calc(${arrowPosition}px - ${this.arrowSize})`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (
|
|
409
|
+
this.loginBox &&
|
|
410
|
+
this.loginBox.classList.contains('ecl-site-header__login-box--active')
|
|
411
|
+
) {
|
|
412
|
+
this.setLoginArrow();
|
|
413
|
+
}
|
|
414
|
+
if (
|
|
415
|
+
this.searchForm &&
|
|
416
|
+
this.searchForm.classList.contains('ecl-site-header__search--active')
|
|
417
|
+
) {
|
|
418
|
+
this.setSearchArrow();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Removes the containers of the notification element
|
|
424
|
+
*/
|
|
425
|
+
handleNotificationClose() {
|
|
426
|
+
if (this.notificationContainer) {
|
|
427
|
+
this.notificationContainer.remove();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Set a max height for the language list content
|
|
433
|
+
*/
|
|
434
|
+
setLanguageListHeight() {
|
|
435
|
+
const viewportHeight = window.innerHeight;
|
|
436
|
+
|
|
437
|
+
if (this.languageListContent) {
|
|
438
|
+
const listTop = this.languageListContent.getBoundingClientRect().top;
|
|
439
|
+
|
|
440
|
+
const availableSpace = viewportHeight - listTop;
|
|
441
|
+
if (availableSpace > 0) {
|
|
442
|
+
this.languageListContent.style.maxHeight = `${availableSpace}px`;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Shows the modal language list overlay.
|
|
449
|
+
*/
|
|
450
|
+
openOverlay() {
|
|
451
|
+
// Display language list
|
|
452
|
+
this.languageListOverlay.hidden = false;
|
|
453
|
+
this.languageListOverlay.setAttribute('aria-modal', 'true');
|
|
454
|
+
this.languageLink.setAttribute('aria-expanded', 'true');
|
|
455
|
+
this.setLanguageListHeight();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Hides the modal language list overlay.
|
|
460
|
+
*/
|
|
461
|
+
closeOverlay() {
|
|
462
|
+
this.languageListOverlay.hidden = true;
|
|
463
|
+
this.languageListOverlay.removeAttribute('aria-modal');
|
|
464
|
+
this.languageLink.setAttribute('aria-expanded', 'false');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Toggles the modal language list overlay.
|
|
469
|
+
*
|
|
470
|
+
* @param {Event} e
|
|
471
|
+
*/
|
|
472
|
+
toggleOverlay(e) {
|
|
473
|
+
if (!this.languageListOverlay || !this.focusTrap) return;
|
|
474
|
+
|
|
475
|
+
e.preventDefault();
|
|
476
|
+
|
|
477
|
+
if (this.languageListOverlay.hasAttribute('hidden')) {
|
|
478
|
+
this.openOverlay();
|
|
479
|
+
this.updateOverlay();
|
|
480
|
+
this.focusTrap.activate();
|
|
481
|
+
} else {
|
|
482
|
+
this.focusTrap.deactivate();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Trigger events on resize
|
|
488
|
+
* Uses a debounce, for performance
|
|
489
|
+
*/
|
|
490
|
+
handleResize() {
|
|
491
|
+
if (
|
|
492
|
+
!this.languageListOverlay ||
|
|
493
|
+
this.languageListOverlay.hasAttribute('hidden')
|
|
494
|
+
)
|
|
495
|
+
return;
|
|
496
|
+
if (
|
|
497
|
+
(this.loginBox &&
|
|
498
|
+
this.loginBox.classList.contains(
|
|
499
|
+
'ecl-site-header__login-box--active',
|
|
500
|
+
)) ||
|
|
501
|
+
(this.searchForm &&
|
|
502
|
+
this.searchForm.classList.contains('ecl-site-header__search--active'))
|
|
503
|
+
) {
|
|
504
|
+
clearTimeout(this.resizeTimer);
|
|
505
|
+
this.resizeTimer = setTimeout(() => {
|
|
506
|
+
this.updateOverlay();
|
|
507
|
+
}, 200);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Handles keyboard events specific to the language list.
|
|
513
|
+
*
|
|
514
|
+
* @param {Event} e
|
|
515
|
+
*/
|
|
516
|
+
handleKeyboardLanguage(e) {
|
|
517
|
+
// Open the menu with space and enter
|
|
518
|
+
if (e.keyCode === 32 || e.key === 'Enter') {
|
|
519
|
+
this.toggleOverlay(e);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Toggles the search form.
|
|
525
|
+
*
|
|
526
|
+
* @param {Event} e
|
|
527
|
+
*/
|
|
528
|
+
toggleSearch(e) {
|
|
529
|
+
if (!this.searchForm) return;
|
|
530
|
+
|
|
531
|
+
e.preventDefault();
|
|
532
|
+
|
|
533
|
+
// Get current status
|
|
534
|
+
const isExpanded =
|
|
535
|
+
this.searchToggle.getAttribute('aria-expanded') === 'true';
|
|
536
|
+
|
|
537
|
+
// Close other boxes
|
|
538
|
+
if (
|
|
539
|
+
this.loginToggle &&
|
|
540
|
+
this.loginToggle.getAttribute('aria-expanded') === 'true'
|
|
541
|
+
) {
|
|
542
|
+
this.toggleLogin(e);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Toggle the search form
|
|
546
|
+
this.searchToggle.setAttribute(
|
|
547
|
+
'aria-expanded',
|
|
548
|
+
isExpanded ? 'false' : 'true',
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
if (!isExpanded) {
|
|
552
|
+
this.searchForm.classList.add('ecl-site-header__search--active');
|
|
553
|
+
this.setSearchArrow();
|
|
554
|
+
} else {
|
|
555
|
+
this.searchForm.classList.remove('ecl-site-header__search--active');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
setLoginArrow() {
|
|
560
|
+
const loginRect = this.loginBox.getBoundingClientRect();
|
|
561
|
+
if (loginRect.x === 0) {
|
|
562
|
+
const loginToggleRect = this.loginToggle.getBoundingClientRect();
|
|
563
|
+
const arrowPosition =
|
|
564
|
+
window.innerWidth - loginToggleRect.right + loginToggleRect.width / 2;
|
|
565
|
+
|
|
566
|
+
this.loginBox.style.setProperty(
|
|
567
|
+
'--ecl-login-arrow-position',
|
|
568
|
+
`calc(${arrowPosition}px - ${this.arrowSize})`,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
setSearchArrow() {
|
|
574
|
+
const searchRect = this.searchForm.getBoundingClientRect();
|
|
575
|
+
if (searchRect.x === 0) {
|
|
576
|
+
const searchToggleRect = this.searchToggle.getBoundingClientRect();
|
|
577
|
+
const arrowPosition =
|
|
578
|
+
window.innerWidth - searchToggleRect.right + searchToggleRect.width / 2;
|
|
579
|
+
|
|
580
|
+
this.searchForm.style.setProperty(
|
|
581
|
+
'--ecl-search-arrow-position',
|
|
582
|
+
`calc(${arrowPosition}px - ${this.arrowSize})`,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Toggles the login form.
|
|
589
|
+
*
|
|
590
|
+
* @param {Event} e
|
|
591
|
+
*/
|
|
592
|
+
toggleLogin(e) {
|
|
593
|
+
if (!this.loginBox) return;
|
|
594
|
+
|
|
595
|
+
e.preventDefault();
|
|
596
|
+
|
|
597
|
+
// Get current status
|
|
598
|
+
const isExpanded =
|
|
599
|
+
this.loginToggle.getAttribute('aria-expanded') === 'true';
|
|
600
|
+
|
|
601
|
+
// Close other boxes
|
|
602
|
+
if (
|
|
603
|
+
this.searchToggle &&
|
|
604
|
+
this.searchToggle.getAttribute('aria-expanded') === 'true'
|
|
605
|
+
) {
|
|
606
|
+
this.toggleSearch(e);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Toggle the login box
|
|
610
|
+
this.loginToggle.setAttribute(
|
|
611
|
+
'aria-expanded',
|
|
612
|
+
isExpanded ? 'false' : 'true',
|
|
613
|
+
);
|
|
614
|
+
if (!isExpanded) {
|
|
615
|
+
this.loginBox.classList.add('ecl-site-header__login-box--active');
|
|
616
|
+
this.setLoginArrow();
|
|
617
|
+
} else {
|
|
618
|
+
this.loginBox.classList.remove('ecl-site-header__login-box--active');
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Handles global keyboard events, triggered outside of the site header.
|
|
624
|
+
*
|
|
625
|
+
* @param {Event} e
|
|
626
|
+
*/
|
|
627
|
+
handleKeyboardGlobal(e) {
|
|
628
|
+
if (!this.languageLink) return;
|
|
629
|
+
const listExpanded = this.languageLink.getAttribute('aria-expanded');
|
|
630
|
+
|
|
631
|
+
// Detect press on Escape
|
|
632
|
+
if (e.key === 'Escape' || e.key === 'Esc') {
|
|
633
|
+
if (listExpanded === 'true') {
|
|
634
|
+
this.toggleOverlay(e);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Handles global click events, triggered outside of the site header.
|
|
641
|
+
*
|
|
642
|
+
* @param {Event} e
|
|
643
|
+
*/
|
|
644
|
+
handleClickGlobal(e) {
|
|
645
|
+
if (!this.languageLink && !this.searchToggle && !this.loginToggle) return;
|
|
646
|
+
const listExpanded =
|
|
647
|
+
this.languageLink && this.languageLink.getAttribute('aria-expanded');
|
|
648
|
+
const loginExpanded =
|
|
649
|
+
this.loginToggle &&
|
|
650
|
+
this.loginToggle.getAttribute('aria-expanded') === 'true';
|
|
651
|
+
const searchExpanded =
|
|
652
|
+
this.searchToggle &&
|
|
653
|
+
this.searchToggle.getAttribute('aria-expanded') === 'true';
|
|
654
|
+
// Check if the language list is open
|
|
655
|
+
if (listExpanded === 'true') {
|
|
656
|
+
// Check if the click occured in the language popover
|
|
657
|
+
if (
|
|
658
|
+
!this.languageListOverlay.contains(e.target) &&
|
|
659
|
+
!this.languageLink.contains(e.target)
|
|
660
|
+
) {
|
|
661
|
+
this.toggleOverlay(e);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (loginExpanded) {
|
|
665
|
+
if (
|
|
666
|
+
!this.loginBox.contains(e.target) &&
|
|
667
|
+
!this.loginToggle.contains(e.target)
|
|
668
|
+
) {
|
|
669
|
+
this.toggleLogin(e);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (searchExpanded) {
|
|
673
|
+
if (
|
|
674
|
+
!this.searchForm.contains(e.target) &&
|
|
675
|
+
!this.searchToggle.contains(e.target)
|
|
676
|
+
) {
|
|
677
|
+
this.toggleSearch(e);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export default SiteHeader;
|