Skip to content

Commit 516a330

Browse files
committed
bitrise-navigation simplify code
1 parent a50f455 commit 516a330

8 files changed

Lines changed: 220 additions & 233 deletions

File tree

src/js/bitrise-navigation.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@
1010
// ---------------------------------------------------------------------------
1111

1212
import { cachedData, setCachedData, initLoader } from './bitrise-navigation/loader';
13-
import { connectedInstances as navInstances, init as initNav } from './bitrise-navigation/BitriseNavigation';
14-
import { connectedInstances as footerInstances, init as initFooter } from './bitrise-navigation/BitriseFooter';
15-
import { renderNavigation } from './bitrise-navigation/renderNavigation';
16-
import { renderFooter } from './bitrise-navigation/renderFooter';
13+
import { clearShadowRoot } from './bitrise-navigation/css';
14+
import {
15+
connectedInstances as navInstances,
16+
renderNavigation,
17+
init as initNav,
18+
} from './bitrise-navigation/BitriseNavigation';
19+
import {
20+
connectedInstances as footerInstances,
21+
renderFooter,
22+
init as initFooter,
23+
} from './bitrise-navigation/BitriseFooter';
1724

1825
// Initialise the shared loader (global callback + script injection).
1926
initLoader();
@@ -52,10 +59,7 @@ if (import.meta.webpackHot) {
5259
// Clean up shadow roots.
5360
document.querySelectorAll('bitrise-navigation, bitrise-footer').forEach((el) => {
5461
if (el.shadowRoot) {
55-
el.shadowRoot.innerHTML = '';
56-
if ('adoptedStyleSheets' in Document.prototype) {
57-
el.shadowRoot.adoptedStyleSheets = [];
58-
}
62+
clearShadowRoot(el.shadowRoot);
5963
}
6064
});
6165

src/js/bitrise-navigation/BitriseFooter.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
11
import { cachedData, ensureLoader, addDataListener, cleanupFontsIfUnused } from './loader';
2-
import { renderFooter } from './renderFooter';
2+
import { processCSS, injectFontFaces, buildShadowCSS, applyStyles, sanitizeHTML, clearShadowRoot } from './css';
33

44
/** All currently-connected <bitrise-footer> elements. */
55
export const connectedInstances = new Set();
66

7+
// ---- Rendering --------------------------------------------------------------
8+
9+
/**
10+
* Render the footer HTML + CSS into a host element's shadow root.
11+
* @param {HTMLElement} host - A <bitrise-footer> element with an attached shadow root.
12+
* @param {{ html: { nav: string, footer: string }, css: string }} data - Payload from the loader script.
13+
*/
14+
export function renderFooter(host, data) {
15+
const { shadowRoot } = host;
16+
if (!shadowRoot) return;
17+
18+
// Clear previous content.
19+
clearShadowRoot(shadowRoot);
20+
21+
if (data.css) {
22+
const { css, fontFaces } = processCSS(data.css);
23+
injectFontFaces(fontFaces);
24+
applyStyles(shadowRoot, buildShadowCSS(css, 'footer'));
25+
}
26+
27+
if (data.html.footer) {
28+
const wrapper = document.createElement('div');
29+
wrapper.innerHTML = sanitizeHTML(data.html.footer);
30+
shadowRoot.appendChild(wrapper);
31+
}
32+
}
33+
734
// ---- Custom element ----------------------------------------------------------
835

936
export class BitriseFooter extends HTMLElement {
@@ -26,10 +53,7 @@ export class BitriseFooter extends HTMLElement {
2653
connectedInstances.delete(this);
2754

2855
if (this.shadowRoot) {
29-
this.shadowRoot.innerHTML = '';
30-
if ('adoptedStyleSheets' in Document.prototype) {
31-
this.shadowRoot.adoptedStyleSheets = [];
32-
}
56+
clearShadowRoot(this.shadowRoot);
3357
}
3458

3559
cleanupFontsIfUnused();

src/js/bitrise-navigation/BitriseNavigation.js

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,59 @@
11
import { cachedData, ensureLoader, addDataListener, cleanupFontsIfUnused } from './loader';
2-
import { renderNavigation } from './renderNavigation';
2+
import { processCSS, injectFontFaces, buildShadowCSS, applyStyles, sanitizeHTML, clearShadowRoot } from './css';
3+
import { bindNavBehaviour, bindSmartScroll } from './behaviour';
34

45
/** All currently-connected <bitrise-navigation> elements. */
56
export const connectedInstances = new Set();
67

8+
const VALID_POSITIONS = ['static', 'sticky', 'smart'];
9+
10+
// ---- Rendering --------------------------------------------------------------
11+
12+
/**
13+
* Render the navigation HTML + CSS into a host element's shadow root.
14+
* @param {HTMLElement} host - A <bitrise-navigation> element with an attached shadow root.
15+
* @param {{ html: string, css: string }} data - Payload from the loader script.
16+
*/
17+
export function renderNavigation(host, data) {
18+
const { shadowRoot } = host;
19+
if (!shadowRoot) return;
20+
21+
// Clean up previous smart-scroll listener.
22+
if (host.cleanupSmartScroll) {
23+
host.cleanupSmartScroll();
24+
host.cleanupSmartScroll = null;
25+
}
26+
27+
// Clear previous content.
28+
clearShadowRoot(shadowRoot);
29+
30+
const attr = host.getAttribute('position');
31+
const positionMode = VALID_POSITIONS.includes(attr) ? attr : 'sticky';
32+
33+
// Combine shared CSS with inline nav CSS extracted from <style> tags.
34+
const rawCSS = [data.css, data.inlineCss].filter(Boolean).join('\n');
35+
if (rawCSS) {
36+
const { css, fontFaces } = processCSS(rawCSS);
37+
injectFontFaces(fontFaces);
38+
applyStyles(shadowRoot, buildShadowCSS(css, positionMode));
39+
}
40+
41+
const wrapper = document.createElement('div');
42+
wrapper.innerHTML = sanitizeHTML(data.html.nav);
43+
shadowRoot.appendChild(wrapper);
44+
45+
// Clean up previous behaviour listeners.
46+
if (host.cleanupBehaviour) {
47+
host.cleanupBehaviour();
48+
host.cleanupBehaviour = null;
49+
}
50+
host.cleanupBehaviour = bindNavBehaviour(shadowRoot);
51+
52+
if (positionMode === 'smart') {
53+
host.cleanupSmartScroll = bindSmartScroll(host);
54+
}
55+
}
56+
757
// ---- Custom element ----------------------------------------------------------
858

959
export class BitriseNavigation extends HTMLElement {
@@ -51,10 +101,7 @@ export class BitriseNavigation extends HTMLElement {
51101
connectedInstances.delete(this);
52102

53103
if (this.shadowRoot) {
54-
this.shadowRoot.innerHTML = '';
55-
if ('adoptedStyleSheets' in Document.prototype) {
56-
this.shadowRoot.adoptedStyleSheets = [];
57-
}
104+
clearShadowRoot(this.shadowRoot);
58105
}
59106

60107
cleanupFontsIfUnused();

src/js/bitrise-navigation/behaviour.js

Lines changed: 88 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,24 @@ const MOBILE_MQ = '(max-width: 1115px)';
1313
// ---------------------------------------------------------------------------
1414

1515
/**
16-
* Open a dropdown by toggling its state attributes.
16+
* Set a dropdown's open/closed state.
1717
* @param {Element} dropdown - A .nav_dropdown_component element
18+
* @param {boolean} open
1819
*/
19-
function openDropdown(dropdown) {
20+
function setDropdownOpen(dropdown, open) {
2021
const toggle = dropdown.querySelector('.w-dropdown-toggle');
2122
if (!toggle) return;
22-
toggle.setAttribute('aria-expanded', 'true');
23-
toggle.classList.add('w--open');
24-
dropdown.querySelector('.w-dropdown-list')?.classList.add('w--open');
25-
}
26-
27-
/**
28-
* Close a dropdown by toggling its state attributes.
29-
* @param {Element} dropdown - A .nav_dropdown_component element
30-
*/
31-
function closeDropdown(dropdown) {
32-
const toggle = dropdown.querySelector('.w-dropdown-toggle');
33-
if (!toggle) return;
34-
toggle.setAttribute('aria-expanded', 'false');
35-
toggle.classList.remove('w--open');
36-
dropdown.querySelector('.w-dropdown-list')?.classList.remove('w--open');
23+
toggle.setAttribute('aria-expanded', String(open));
24+
toggle.classList.toggle('w--open', open);
25+
dropdown.querySelector('.w-dropdown-list')?.classList.toggle('w--open', open);
3726
}
3827

3928
/**
4029
* Close every open dropdown in the shadow root.
4130
* @param {ShadowRoot} root
4231
*/
4332
function closeAllDropdowns(root) {
44-
root.querySelectorAll('.nav_dropdown_component.w-dropdown').forEach(closeDropdown);
33+
root.querySelectorAll('.nav_dropdown_component.w-dropdown').forEach((d) => setDropdownOpen(d, false));
4534
}
4635

4736
/**
@@ -61,28 +50,36 @@ function bindDropdowns(root) {
6150
if (!toggle) return;
6251

6352
// Desktop: hover
64-
dropdown.addEventListener('mouseenter', () => {
65-
if (mql.matches) return;
66-
closeAllDropdowns(root);
67-
openDropdown(dropdown);
68-
}, { signal: ac.signal });
69-
70-
dropdown.addEventListener('mouseleave', () => {
71-
if (mql.matches) return;
72-
closeDropdown(dropdown);
73-
}, { signal: ac.signal });
53+
dropdown.addEventListener(
54+
'mouseenter',
55+
() => {
56+
if (mql.matches) return;
57+
closeAllDropdowns(root);
58+
setDropdownOpen(dropdown, true);
59+
},
60+
{ signal: ac.signal },
61+
);
62+
63+
dropdown.addEventListener(
64+
'mouseleave',
65+
() => {
66+
if (mql.matches) return;
67+
setDropdownOpen(dropdown, false);
68+
},
69+
{ signal: ac.signal },
70+
);
7471

7572
// Mobile: click (only when mobile menu is open)
76-
toggle.addEventListener('click', (e) => {
77-
if (!mql.matches) return;
78-
e.preventDefault();
79-
const isOpen = toggle.getAttribute('aria-expanded') === 'true';
80-
if (isOpen) {
81-
closeDropdown(dropdown);
82-
} else {
83-
openDropdown(dropdown);
84-
}
85-
}, { signal: ac.signal });
73+
toggle.addEventListener(
74+
'click',
75+
(e) => {
76+
if (!mql.matches) return;
77+
e.preventDefault();
78+
const isOpen = toggle.getAttribute('aria-expanded') === 'true';
79+
setDropdownOpen(dropdown, !isOpen);
80+
},
81+
{ signal: ac.signal },
82+
);
8683
});
8784

8885
return () => controllers.forEach((ac) => ac.abort());
@@ -125,71 +122,74 @@ function bindMobileMenu(root) {
125122
const midLine = lines[1];
126123
const botLine = lines[2];
127124

128-
function setHamburgerOpen() {
125+
/**
126+
* Apply transforms to hamburger lines to match the open/closed state.
127+
* @param {boolean} open - Whether the menu is open, which determines the transform applied.
128+
*/
129+
function setHamburgerState(open) {
129130
if (topLine) {
130-
topLine.style.transform = 'translate3d(0px, 0.425rem, 0px) scale3d(1, 1, 1) rotateX(0deg) rotateY(0deg) rotateZ(45deg) skew(0deg, 0deg)';
131-
topLine.style.transformStyle = 'preserve-3d';
131+
topLine.style.transform = open
132+
? 'translate3d(0px, 0.425rem, 0px) scale3d(1, 1, 1) rotateX(0deg) rotateY(0deg) rotateZ(45deg) skew(0deg, 0deg)'
133+
: '';
134+
topLine.style.transformStyle = open ? 'preserve-3d' : '';
132135
}
133136
if (midLine) {
134-
midLine.style.opacity = '0';
137+
midLine.style.opacity = open ? '0' : '';
135138
}
136139
if (botLine) {
137-
botLine.style.transform = 'translate3d(0px, -0.425rem, 0px) scale3d(1, 1, 1) rotateX(0deg) rotateY(0deg) rotateZ(-45deg) skew(0deg, 0deg)';
138-
botLine.style.transformStyle = 'preserve-3d';
140+
botLine.style.transform = open
141+
? 'translate3d(0px, -0.425rem, 0px) scale3d(1, 1, 1) rotateX(0deg) rotateY(0deg) rotateZ(-45deg) skew(0deg, 0deg)'
142+
: '';
143+
botLine.style.transformStyle = open ? 'preserve-3d' : '';
139144
}
140145
}
141146

142-
function setHamburgerClosed() {
143-
if (topLine) { topLine.style.transform = ''; topLine.style.transformStyle = ''; }
144-
if (midLine) { midLine.style.opacity = ''; }
145-
if (botLine) { botLine.style.transform = ''; botLine.style.transformStyle = ''; }
146-
}
147-
148-
function openMenu() {
149-
// Move menu into overlay (like original Webflow JS).
150-
if (overlay) {
151-
overlay.style.display = 'block';
152-
overlay.style.height = `${document.documentElement.scrollHeight}px`;
153-
overlay.appendChild(menu);
154-
menu.setAttribute('data-nav-menu-open', '');
155-
}
156-
toggle.classList.add('w--open');
157-
toggle.setAttribute('aria-expanded', 'true');
158-
setHamburgerOpen();
159-
document.body.style.overflow = 'hidden';
160-
}
161-
162-
function closeMenu() {
163-
toggle.classList.remove('w--open');
164-
toggle.setAttribute('aria-expanded', 'false');
165-
setHamburgerClosed();
166-
closeAllDropdowns(root);
167-
// Move menu back to original position.
168-
if (overlay) {
169-
menu.removeAttribute('data-nav-menu-open');
170-
if (menuNextSibling) {
171-
menuParent.insertBefore(menu, menuNextSibling);
172-
} else {
173-
menuParent.appendChild(menu);
147+
/**
148+
* Set the menu open or closed.
149+
* @param {boolean} open - Whether the menu should be open.
150+
*/
151+
function setMenuOpen(open) {
152+
toggle.classList.toggle('w--open', open);
153+
toggle.setAttribute('aria-expanded', String(open));
154+
setHamburgerState(open);
155+
156+
if (open) {
157+
// Move menu into overlay (like original Webflow JS).
158+
if (overlay) {
159+
overlay.style.display = 'block';
160+
overlay.style.height = `${document.documentElement.scrollHeight}px`;
161+
overlay.appendChild(menu);
162+
menu.setAttribute('data-nav-menu-open', '');
174163
}
175-
overlay.style.display = '';
176-
overlay.style.height = '';
164+
document.body.style.overflow = 'hidden';
165+
} else {
166+
closeAllDropdowns(root);
167+
// Move menu back to original position.
168+
if (overlay) {
169+
menu.removeAttribute('data-nav-menu-open');
170+
if (menuNextSibling) {
171+
menuParent.insertBefore(menu, menuNextSibling);
172+
} else {
173+
menuParent.appendChild(menu);
174+
}
175+
overlay.style.display = '';
176+
overlay.style.height = '';
177+
}
178+
document.body.style.overflow = '';
177179
}
178-
document.body.style.overflow = '';
179180
}
180181

181-
toggle.addEventListener('click', () => {
182-
const opening = !toggle.classList.contains('w--open');
183-
if (opening) {
184-
openMenu();
185-
} else {
186-
closeMenu();
187-
}
188-
}, { signal: ac.signal });
182+
toggle.addEventListener(
183+
'click',
184+
() => {
185+
setMenuOpen(!toggle.classList.contains('w--open'));
186+
},
187+
{ signal: ac.signal },
188+
);
189189

190190
return () => {
191191
ac.abort();
192-
closeMenu();
192+
setMenuOpen(false);
193193
};
194194
}
195195

0 commit comments

Comments
 (0)