import { PktElement } from '@/base-elements/element' import { html, nothing, type PropertyValues, type TemplateResult } from 'lit' import { customElement, property, state } from 'lit/decorators.js' import { classMap } from 'lit/directives/class-map.js' import { booleanishConverter, type Booleanish } from 'shared-types' import { DEFAULT_HEADER_FOOTER_URL, deriveSocialIcon, fetchHeaderFooterData, mapOdsIcon, selectLocaleData, } from 'shared-utils/header-footer' import type { IHeaderFooterLocaleData, IHeaderMenuButton, IHeaderMenuLink, IHeaderMenuSection, IHeaderMenuServices, IPktHeaderMenu, THeaderFooterApi, THeaderMenuLocale, } from './types' import '@/components/accordion' import '@/components/icon' type LoadState = 'idle' | 'loading' | 'ready' | 'error' export class PktHeaderMenu extends PktElement implements IPktHeaderMenu { @property({ type: String, attribute: 'data-url' }) dataUrl: string = DEFAULT_HEADER_FOOTER_URL @property({ type: Object, attribute: false }) data?: THeaderFooterApi @property({ type: String }) locale: THeaderMenuLocale = 'nb-NO' @property({ type: Boolean, reflect: true, converter: booleanishConverter }) open: Booleanish = false @property({ type: Number, attribute: 'mobile-breakpoint' }) mobileBreakpoint: number = 768 @state() private loadState: LoadState = 'idle' @state() private fetchedData?: THeaderFooterApi @state() private isMobile = false private abortController?: AbortController private menuWidth = 0 private resizeObserver?: ResizeObserver override connectedCallback(): void { super.connectedCallback() this.classList.add('pkt-header-menu') this.updateOpenClass() this.observeWidth() if (!this.data) { void this.loadData() } else { this.loadState = 'ready' this.dispatchEvent( new CustomEvent('data-loaded', { detail: { data: this.data }, bubbles: true, composed: true, }), ) } } private updateOpenClass() { this.classList.toggle('pkt-header-menu--open', Boolean(this.open)) } override disconnectedCallback(): void { super.disconnectedCallback() this.abortController?.abort() this.resizeObserver?.disconnect() this.resizeObserver = undefined } override updated(changedProperties: PropertyValues): void { super.updated(changedProperties) if (changedProperties.has('open')) { this.updateOpenClass() } if ( changedProperties.has('mobileBreakpoint') && changedProperties.get('mobileBreakpoint') !== undefined ) { this.refreshIsMobile() } if ( changedProperties.has('dataUrl') && changedProperties.get('dataUrl') !== undefined && !this.data ) { void this.loadData() } if (changedProperties.has('data') && changedProperties.get('data') !== undefined && this.data) { this.loadState = 'ready' this.dispatchEvent( new CustomEvent('data-loaded', { detail: { data: this.data }, bubbles: true, composed: true, }), ) } } private observeWidth() { if (typeof ResizeObserver === 'undefined') return this.menuWidth = this.offsetWidth this.refreshIsMobile() this.resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { this.menuWidth = entry.borderBoxSize && entry.borderBoxSize.length > 0 ? entry.borderBoxSize[0].inlineSize : entry.contentRect.width this.refreshIsMobile() } }) this.resizeObserver.observe(this) } private refreshIsMobile() { this.isMobile = this.menuWidth < this.mobileBreakpoint } private async loadData() { this.abortController?.abort() this.abortController = new AbortController() this.loadState = 'loading' try { const data = await fetchHeaderFooterData(this.dataUrl, this.abortController.signal) this.fetchedData = data this.loadState = 'ready' this.dispatchEvent( new CustomEvent('data-loaded', { detail: { data }, bubbles: true, composed: true, }), ) } catch (error) { if ((error as Error).name === 'AbortError') return this.loadState = 'error' this.dispatchEvent( new CustomEvent('data-error', { detail: { error }, bubbles: true, composed: true, }), ) } } private get effectiveData(): THeaderFooterApi | undefined { return this.data ?? this.fetchedData } private get localeData(): IHeaderFooterLocaleData | undefined { return selectLocaleData(this.effectiveData, this.locale) } render() { if (this.loadState === 'loading') { return html` ` } if (this.loadState === 'error' || !this.localeData) { return html` ` } const { megamenu, i18n } = this.localeData const navAriaLabel = i18n?.navAriaLabel || 'Hovedmeny' if (this.isMobile) { return html` ` } return html` ` } private renderMobileAccordion( services: IHeaderMenuServices, sections: IHeaderMenuSection[], ): TemplateResult { return html`
${this.renderServicesList(services)} ${sections.map( (section, index) => html` ${this.renderSectionList(section.links)} `, )}
` } private renderServicesList(services: IHeaderMenuServices): TemplateResult { return html` ` } private renderServices(services: IHeaderMenuServices): TemplateResult { return html`

${services.title}

${this.renderServicesList(services)}
` } private renderButtons( buttons: IHeaderMenuButton[] | undefined, isMobilePlacement: boolean, ): TemplateResult | typeof nothing { if (!buttons || buttons.length === 0) return nothing const classes = classMap({ 'pkt-header-menu__buttons': true, 'pkt-header-menu__buttons--mobile': isMobilePlacement, }) return html`
${buttons.map( (button) => html` ${button.text} `, )}
` } private renderSections(sections: IHeaderMenuSection[]): TemplateResult | typeof nothing { if (!sections || sections.length === 0) return nothing return html`
${sections.map( (section) => html`

${section.title}

${this.renderSectionList(section.links)}
`, )}
` } private renderSectionList(links: IHeaderMenuLink[]): TemplateResult { return html` ` } private renderFooter( links: IHeaderMenuLink[], some: IHeaderMenuLink[], ): TemplateResult | typeof nothing { if ((!links || links.length === 0) && (!some || some.length === 0)) return nothing return html` ` } private renderSocialLink(entry: IHeaderMenuLink): TemplateResult { const iconName = deriveSocialIcon(entry.url, entry.text) return html`
  • ` } } declare global { interface HTMLElementTagNameMap { 'pkt-header-menu': PktHeaderMenu } } try { customElement('pkt-header-menu')(PktHeaderMenu) } catch (e) { console.warn('Forsøker å definere , men den er allerede definert') }