Skip to content

Commit 624a09c

Browse files
authored
Merge pull request #203 from ESA-APEx/automated_header_refresh
feat: dynamically load in the navigation
2 parents 8af6110 + fcae0e0 commit 624a09c

2 files changed

Lines changed: 151 additions & 1 deletion

File tree

‎html/apex-navigation.html‎

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="container"><div class="c-header__bottom">
1+
<div class="container" id="apex-header-navigation"><div class="c-header__bottom">
22
<a href="https://apex.esa.int/" class="c-header__logo">
33
APEx
44
<span>Application Propagation Environments</span>
@@ -254,3 +254,139 @@ <h6>
254254
</div>
255255

256256
</div></div>
257+
258+
<script type="application/javascript">
259+
(function () {
260+
'use strict';
261+
262+
const navigationRoot = document.getElementById('apex-header-navigation');
263+
if (!navigationRoot) {
264+
return;
265+
}
266+
267+
const NAV_SOURCES = [
268+
{
269+
label: 'apex-primary',
270+
url: 'https://apex.esa.int/'
271+
},
272+
{ // This is a CORS proxy to fetch the navigation from apex.esa.int for local testing.
273+
label: 'apex-corsproxy',
274+
url: 'https://corsproxy.io/?https://apex.esa.int/'
275+
}
276+
];
277+
278+
function absolutizeApexUrl(value) {
279+
if (!value || value.startsWith('#') || value.startsWith('javascript:')) {
280+
return value;
281+
}
282+
283+
if (value.startsWith('//')) {
284+
return 'https:' + value;
285+
}
286+
287+
if (value.startsWith('/')) {
288+
return 'https://apex.esa.int' + value;
289+
}
290+
291+
return value;
292+
}
293+
294+
function normalizeApexLinks(rootElement) {
295+
const attributeSelectors = [
296+
['a[href]', 'href'],
297+
['img[src]', 'src'],
298+
['source[src]', 'src'],
299+
['source[srcset]', 'srcset'],
300+
['img[srcset]', 'srcset'],
301+
['link[href]', 'href']
302+
];
303+
304+
attributeSelectors.forEach(([selector, attribute]) => {
305+
rootElement.querySelectorAll(selector).forEach((element) => {
306+
const value = element.getAttribute(attribute);
307+
if (!value) {
308+
return;
309+
}
310+
311+
if (attribute === 'srcset') {
312+
const normalizedSrcset = value
313+
.split(',')
314+
.map((candidate) => {
315+
const trimmedCandidate = candidate.trim();
316+
if (!trimmedCandidate) {
317+
return trimmedCandidate;
318+
}
319+
320+
const parts = trimmedCandidate.split(/\s+/);
321+
parts[0] = absolutizeApexUrl(parts[0]);
322+
return parts.join(' ');
323+
})
324+
.join(', ');
325+
element.setAttribute(attribute, normalizedSrcset);
326+
return;
327+
}
328+
329+
element.setAttribute(attribute, absolutizeApexUrl(value));
330+
});
331+
});
332+
}
333+
334+
function extractNavigationMarkup(pageHtml) {
335+
const parser = new DOMParser();
336+
const pageDoc = parser.parseFromString(pageHtml, 'text/html');
337+
const navigation = pageDoc.querySelector('.c-header__bottom');
338+
return navigation ? navigation.outerHTML : null;
339+
}
340+
341+
async function fetchSourceMarkup(source) {
342+
const response = await fetch(source.url, {
343+
method: 'GET',
344+
credentials: 'omit',
345+
mode: 'cors'
346+
});
347+
348+
if (!response.ok) {
349+
throw new Error('Navigation request failed with status ' + response.status);
350+
}
351+
352+
const pageHtml = await response.text();
353+
return extractNavigationMarkup(pageHtml);
354+
}
355+
356+
async function loadRemoteNavigation() {
357+
for (const source of NAV_SOURCES) {
358+
try {
359+
const markup = await fetchSourceMarkup(source);
360+
if (!markup) {
361+
continue;
362+
}
363+
364+
navigationRoot.innerHTML = markup;
365+
normalizeApexLinks(navigationRoot);
366+
navigationRoot.setAttribute('data-navigation-source', source.label);
367+
document.dispatchEvent(new CustomEvent('apex:navigation:loaded', {
368+
detail: {
369+
source: source.label
370+
}
371+
}));
372+
return;
373+
} catch (error) {
374+
// Keep fallback navigation if remote source is unavailable.
375+
}
376+
}
377+
378+
navigationRoot.setAttribute('data-navigation-source', 'fallback');
379+
document.dispatchEvent(new CustomEvent('apex:navigation:loaded', {
380+
detail: {
381+
source: 'fallback'
382+
}
383+
}));
384+
}
385+
386+
if (document.readyState === 'loading') {
387+
document.addEventListener('DOMContentLoaded', loadRemoteNavigation, { once: true });
388+
} else {
389+
loadRemoteNavigation();
390+
}
391+
})();
392+
</script>

‎html/apex-scripts.html‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
const megaMenu = document.querySelector('.js-mega-menu');
99
if (!megaMenu) return;
1010

11+
if (megaMenu.dataset.initialized === 'true') {
12+
return;
13+
}
14+
megaMenu.dataset.initialized = 'true';
15+
1116
const megaMenuTopics = document.querySelectorAll('.js-mega-menu-topic');
1217
const megaMenuContents = document.querySelectorAll('.js-mega-menu-content');
1318

@@ -50,6 +55,10 @@
5055

5156
if (menu) {
5257
hamburgers.forEach(hamburger => {
58+
if (hamburger.dataset.initialized === 'true') {
59+
return;
60+
}
61+
hamburger.dataset.initialized = 'true';
5362
hamburger.addEventListener('click', (e) => this.handleClickHamburger(e, menu, body));
5463
});
5564
} else {
@@ -66,6 +75,11 @@
6675

6776
new MegaMenu();
6877
new HamburgerMenu();
78+
79+
document.addEventListener('apex:navigation:loaded', () => {
80+
new MegaMenu();
81+
new HamburgerMenu();
82+
});
6983
});
7084

7185

0 commit comments

Comments
 (0)