@ecl/site-header 5.0.0-RC1
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 +144 -0
- package/_site-header-dropdown.scss +329 -0
- package/package.json +44 -0
- package/site-header-ec.scss +548 -0
- package/site-header-eu.scss +546 -0
- package/site-header-language-switcher.html.twig +178 -0
- package/site-header-print.scss +50 -0
- package/site-header.html.twig +528 -0
- package/site-header.js +837 -0
package/site-header.js
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
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
|
+
customActionToggleSelector = '[data-ecl-custom-action]',
|
|
57
|
+
customActionOverlaySelector = '[data-ecl-custom-action-overlay]',
|
|
58
|
+
customActionCloseSelector = '[data-ecl-custom-action-close]',
|
|
59
|
+
} = {},
|
|
60
|
+
) {
|
|
61
|
+
// Check element
|
|
62
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
63
|
+
throw new TypeError(
|
|
64
|
+
'DOM element should be given to initialize this widget.',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.element = element;
|
|
69
|
+
|
|
70
|
+
// Options
|
|
71
|
+
this.containerSelector = containerSelector;
|
|
72
|
+
this.languageLinkSelector = languageLinkSelector;
|
|
73
|
+
this.languageListOverlaySelector = languageListOverlaySelector;
|
|
74
|
+
this.languageListEuSelector = languageListEuSelector;
|
|
75
|
+
this.languageListNonEuSelector = languageListNonEuSelector;
|
|
76
|
+
this.languageListContentSelector = languageListContentSelector;
|
|
77
|
+
this.closeOverlaySelector = closeOverlaySelector;
|
|
78
|
+
this.searchToggleSelector = searchToggleSelector;
|
|
79
|
+
this.searchFormSelector = searchFormSelector;
|
|
80
|
+
this.loginToggleSelector = loginToggleSelector;
|
|
81
|
+
this.notificationSelector = notificationSelector;
|
|
82
|
+
this.loginBoxSelector = loginBoxSelector;
|
|
83
|
+
this.attachClickListener = attachClickListener;
|
|
84
|
+
this.attachKeyListener = attachKeyListener;
|
|
85
|
+
this.attachResizeListener = attachResizeListener;
|
|
86
|
+
this.tabletBreakpoint = tabletBreakpoint;
|
|
87
|
+
this.customActionToggleSelector = customActionToggleSelector;
|
|
88
|
+
this.customActionOverlaySelector = customActionOverlaySelector;
|
|
89
|
+
this.customActionCloseSelector = customActionCloseSelector;
|
|
90
|
+
|
|
91
|
+
// Private variables
|
|
92
|
+
this.languageMaxColumnItems = 8;
|
|
93
|
+
this.languageLink = null;
|
|
94
|
+
this.languageListOverlay = null;
|
|
95
|
+
this.languageListEu = null;
|
|
96
|
+
this.languageListNonEu = null;
|
|
97
|
+
this.languageListContent = null;
|
|
98
|
+
this.close = null;
|
|
99
|
+
this.focusTrap = null;
|
|
100
|
+
this.searchToggle = null;
|
|
101
|
+
this.searchForm = null;
|
|
102
|
+
this.loginToggle = null;
|
|
103
|
+
this.loginBox = null;
|
|
104
|
+
this.resizeTimer = null;
|
|
105
|
+
this.direction = null;
|
|
106
|
+
this.notificationContainer = null;
|
|
107
|
+
this.customActionToggle = null;
|
|
108
|
+
this.customActionOverlay = null;
|
|
109
|
+
this.customActionClose = null;
|
|
110
|
+
this.customActionFocusTrap = null;
|
|
111
|
+
|
|
112
|
+
// Bind `this` for use in callbacks
|
|
113
|
+
this.openOverlay = this.openOverlay.bind(this);
|
|
114
|
+
this.closeOverlay = this.closeOverlay.bind(this);
|
|
115
|
+
this.toggleOverlay = this.toggleOverlay.bind(this);
|
|
116
|
+
this.toggleSearch = this.toggleSearch.bind(this);
|
|
117
|
+
this.toggleLogin = this.toggleLogin.bind(this);
|
|
118
|
+
this.setLoginArrow = this.setLoginArrow.bind(this);
|
|
119
|
+
this.setSearchArrow = this.setSearchArrow.bind(this);
|
|
120
|
+
this.handleKeyboardLanguage = this.handleKeyboardLanguage.bind(this);
|
|
121
|
+
this.handleKeyboardGlobal = this.handleKeyboardGlobal.bind(this);
|
|
122
|
+
this.handleClickGlobal = this.handleClickGlobal.bind(this);
|
|
123
|
+
this.handleResize = this.handleResize.bind(this);
|
|
124
|
+
this.setLanguageListHeight = this.setLanguageListHeight.bind(this);
|
|
125
|
+
this.handleNotificationClose = this.handleNotificationClose.bind(this);
|
|
126
|
+
|
|
127
|
+
this.openCustomAction = this.openCustomAction.bind(this);
|
|
128
|
+
this.closeCustomAction = this.closeCustomAction.bind(this);
|
|
129
|
+
this.toggleCustomAction = this.toggleCustomAction.bind(this);
|
|
130
|
+
this.handleKeyboardCustomAction =
|
|
131
|
+
this.handleKeyboardCustomAction.bind(this);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Initialise component.
|
|
136
|
+
*/
|
|
137
|
+
init() {
|
|
138
|
+
if (!ECL) {
|
|
139
|
+
throw new TypeError('Called init but ECL is not present');
|
|
140
|
+
}
|
|
141
|
+
ECL.components = ECL.components || new Map();
|
|
142
|
+
this.arrowSize = '0.5rem';
|
|
143
|
+
// Bind global events
|
|
144
|
+
if (this.attachKeyListener) {
|
|
145
|
+
document.addEventListener('keyup', this.handleKeyboardGlobal);
|
|
146
|
+
}
|
|
147
|
+
if (this.attachClickListener) {
|
|
148
|
+
document.addEventListener('click', this.handleClickGlobal);
|
|
149
|
+
}
|
|
150
|
+
if (this.attachResizeListener) {
|
|
151
|
+
window.addEventListener('resize', this.handleResize);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Site header elements
|
|
155
|
+
this.container = queryOne(this.containerSelector);
|
|
156
|
+
|
|
157
|
+
// Language list management
|
|
158
|
+
this.languageLink = queryOne(this.languageLinkSelector);
|
|
159
|
+
this.languageListOverlay = queryOne(this.languageListOverlaySelector);
|
|
160
|
+
this.languageListEu = queryOne(this.languageListEuSelector);
|
|
161
|
+
this.languageListNonEu = queryOne(this.languageListNonEuSelector);
|
|
162
|
+
this.languageListContent = queryOne(this.languageListContentSelector);
|
|
163
|
+
this.close = queryOne(this.closeOverlaySelector);
|
|
164
|
+
this.notification = queryOne(this.notificationSelector);
|
|
165
|
+
|
|
166
|
+
// direction
|
|
167
|
+
this.direction = getComputedStyle(this.element).direction;
|
|
168
|
+
if (this.direction === 'rtl') {
|
|
169
|
+
this.element.classList.add('ecl-site-header--rtl');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Create focus trap
|
|
173
|
+
if (this.languageListOverlay) {
|
|
174
|
+
this.focusTrap = createFocusTrap(this.languageListOverlay, {
|
|
175
|
+
onDeactivate: this.closeOverlay,
|
|
176
|
+
allowOutsideClick: true,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (this.attachClickListener && this.languageLink) {
|
|
181
|
+
this.languageLink.addEventListener('click', this.toggleOverlay);
|
|
182
|
+
}
|
|
183
|
+
if (this.attachClickListener && this.close) {
|
|
184
|
+
this.close.addEventListener('click', this.toggleOverlay);
|
|
185
|
+
}
|
|
186
|
+
if (this.attachKeyListener && this.languageLink) {
|
|
187
|
+
this.languageLink.addEventListener(
|
|
188
|
+
'keydown',
|
|
189
|
+
this.handleKeyboardLanguage,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Search form management
|
|
194
|
+
this.searchToggle = queryOne(this.searchToggleSelector);
|
|
195
|
+
this.searchForm = queryOne(this.searchFormSelector);
|
|
196
|
+
|
|
197
|
+
if (this.attachClickListener && this.searchToggle) {
|
|
198
|
+
this.searchToggle.addEventListener('click', this.toggleSearch);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Login management
|
|
202
|
+
this.loginToggle = queryOne(this.loginToggleSelector);
|
|
203
|
+
this.loginBox = queryOne(this.loginBoxSelector);
|
|
204
|
+
|
|
205
|
+
if (this.attachClickListener && this.loginToggle) {
|
|
206
|
+
this.loginToggle.addEventListener('click', this.toggleLogin);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Custom action management
|
|
210
|
+
this.customActionToggle = queryOne(this.customActionToggleSelector);
|
|
211
|
+
this.customActionOverlay = queryOne(this.customActionOverlaySelector);
|
|
212
|
+
this.customActionClose = queryOne(this.customActionCloseSelector);
|
|
213
|
+
|
|
214
|
+
if (this.customActionOverlay) {
|
|
215
|
+
// Create a separate focus trap for the custom action overlay
|
|
216
|
+
this.customActionFocusTrap = createFocusTrap(this.customActionOverlay, {
|
|
217
|
+
onDeactivate: this.closeCustomAction,
|
|
218
|
+
allowOutsideClick: true,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.attachClickListener && this.customActionToggle) {
|
|
223
|
+
this.customActionToggle.addEventListener(
|
|
224
|
+
'click',
|
|
225
|
+
this.toggleCustomAction,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (this.attachClickListener && this.customActionClose) {
|
|
229
|
+
this.customActionClose.addEventListener('click', this.toggleCustomAction);
|
|
230
|
+
}
|
|
231
|
+
if (this.attachKeyListener && this.customActionToggle) {
|
|
232
|
+
this.customActionToggle.addEventListener(
|
|
233
|
+
'keydown',
|
|
234
|
+
this.handleKeyboardCustomAction,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Set ecl initialized attribute
|
|
239
|
+
this.element.setAttribute('data-ecl-auto-initialized', 'true');
|
|
240
|
+
ECL.components.set(this.element, this);
|
|
241
|
+
|
|
242
|
+
if (this.notification) {
|
|
243
|
+
this.notificationContainer = this.notification.closest(
|
|
244
|
+
'.ecl-site-header__notification',
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
const eclNotification = ECL.components.get(this.notification);
|
|
249
|
+
|
|
250
|
+
if (eclNotification) {
|
|
251
|
+
eclNotification.on('onClose', this.handleNotificationClose);
|
|
252
|
+
}
|
|
253
|
+
}, 0);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Destroy component.
|
|
259
|
+
*/
|
|
260
|
+
destroy() {
|
|
261
|
+
if (this.attachClickListener && this.languageLink) {
|
|
262
|
+
this.languageLink.removeEventListener('click', this.toggleOverlay);
|
|
263
|
+
}
|
|
264
|
+
if (this.focusTrap) {
|
|
265
|
+
this.focusTrap.deactivate();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (this.attachKeyListener && this.languageLink) {
|
|
269
|
+
this.languageLink.removeEventListener(
|
|
270
|
+
'keydown',
|
|
271
|
+
this.handleKeyboardLanguage,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (this.attachClickListener && this.close) {
|
|
276
|
+
this.close.removeEventListener('click', this.toggleOverlay);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (this.attachClickListener && this.searchToggle) {
|
|
280
|
+
this.searchToggle.removeEventListener('click', this.toggleSearch);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (this.attachClickListener && this.loginToggle) {
|
|
284
|
+
this.loginToggle.removeEventListener('click', this.toggleLogin);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (this.attachClickListener && this.customActionToggle) {
|
|
288
|
+
this.customActionToggle.removeEventListener(
|
|
289
|
+
'click',
|
|
290
|
+
this.toggleCustomAction,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (this.customActionFocusTrap) {
|
|
294
|
+
this.customActionFocusTrap.deactivate();
|
|
295
|
+
}
|
|
296
|
+
if (this.attachKeyListener && this.customActionToggle) {
|
|
297
|
+
this.customActionToggle.removeEventListener(
|
|
298
|
+
'keydown',
|
|
299
|
+
this.handleKeyboardCustomAction,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (this.attachClickListener && this.customActionClose) {
|
|
303
|
+
this.customActionClose.removeEventListener(
|
|
304
|
+
'click',
|
|
305
|
+
this.toggleCustomAction,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (this.attachKeyListener) {
|
|
310
|
+
document.removeEventListener('keyup', this.handleKeyboardGlobal);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (this.attachClickListener) {
|
|
314
|
+
document.removeEventListener('click', this.handleClickGlobal);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (this.attachResizeListener) {
|
|
318
|
+
window.removeEventListener('resize', this.handleResize);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (this.element) {
|
|
322
|
+
this.element.removeAttribute('data-ecl-auto-initialized');
|
|
323
|
+
this.element.classList.remove('ecl-site-header--rtl');
|
|
324
|
+
ECL.components.delete(this.element);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Method repositions a popover overlay in the viewport.
|
|
330
|
+
* It also (optionally) handles language-list–specific logic such as columns.
|
|
331
|
+
*
|
|
332
|
+
* @param {HTMLElement} overlay The overlay element to be positioned
|
|
333
|
+
* @param {HTMLElement} toggle The toggle button/link that opens the overlay
|
|
334
|
+
* @param {String} arrowCssVar The CSS variable name for arrow positioning
|
|
335
|
+
*/
|
|
336
|
+
updateOverlayPosition(
|
|
337
|
+
overlay,
|
|
338
|
+
toggle,
|
|
339
|
+
arrowCssVar = '--ecl-overlay-arrow-position',
|
|
340
|
+
) {
|
|
341
|
+
if (!overlay || !toggle || !this.container) return;
|
|
342
|
+
|
|
343
|
+
// If this overlay is the language overlay, handle columns (EU vs Non-EU items)
|
|
344
|
+
if (overlay === this.languageListOverlay) {
|
|
345
|
+
// Calculate columns for the EU language list
|
|
346
|
+
if (this.languageListEu) {
|
|
347
|
+
const itemsEu = queryAll(
|
|
348
|
+
'.ecl-site-header__language-item',
|
|
349
|
+
this.languageListEu,
|
|
350
|
+
);
|
|
351
|
+
const columnsEu = Math.ceil(
|
|
352
|
+
itemsEu.length / this.languageMaxColumnItems,
|
|
353
|
+
);
|
|
354
|
+
if (columnsEu > 1) {
|
|
355
|
+
this.languageListEu.classList.add(
|
|
356
|
+
`ecl-site-header__language-category--${columnsEu}-col`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Calculate columns for the Non-EU language list
|
|
362
|
+
if (this.languageListNonEu) {
|
|
363
|
+
const itemsNonEu = queryAll(
|
|
364
|
+
'.ecl-site-header__language-item',
|
|
365
|
+
this.languageListNonEu,
|
|
366
|
+
);
|
|
367
|
+
const columnsNonEu = Math.ceil(
|
|
368
|
+
itemsNonEu.length / this.languageMaxColumnItems,
|
|
369
|
+
);
|
|
370
|
+
if (columnsNonEu > 1) {
|
|
371
|
+
this.languageListNonEu.classList.add(
|
|
372
|
+
`ecl-site-header__language-category--${columnsNonEu}-col`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Remove stacked classes first
|
|
378
|
+
if (this.languageListEu) {
|
|
379
|
+
this.languageListEu.parentNode.classList.remove(
|
|
380
|
+
'ecl-site-header__language-content--stack',
|
|
381
|
+
);
|
|
382
|
+
} else if (this.languageListNonEu) {
|
|
383
|
+
this.languageListNonEu.parentNode.classList.remove(
|
|
384
|
+
'ecl-site-header__language-content--stack',
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Clear leftover classes/inline styles
|
|
390
|
+
overlay.classList.remove(
|
|
391
|
+
'ecl-site-header__language-container--push-right',
|
|
392
|
+
'ecl-site-header__language-container--push-left',
|
|
393
|
+
'ecl-site-header__language-container--full',
|
|
394
|
+
);
|
|
395
|
+
overlay.style.removeProperty(arrowCssVar);
|
|
396
|
+
overlay.style.removeProperty('right');
|
|
397
|
+
overlay.style.removeProperty('left');
|
|
398
|
+
|
|
399
|
+
// Calculate bounding rects
|
|
400
|
+
let popoverRect = overlay.getBoundingClientRect();
|
|
401
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
402
|
+
const screenWidth = window.innerWidth;
|
|
403
|
+
const toggleRect = toggle.getBoundingClientRect();
|
|
404
|
+
|
|
405
|
+
// If this is the language overlay, handle “too wide” scenario
|
|
406
|
+
if (overlay === this.languageListOverlay) {
|
|
407
|
+
if (popoverRect.width > containerRect.width) {
|
|
408
|
+
// Stack elements
|
|
409
|
+
if (this.languageListEu) {
|
|
410
|
+
this.languageListEu.parentNode.classList.add(
|
|
411
|
+
'ecl-site-header__language-content--stack',
|
|
412
|
+
);
|
|
413
|
+
} else if (this.languageListNonEu) {
|
|
414
|
+
this.languageListNonEu.parentNode.classList.add(
|
|
415
|
+
'ecl-site-header__language-content--stack',
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Mobile: full width if below tablet breakpoint
|
|
422
|
+
if (window.innerWidth < this.tabletBreakpoint) {
|
|
423
|
+
overlay.classList.add('ecl-site-header__language-container--full');
|
|
424
|
+
// Recompute popoverRect after applying the class
|
|
425
|
+
popoverRect = overlay.getBoundingClientRect();
|
|
426
|
+
// Position arrow
|
|
427
|
+
const arrowPosition =
|
|
428
|
+
popoverRect.right - toggleRect.right + toggleRect.width / 2;
|
|
429
|
+
overlay.style.setProperty(
|
|
430
|
+
arrowCssVar,
|
|
431
|
+
`calc(${arrowPosition}px - ${this.arrowSize})`,
|
|
432
|
+
);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// If popover extends beyond right edge in LTR
|
|
437
|
+
if (this.direction === 'ltr' && popoverRect.right > screenWidth) {
|
|
438
|
+
overlay.classList.add('ecl-site-header__language-container--push-right');
|
|
439
|
+
overlay.style.setProperty(
|
|
440
|
+
'right',
|
|
441
|
+
`calc(-${containerRect.right}px + ${toggleRect.right}px)`,
|
|
442
|
+
);
|
|
443
|
+
const arrowPos =
|
|
444
|
+
containerRect.right - toggleRect.right + toggleRect.width / 2;
|
|
445
|
+
overlay.style.setProperty(
|
|
446
|
+
arrowCssVar,
|
|
447
|
+
`calc(${arrowPos}px - ${this.arrowSize})`,
|
|
448
|
+
);
|
|
449
|
+
} else if (this.direction === 'rtl' && popoverRect.left < 0) {
|
|
450
|
+
// If popover extends beyond left edge in RTL
|
|
451
|
+
overlay.classList.add('ecl-site-header__language-container--push-left');
|
|
452
|
+
overlay.style.setProperty(
|
|
453
|
+
'left',
|
|
454
|
+
`calc(-${toggleRect.left}px + ${containerRect.left}px)`,
|
|
455
|
+
);
|
|
456
|
+
const arrowPos =
|
|
457
|
+
toggleRect.right - containerRect.left - toggleRect.width / 2;
|
|
458
|
+
overlay.style.setProperty(arrowCssVar, `${arrowPos}px`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Wrapper: Update display of the modal language list overlay
|
|
464
|
+
* (Calls the new `updateOverlayPosition` for the language popover).
|
|
465
|
+
*/
|
|
466
|
+
updateOverlay() {
|
|
467
|
+
if (!this.languageListOverlay || !this.languageLink) return;
|
|
468
|
+
this.updateOverlayPosition(this.languageListOverlay, this.languageLink);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Removes the containers of the notification element
|
|
473
|
+
*/
|
|
474
|
+
handleNotificationClose() {
|
|
475
|
+
if (this.notificationContainer) {
|
|
476
|
+
this.notificationContainer.remove();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Set a max height for the language list content
|
|
482
|
+
*/
|
|
483
|
+
setLanguageListHeight() {
|
|
484
|
+
const viewportHeight = window.innerHeight;
|
|
485
|
+
|
|
486
|
+
if (this.languageListContent) {
|
|
487
|
+
const listTop = this.languageListContent.getBoundingClientRect().top;
|
|
488
|
+
|
|
489
|
+
const availableSpace = viewportHeight - listTop;
|
|
490
|
+
if (availableSpace > 0) {
|
|
491
|
+
this.languageListContent.style.maxHeight = `${availableSpace}px`;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Shows the modal language list overlay.
|
|
498
|
+
*/
|
|
499
|
+
openOverlay() {
|
|
500
|
+
// Display language list
|
|
501
|
+
this.languageListOverlay.hidden = false;
|
|
502
|
+
this.languageListOverlay.setAttribute('aria-modal', 'true');
|
|
503
|
+
this.languageLink.setAttribute('aria-expanded', 'true');
|
|
504
|
+
this.setLanguageListHeight();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Hides the modal language list overlay.
|
|
509
|
+
*/
|
|
510
|
+
closeOverlay() {
|
|
511
|
+
this.languageListOverlay.hidden = true;
|
|
512
|
+
this.languageListOverlay.removeAttribute('aria-modal');
|
|
513
|
+
this.languageLink.setAttribute('aria-expanded', 'false');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Toggles the modal language list overlay.
|
|
518
|
+
*
|
|
519
|
+
* @param {Event} e
|
|
520
|
+
*/
|
|
521
|
+
toggleOverlay(e) {
|
|
522
|
+
if (!this.languageListOverlay || !this.focusTrap) return;
|
|
523
|
+
|
|
524
|
+
e.preventDefault();
|
|
525
|
+
|
|
526
|
+
if (this.languageListOverlay.hasAttribute('hidden')) {
|
|
527
|
+
this.openOverlay();
|
|
528
|
+
this.updateOverlayPosition(this.languageListOverlay, this.languageLink);
|
|
529
|
+
this.focusTrap.activate();
|
|
530
|
+
} else {
|
|
531
|
+
this.focusTrap.deactivate();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Toggles the search form.
|
|
537
|
+
*
|
|
538
|
+
* @param {Event} e
|
|
539
|
+
*/
|
|
540
|
+
toggleSearch(e) {
|
|
541
|
+
if (!this.searchForm) return;
|
|
542
|
+
|
|
543
|
+
e.preventDefault();
|
|
544
|
+
|
|
545
|
+
// Get current status
|
|
546
|
+
const isExpanded =
|
|
547
|
+
this.searchToggle.getAttribute('aria-expanded') === 'true';
|
|
548
|
+
|
|
549
|
+
// Close other boxes
|
|
550
|
+
if (
|
|
551
|
+
this.loginToggle &&
|
|
552
|
+
this.loginToggle.getAttribute('aria-expanded') === 'true'
|
|
553
|
+
) {
|
|
554
|
+
this.toggleLogin(e);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Toggle the search form
|
|
558
|
+
this.searchToggle.setAttribute(
|
|
559
|
+
'aria-expanded',
|
|
560
|
+
isExpanded ? 'false' : 'true',
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
if (!isExpanded) {
|
|
564
|
+
this.searchForm.classList.add('ecl-site-header__search--active');
|
|
565
|
+
this.setSearchArrow();
|
|
566
|
+
} else {
|
|
567
|
+
this.searchForm.classList.remove('ecl-site-header__search--active');
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
setLoginArrow() {
|
|
572
|
+
const loginRect = this.loginBox.getBoundingClientRect();
|
|
573
|
+
if (loginRect.x === 0) {
|
|
574
|
+
const loginToggleRect = this.loginToggle.getBoundingClientRect();
|
|
575
|
+
const arrowPosition =
|
|
576
|
+
document.documentElement.clientWidth -
|
|
577
|
+
loginToggleRect.right +
|
|
578
|
+
loginToggleRect.width / 2;
|
|
579
|
+
|
|
580
|
+
this.loginBox.style.setProperty(
|
|
581
|
+
'--ecl-login-arrow-position',
|
|
582
|
+
`calc(${arrowPosition}px - ${this.arrowSize})`,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
setSearchArrow() {
|
|
588
|
+
const searchRect = this.searchForm.getBoundingClientRect();
|
|
589
|
+
if (searchRect.x === 0) {
|
|
590
|
+
const searchToggleRect = this.searchToggle.getBoundingClientRect();
|
|
591
|
+
const arrowPosition =
|
|
592
|
+
document.documentElement.clientWidth -
|
|
593
|
+
searchToggleRect.right +
|
|
594
|
+
searchToggleRect.width / 2;
|
|
595
|
+
|
|
596
|
+
this.searchForm.style.setProperty(
|
|
597
|
+
'--ecl-search-arrow-position',
|
|
598
|
+
`calc(${arrowPosition}px - ${this.arrowSize})`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Toggles the login form.
|
|
605
|
+
*
|
|
606
|
+
* @param {Event} e
|
|
607
|
+
*/
|
|
608
|
+
toggleLogin(e) {
|
|
609
|
+
if (!this.loginBox) return;
|
|
610
|
+
|
|
611
|
+
e.preventDefault();
|
|
612
|
+
|
|
613
|
+
// Get current status
|
|
614
|
+
const isExpanded =
|
|
615
|
+
this.loginToggle.getAttribute('aria-expanded') === 'true';
|
|
616
|
+
|
|
617
|
+
// Close other boxes
|
|
618
|
+
if (
|
|
619
|
+
this.searchToggle &&
|
|
620
|
+
this.searchToggle.getAttribute('aria-expanded') === 'true'
|
|
621
|
+
) {
|
|
622
|
+
this.toggleSearch(e);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Toggle the login box
|
|
626
|
+
this.loginToggle.setAttribute(
|
|
627
|
+
'aria-expanded',
|
|
628
|
+
isExpanded ? 'false' : 'true',
|
|
629
|
+
);
|
|
630
|
+
if (!isExpanded) {
|
|
631
|
+
this.loginBox.classList.add('ecl-site-header__login-box--active');
|
|
632
|
+
this.setLoginArrow();
|
|
633
|
+
} else {
|
|
634
|
+
this.loginBox.classList.remove('ecl-site-header__login-box--active');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Handles keyboard events specific to the language list.
|
|
640
|
+
*
|
|
641
|
+
* @param {Event} e
|
|
642
|
+
*/
|
|
643
|
+
handleKeyboardLanguage(e) {
|
|
644
|
+
// Open the menu with space and enter
|
|
645
|
+
if (e.keyCode === 32 || e.key === 'Enter') {
|
|
646
|
+
this.toggleOverlay(e);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Handles global keyboard events, triggered outside of the site header.
|
|
652
|
+
*
|
|
653
|
+
* @param {Event} e
|
|
654
|
+
*/
|
|
655
|
+
handleKeyboardGlobal(e) {
|
|
656
|
+
if (!this.languageLink) return;
|
|
657
|
+
const listExpanded = this.languageLink.getAttribute('aria-expanded');
|
|
658
|
+
|
|
659
|
+
// Detect press on Escape
|
|
660
|
+
if (e.key === 'Escape' || e.key === 'Esc') {
|
|
661
|
+
if (listExpanded === 'true') {
|
|
662
|
+
this.toggleOverlay(e);
|
|
663
|
+
}
|
|
664
|
+
if (
|
|
665
|
+
this.customActionToggle &&
|
|
666
|
+
this.customActionToggle.getAttribute('aria-expanded') === 'true'
|
|
667
|
+
) {
|
|
668
|
+
this.toggleCustomAction(e);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Handles global click events, triggered outside of the site header.
|
|
675
|
+
*
|
|
676
|
+
* @param {Event} e
|
|
677
|
+
*/
|
|
678
|
+
handleClickGlobal(e) {
|
|
679
|
+
if (!this.languageLink && !this.searchToggle && !this.loginToggle) return;
|
|
680
|
+
const listExpanded =
|
|
681
|
+
this.languageLink && this.languageLink.getAttribute('aria-expanded');
|
|
682
|
+
const loginExpanded =
|
|
683
|
+
this.loginToggle &&
|
|
684
|
+
this.loginToggle.getAttribute('aria-expanded') === 'true';
|
|
685
|
+
const searchExpanded =
|
|
686
|
+
this.searchToggle &&
|
|
687
|
+
this.searchToggle.getAttribute('aria-expanded') === 'true';
|
|
688
|
+
// Check if the language list is open
|
|
689
|
+
if (listExpanded === 'true') {
|
|
690
|
+
// Check if the click occured in the language popover
|
|
691
|
+
if (
|
|
692
|
+
!this.languageListOverlay.contains(e.target) &&
|
|
693
|
+
!this.languageLink.contains(e.target)
|
|
694
|
+
) {
|
|
695
|
+
this.toggleOverlay(e);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (loginExpanded) {
|
|
699
|
+
if (
|
|
700
|
+
!this.loginBox.contains(e.target) &&
|
|
701
|
+
!this.loginToggle.contains(e.target)
|
|
702
|
+
) {
|
|
703
|
+
this.toggleLogin(e);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (searchExpanded) {
|
|
707
|
+
if (
|
|
708
|
+
!this.searchForm.contains(e.target) &&
|
|
709
|
+
!this.searchToggle.contains(e.target)
|
|
710
|
+
) {
|
|
711
|
+
this.toggleSearch(e);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Custom action
|
|
716
|
+
const customActionExpanded =
|
|
717
|
+
this.customActionToggle &&
|
|
718
|
+
this.customActionToggle.getAttribute('aria-expanded') === 'true';
|
|
719
|
+
if (customActionExpanded) {
|
|
720
|
+
if (
|
|
721
|
+
!this.customActionOverlay.contains(e.target) &&
|
|
722
|
+
!this.customActionToggle.contains(e.target)
|
|
723
|
+
) {
|
|
724
|
+
this.toggleCustomAction(e);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Trigger events on resize
|
|
731
|
+
* Uses a debounce, for performance
|
|
732
|
+
*/
|
|
733
|
+
handleResize() {
|
|
734
|
+
if (this.resizeTimer) {
|
|
735
|
+
clearTimeout(this.resizeTimer);
|
|
736
|
+
}
|
|
737
|
+
this.resizeTimer = setTimeout(() => {
|
|
738
|
+
// If language overlay is open, reposition
|
|
739
|
+
if (
|
|
740
|
+
this.languageListOverlay &&
|
|
741
|
+
!this.languageListOverlay.hasAttribute('hidden')
|
|
742
|
+
) {
|
|
743
|
+
this.updateOverlayPosition(this.languageListOverlay, this.languageLink);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// If custom action overlay is open, reposition
|
|
747
|
+
if (
|
|
748
|
+
this.customActionOverlay &&
|
|
749
|
+
!this.customActionOverlay.hasAttribute('hidden')
|
|
750
|
+
) {
|
|
751
|
+
this.updateOverlayPosition(
|
|
752
|
+
this.customActionOverlay,
|
|
753
|
+
this.customActionToggle,
|
|
754
|
+
'--ecl-overlay-arrow-position',
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// If the login box is open, re-position arrow
|
|
759
|
+
if (
|
|
760
|
+
this.loginBox &&
|
|
761
|
+
this.loginBox.classList.contains('ecl-site-header__login-box--active')
|
|
762
|
+
) {
|
|
763
|
+
this.setLoginArrow();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// If the search box is open, re-position arrow
|
|
767
|
+
if (
|
|
768
|
+
this.searchForm &&
|
|
769
|
+
this.searchForm.classList.contains('ecl-site-header__search--active')
|
|
770
|
+
) {
|
|
771
|
+
this.setSearchArrow();
|
|
772
|
+
}
|
|
773
|
+
}, 200);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Shows the custom action overlay.
|
|
778
|
+
*/
|
|
779
|
+
openCustomAction() {
|
|
780
|
+
if (!this.customActionOverlay) return;
|
|
781
|
+
this.customActionOverlay.hidden = false;
|
|
782
|
+
this.customActionOverlay.setAttribute('aria-modal', 'true');
|
|
783
|
+
this.customActionToggle.setAttribute('aria-expanded', 'true');
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Hides the custom action overlay.
|
|
788
|
+
*/
|
|
789
|
+
closeCustomAction() {
|
|
790
|
+
if (!this.customActionOverlay) return;
|
|
791
|
+
this.customActionOverlay.hidden = true;
|
|
792
|
+
this.customActionOverlay.removeAttribute('aria-modal');
|
|
793
|
+
this.customActionToggle.setAttribute('aria-expanded', 'false');
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Toggles the custom action overlay.
|
|
798
|
+
*
|
|
799
|
+
* @param {Event} e
|
|
800
|
+
*/
|
|
801
|
+
toggleCustomAction(e) {
|
|
802
|
+
if (!this.customActionOverlay || !this.customActionFocusTrap) return;
|
|
803
|
+
|
|
804
|
+
e.preventDefault();
|
|
805
|
+
|
|
806
|
+
// Check current state
|
|
807
|
+
const isHidden = this.customActionOverlay.hasAttribute('hidden');
|
|
808
|
+
|
|
809
|
+
if (isHidden) {
|
|
810
|
+
this.openCustomAction();
|
|
811
|
+
// Reuse the same overlay positioning logic,
|
|
812
|
+
// but use a different CSS arrow var for custom action if you like.
|
|
813
|
+
this.updateOverlayPosition(
|
|
814
|
+
this.customActionOverlay,
|
|
815
|
+
this.customActionToggle,
|
|
816
|
+
'--ecl-overlay-arrow-position',
|
|
817
|
+
);
|
|
818
|
+
this.customActionFocusTrap.activate();
|
|
819
|
+
} else {
|
|
820
|
+
this.customActionFocusTrap.deactivate();
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Handles keyboard events specific to the custom action toggle.
|
|
826
|
+
*
|
|
827
|
+
* @param {Event} e
|
|
828
|
+
*/
|
|
829
|
+
handleKeyboardCustomAction(e) {
|
|
830
|
+
// Open the custom action with space and enter
|
|
831
|
+
if (e.keyCode === 32 || e.key === 'Enter') {
|
|
832
|
+
this.toggleCustomAction(e);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export default SiteHeader;
|