UNPKG

@progressive-development/pd-page

Version:

Progressive development page helper, teaser, menu, footer.

621 lines (583 loc) 18.7 kB
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 };