@progressive-development/pd-page
Version:
Progressive development page helper, teaser, menu, footer.
621 lines (583 loc) • 18.7 kB
JavaScript
import { css, LitElement, html } from 'lit';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { localized, msg } from '@lit/localize';
import { pdIcons } from '@progressive-development/pd-icon';
import '@progressive-development/pd-icon/pd-icon';
import '@progressive-development/pd-forms/pd-button-select-group';
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
let PdMenu = class extends LitElement {
constructor() {
super(...arguments);
this.activeRoute = "";
this.selectedLocale = "";
this.locales = [];
this.menuItems = [];
this.topMenuItems = [];
this.noBurgerMenu = false;
this.bgImage = "";
this._activeSecIndex = 0;
this._activeBurgerMenu = false;
this._smallScreen = false;
/** @ignore */
this._boundScrollHandler = this._handleScroll.bind(this);
/** @ignore */
this._boundMediaQueryHandler = this._handleMediaQuery.bind(this);
this._ticking = false;
}
connectedCallback() {
super.connectedCallback();
document.addEventListener("scroll", this._boundScrollHandler);
this._mediaQueryList = window.matchMedia("(max-width: 767px)");
this._mediaQueryList.addEventListener(
"change",
this._boundMediaQueryHandler
);
this._smallScreen = this._mediaQueryList.matches && !this.noBurgerMenu;
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener("scroll", this._boundScrollHandler);
if (this._mediaQueryList) {
this._mediaQueryList.removeEventListener(
"change",
this._boundMediaQueryHandler
);
}
}
_handleScroll() {
const scrollY = window.scrollY;
if (!this._ticking) {
window.requestAnimationFrame(() => {
this._updateActiveSection(scrollY);
this._ticking = false;
});
this._ticking = true;
}
}
_handleMediaQuery(e) {
this._smallScreen = e.matches && !this.noBurgerMenu;
}
_updateActiveSection(_scrollPos) {
let activeIndex = -1;
let closestDistance = Infinity;
let lastVisibleIndex = -1;
this.menuItems.forEach((item, index) => {
if (item.ref) {
const rect = item.ref.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
lastVisibleIndex = index;
}
if (rect.top <= 150 && rect.bottom > 0) {
const distance = Math.abs(rect.top);
if (distance < closestDistance) {
activeIndex = index;
closestDistance = distance;
}
}
}
});
if (lastVisibleIndex >= 0) {
const atBottom = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 50;
if (atBottom && lastVisibleIndex > activeIndex) {
activeIndex = lastVisibleIndex;
}
}
const newActiveIndex = activeIndex >= 0 ? activeIndex + 1 : 0;
if (newActiveIndex !== this._activeSecIndex && newActiveIndex > 0) {
const activeItem = this.menuItems[newActiveIndex - 1];
if (activeItem?.sec) {
this.dispatchEvent(
new CustomEvent("section-activated", {
detail: { sectionId: activeItem.sec },
bubbles: true,
composed: true
})
);
}
}
this._activeSecIndex = newActiveIndex;
}
render() {
return html`
${this.bgImage ? html`
<div
class="menu-bg"
style="background-image: url('${this.bgImage}')"
></div>
<div class="menu-bg-gradient"></div>
` : ""}
<nav
class="menu-container"
aria-label="${msg("Hauptnavigation", { id: "pd.menu.nav.aria" })}"
>
<slot name="slotLogo" @click="${this._logoClicked}"></slot>
<ul
id="${this._smallScreen ? "burger-menu-list" : ""}"
class="${classMap({
"menu-ul": true,
"burger-ul": this._smallScreen,
"burger-open": this._activeBurgerMenu
})}"
role="menubar"
>
${this._renderItems(
this._smallScreen ? [...this.menuItems, ...this.topMenuItems] : this.menuItems,
true
)}
</ul>
${!this._smallScreen ? html`
<ul class="menu-ul topMenu" role="menubar">
${this._renderItems(this.topMenuItems, false)}
${this.locales.length > 1 ? html`
<pd-button-select-group
class="locale-selector"
.options="${this.locales}"
.initValue="${this._getSelectedLocaleIndex()}"
group-label="${msg("Sprache auswählen", {
id: "pd.menu.locale.aria"
})}"
@pd-button-selection-changed="${this._handleLocaleChange}"
></pd-button-select-group>
` : ""}
</ul>
` : html`
<div class="menu-wrapper">
${this.locales.length > 1 ? html`
<pd-button-select-group
class="locale-selector"
.options="${this._getCompactLocales()}"
.initValue="${this._getSelectedLocaleIndex()}"
group-label="${msg("Sprache auswählen", {
id: "pd.menu.locale.aria"
})}"
@pd-button-selection-changed="${this._handleLocaleChange}"
></pd-button-select-group>
` : ""}
<div
class="burger-menu"
role="button"
tabindex="0"
aria-expanded="${this._activeBurgerMenu}"
aria-controls="burger-menu-list"
aria-label="${msg("Menü öffnen", {
id: "pd.menu.burger.aria"
})}"
@click="${this._toggleBurgerMenu}"
@keydown="${this._handleBurgerKeydown}"
>
<pd-icon
class="burgerLogo"
icon="${pdIcons.ICON_BURGER_MENU}"
?activeIcon="${!this._activeBurgerMenu}"
></pd-icon>
</div>
</div>
`}
</nav>
`;
}
_renderItems(items, highlight) {
return html`
${items.map(
(item, index) => html`
<li
class="item menu-li ${this._smallScreen ? "burger-li burger-item" : ""}
${highlight && (this._activeSecIndex === index + 1 || item.route === this.activeRoute) ? "active" : ""}"
role="menuitem"
tabindex="0"
data-key="${item.key}"
@click="${this._menuItemClicked}"
@keydown="${this._handleMenuItemKeydown}"
>
${item.icon ? html`<pd-icon
class="topItemLogo"
icon="${item.icon}"
></pd-icon>` : ""}
${item.name}
</li>
`
)}
`;
}
/**
* Handles keyboard events on menu items.
*/
_handleMenuItemKeydown(e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
this._menuItemClicked(e);
}
}
/**
* Handles keyboard events on burger menu toggle.
*/
_handleBurgerKeydown(e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
this._toggleBurgerMenu();
} else if (e.key === "Escape" && this._activeBurgerMenu) {
e.preventDefault();
this._activeBurgerMenu = false;
}
}
_toggleBurgerMenu() {
this._activeBurgerMenu = !this._activeBurgerMenu;
}
/**
* Returns the index of the currently selected locale in the locales array.
*/
_getSelectedLocaleIndex() {
return this.locales.findIndex((l) => l.value === this.selectedLocale);
}
/**
* Returns compact locale options for mobile display.
* Uses shortText if available, otherwise falls back to text.
*/
_getCompactLocales() {
return this.locales.map((l) => ({
...l,
text: l.shortText ?? l.text
}));
}
/**
* Handles locale selection change from PdButtonSelectGroup.
*/
_handleLocaleChange(e) {
const index = e.detail.selected;
const localeValue = this.locales[index]?.value;
if (localeValue && localeValue !== this.selectedLocale) {
this.dispatchEvent(
new CustomEvent("locale-change", {
detail: localeValue,
bubbles: true,
composed: true
})
);
}
}
_menuItemClicked(e) {
const key = e.target.dataset.key;
const item = [...this.menuItems, ...this.topMenuItems].find(
(i) => i.key === key
);
if (!item) return;
if (item.sec && item.ref) {
this.dispatchEvent(
new CustomEvent("route-event", {
detail: { el: item.ref },
bubbles: true,
composed: true
})
);
} else if (item.route && item.sec) {
this.dispatchEvent(
new CustomEvent("route-event", {
detail: {
route: item.route,
sectionId: item.sec
},
bubbles: true,
composed: true
})
);
} else if (item.route) {
this.dispatchEvent(
new CustomEvent("route-event", {
detail: { route: item.route },
bubbles: true,
composed: true
})
);
} else if (item.action) {
item.action();
}
if (this._activeBurgerMenu) {
this._activeBurgerMenu = false;
}
}
_logoClicked() {
PdMenu._scrollToTop();
if (this.logoRoute) {
this.dispatchEvent(
new CustomEvent("route-event", {
detail: {
route: this.logoRoute
},
bubbles: true,
composed: true
})
);
}
}
static _scrollToTop() {
window.scrollTo({ top: 0, behavior: "smooth" });
}
};
PdMenu.styles = [
css`
:host {
display: block;
position: relative;
height: var(--pd-menu-height, 100px);
background: var(--pd-menu-bg-col, var(--pd-default-col));
box-shadow: var(--pd-menu-shadow, var(--pd-shadow-lg));
border-radius: var(--pd-menu-border-radius, 0);
/*
Add into component, before styled from outside by using element
removed fixed again => not fit for the teaser, custom properties possible, but now handle this in SPAHelper header element
width: 100%;
position: fixed;
top: 0;
*/
}
/* Remove border-radius, shadow when burger menu is expanded */
:host([burger-open]) {
box-shadow: none;
border-radius: 0;
}
/* Background image layer */
.menu-bg {
position: absolute;
inset: 0;
background-size: var(--pd-menu-bg-size, cover);
background-repeat: var(--pd-menu-bg-repeat, no-repeat);
background-attachment: var(--pd-menu-bg-attachment, scroll);
background-position: var(--pd-menu-bg-position, center);
border-radius: var(--pd-menu-border-radius, 0);
z-index: 0;
}
.menu-bg-gradient {
position: absolute;
inset: 0;
background: var(--pd-menu-gradient, none);
border-radius: var(--pd-menu-border-radius, 0);
z-index: 1;
}
:host([bg-image]) .menu-container {
position: relative;
z-index: 2;
}
.menu-container {
height: 100%;
padding-left: var(--pd-spacing-md, 1rem);
padding-right: var(--pd-spacing-md, 1rem);
display: flex;
justify-content: space-between;
align-items: center;
white-space: nowrap;
max-width: var(
--pd-menu-max-width,
var(--pd-content-max-width, 1170px)
);
margin: 0 auto;
}
.menu-ul {
display: flex;
align-items: center;
list-style: none;
margin: 0;
margin-top: var(--pd-menu-content-offset-y, 0);
padding: 0;
}
.burger-ul {
visibility: hidden;
overflow: hidden;
z-index: -1;
position: absolute;
flex-direction: column;
align-items: stretch; /* Override .menu-ul center, stretch children to full width */
top: calc(var(--pd-menu-height, 100px) * -1);
left: 0;
right: 0;
background-color: var(
--pd-menu-burger-bg-col,
var(--pd-default-bg-col)
);
box-shadow: var(--pd-shadow-xl);
border-bottom: var(--pd-menu-burger-bottomborder);
padding: var(--pd-spacing-sm, 0.5rem) 0;
border-radius: 0 0 var(--pd-radius-lg) var(--pd-radius-lg);
transition: 0.5s;
transition-timing-function: ease-out;
transform: translateY(0);
opacity: 0;
}
.burger-ul.burger-open {
visibility: visible;
opacity: 1;
top: var(--pd-menu-burger-top, var(--pd-menu-height, 100px));
transition-timing-function: ease-in;
}
.menu-li {
display: flex;
align-items: center;
cursor: pointer;
border-radius: var(--pd-radius-md);
}
.burger-li {
border-radius: 0;
margin: 0;
border-bottom: 1px solid var(--pd-default-lightest-col);
transition: background-color 0.2s ease;
}
.burger-li:last-child {
border-bottom: none;
}
.burger-li:hover {
background-color: var(--pd-default-bg-light-col);
box-shadow: inset 4px 0 0 var(--pd-default-col);
}
.item {
color: var(--pd-menu-font-col, var(--pd-on-primary-col));
font-family: var(
--pd-menu-font-family,
var(--pd-default-font-title-family)
);
font-size: var(--pd-menu-font-size, 1.2em);
font-weight: var(--pd-menu-font-weight, bold);
padding: var(--pd-menu-item-padding, 0.6em);
text-shadow: var(--pd-menu-text-shadow, none);
}
.burger-item {
color: var(--pd-menu-burger-font-col, var(--pd-default-font-link-col));
font-size: var(--pd-default-font-content-size, 1rem);
font-weight: 500;
/* Override .item padding */
padding: 1.25rem var(--pd-spacing-lg, 2rem) 1.25rem 0.75rem;
}
.burger-item.active,
.burger-item:hover {
color: var(--pd-default-col);
}
.item.active,
.item:hover {
color: var(--pd-default-hover-col);
}
.item:hover {
background-color: var(--pd-menu-item-bg-hover-col);
}
.item:hover .topItemLogo {
--pd-icon-stroke-col: var(--pd-default-hover-col);
}
.topMenu {
width: 100%;
justify-content: end;
padding-left: 10px;
}
.topItemLogo {
--pd-icon-size: 1.4rem;
--pd-icon-bg-col: transparent;
--pd-icon-col: var(--pd-menu-font-col, var(--pd-on-primary-col));
pointer-events: none;
}
.burger-li .topItemLogo {
--pd-icon-col: var(
--pd-menu-burger-font-col,
var(--pd-default-font-link-col)
);
--pd-icon-size: 1.2rem;
margin-right: 0.5rem;
}
.burgerLogo {
/* Match height of locale-selector button (min-height 44px) */
--pd-icon-size: 2.5rem;
--pd-icon-bg-col: transparent;
pointer-events: none;
}
.menu-wrapper {
width: 100%;
display: flex;
align-items: center;
justify-content: end;
padding-right: 0.5rem;
}
.burger-menu {
display: flex;
gap: 0.3rem;
align-items: center;
cursor: pointer;
--pd-icon-col: var(--pd-default-hover-col);
--pd-icon-col-active: var(--pd-menu-font-col, var(--pd-on-primary-col));
}
.burger-menu:hover {
--pd-icon-col: var(--pd-default-hover-col);
--pd-icon-col-active: var(--pd-default-hover-col);
}
/* Doppelt zum logo-container, pd-logo sonst nicht mehr funktional
*/
::slotted(.logo) {
max-width: var(--pd-menu-logo-maxwidth, 8rem);
height: var(--pd-menu-logo-height, 60%);
padding: var(--pd-menu-logo-padding, 0 1.5rem 0 0);
fill: var(--pd-menu-logo-color, #067394);
cursor: pointer;
margin-top: var(--pd-menu-content-offset-y, 0);
object-fit: contain;
}
/* Locale selector styling */
.locale-selector {
--pd-button-border-col: transparent;
--pd-button-bg-col: transparent;
--pd-button-font-col: var(--pd-menu-font-col, var(--pd-on-primary-col));
--pd-button-bg-col-hover: var(--pd-menu-item-bg-hover-col);
--pd-button-scale: 0.8;
}
/* Focus-visible styles for accessibility */
.menu-li:focus-visible {
outline: 2px solid var(--pd-focus-ring-col, var(--pd-default-hover-col));
outline-offset: 2px;
}
.burger-menu:focus-visible {
outline: 2px solid var(--pd-focus-ring-col, var(--pd-default-hover-col));
outline-offset: 2px;
border-radius: var(--pd-radius-md);
}
`
];
__decorateClass([
property({ type: String })
], PdMenu.prototype, "activeRoute", 2);
__decorateClass([
property({ type: String })
], PdMenu.prototype, "selectedLocale", 2);
__decorateClass([
property({ type: String })
], PdMenu.prototype, "logoRoute", 2);
__decorateClass([
property({ type: Array })
], PdMenu.prototype, "locales", 2);
__decorateClass([
property({ type: Array })
], PdMenu.prototype, "menuItems", 2);
__decorateClass([
property({ type: Array })
], PdMenu.prototype, "topMenuItems", 2);
__decorateClass([
property({ type: Boolean })
], PdMenu.prototype, "noBurgerMenu", 2);
__decorateClass([
property({ type: String, reflect: true, attribute: "bg-image" })
], PdMenu.prototype, "bgImage", 2);
__decorateClass([
state()
], PdMenu.prototype, "_activeSecIndex", 2);
__decorateClass([
property({ type: Boolean, reflect: true, attribute: "burger-open" })
], PdMenu.prototype, "_activeBurgerMenu", 2);
__decorateClass([
state()
], PdMenu.prototype, "_smallScreen", 2);
PdMenu = __decorateClass([
localized()
], PdMenu);
export { PdMenu };