export interface DesktopLinks { label: string; url: string; menuLinkId: string; } export interface DonateLink { url: string; label: string; } export interface ComponentLabels { open: string; close: string; expand?: string; label?: string; } import {Component, Element, Event, EventEmitter, h, Listen, Prop, State} from '@stencil/core'; import ResizeObserver from 'resize-observer-polyfill'; import {MenuType, ColorVariant, SiteLogoSize} from '../../utils/utils'; @Component({ tag: 'hy-site-header', styleUrl: 'site-header.scss', shadow: true, }) export class SiteHeader { @Element() el: HTMLElement; @Prop() isGroup: boolean = false; @Prop() dataMenuLanguage: string; @Prop() dataMenuDonate: string; @Prop() dataSiteHeaderLabels: string; @Prop() dataSiteSearchLabels: string; @Prop() logoUrl?: string; @Prop() logoLabel?: string; @Prop() siteLabel?: string; @Prop() siteUrl?: string; @Prop() menuLabel: string = 'Menu'; @Prop() menuLabelOpen?: string; @Prop() menuLabelClose?: string; @Prop({reflect: true}) menuType: MenuType = MenuType.default; /* First level menu links to be displayed on Desktop screens. * */ @Prop() dataDesktopLinks: DesktopLinks[] | string; @Prop() dataMainMenuLinks: DesktopLinks[] | string; @Prop() dataSearchTools: DesktopLinks[] | string; @State() isMobile: boolean; @State() isMenuOpen: boolean = false; @State() isDesktopMenuOpen: boolean = false; @Event() headerScrollUp: EventEmitter; @Event() headerScrollDown: EventEmitter; @Event() menuMobileToggled: EventEmitter; @Event() menuMobileTopToggled: EventEmitter; private ro: ResizeObserver; private donateLink: DonateLink[]; private menuLabels: ComponentLabels[]; private searchLabels: ComponentLabels[]; private groupPages: ComponentLabels[]; private languageLabels: ComponentLabels[]; @State() lastScrollTop = 0; @State() delta = 5; @State() navbarHeight = 0; @State() didScroll = false; @State() intervalId; // Listener for toggling mobile menu panel on or off. @Listen('mobileMenuToggle') mobileMenuToggle() { this.isMenuOpen = !this.isMenuOpen; this.menuMobileToggled.emit(); } // Listener for toggling mobile menu top panel on or off. @Listen('mobileMenuTopToggle') mobileMenuTopToggle() { this.isMenuOpen = !this.isMenuOpen; this.menuMobileTopToggled.emit(); } @Listen('scroll', {target: 'window'}) handleScroll() { if (this.el.getAttribute('menu-type') === 'desktop') { this.didScroll = true; } } componentDidLoad() { // Set the browser resize observer to gather information about browser width. this.ro = new ResizeObserver((entries) => { for (const entry of entries) { this.applySizeClasses(entry.contentRect.width); } }); this.ro.observe(document.body); // Pass the dataMenuLanguage prop to menu component. this.el.children[0].setAttribute('data-menu-language', this.dataMenuLanguage); this.navbarHeight = this.el.getBoundingClientRect().height; } componentWillLoad() { // Check for site header labels and set them to variables accordingly. if (this.dataSiteHeaderLabels) { const labels = JSON.parse(this.dataSiteHeaderLabels); this.menuLabels = labels.menu_labels; this.languageLabels = labels.language_labels; this.searchLabels = labels.search_labels; this.groupPages = labels.group_pages; this.el.children[0].setAttribute('menu-button-breadcrumb-home', this.menuLabels['home']); this.el.children[0].setAttribute('menu-button-breadcrumb-main', this.menuLabels['main']); this.el.children[0].setAttribute('menu-button-breadcrumb-logourl', this.logoUrl); } // Check for the donation link information and set it to variables accordingly. // Also pass the donation link data to menu-component. if (this.dataMenuDonate) { this.donateLink = JSON.parse(this.dataMenuDonate); this.el.children[0].setAttribute('data-menu-donate', this.dataMenuDonate); } } componentWillUpdate() { // Pass the necessary information to menu component. These props will be // used in mobile menu. this.el.children[0].setAttribute('open', this.isMenuOpen.toString()); this.el.children[0].setAttribute('logo-label', this.logoLabel); this.el.children[0].setAttribute('logo-url', this.logoUrl); this.el.children[0].setAttribute('site-label', this.siteLabel); this.el.children[0].setAttribute('site-url', this.siteUrl); this.el.children[0].setAttribute('menu-button-submenu-expand', this.menuLabels['expand']); this.el.children[0].setAttribute('menu-button-breadcrumb-return', this.menuLabels['return']); this.el.children[0].setAttribute('menu-button-breadcrumb-home', this.menuLabels['home']); this.el.children[0].setAttribute('menu-button-breadcrumb-main', this.menuLabels['main']); this.el.children[0].setAttribute('menu-button-breadcrumb-logourl', this.logoUrl); this.el.children[0].setAttribute('menu-language-label-open', this.languageLabels['open']); this.el.children[0].setAttribute('menu-language-label-close', this.languageLabels['close']); this.el.children[0].setAttribute('label-front-page', this.menuLabels['front_page']); this.intervalId = setInterval(() => { this.timer(); }, 250); } timer() { if (this.didScroll) { this.hasScrolled(); this.didScroll = false; } } hasScrolled() { let topOffset = window.pageYOffset; const bodyElementClasses = document.querySelector('body').classList; if ( bodyElementClasses.contains('hy-menu-sidepanel__no-scroll') || bodyElementClasses.contains('hy-menu-sidebar__no-scroll') ) { return; } if (Math.abs(this.lastScrollTop - topOffset) <= this.delta) { return; } let hySiteHeader = this.el.shadowRoot.querySelector('.hy-site-header') as HTMLElement; // If current position > last position AND scrolled past navbar... if (topOffset > this.lastScrollTop && topOffset > this.navbarHeight) { // Scroll Down hySiteHeader.classList.remove('hy-site-header--sticky-visible'); hySiteHeader.classList.remove('has-toolbar', 'has-toolbar--large'); // Close Search panel //CludoSayt.hide(); this.headerScrollDown.emit(); } else { // Scroll Up if (topOffset < this.el.offsetTop + this.navbarHeight) { hySiteHeader.classList.remove('hy-site-header--sticky-active'); hySiteHeader.classList.remove('hy-site-header--sticky-visible'); hySiteHeader.classList.remove('hy-site-header--sticky-hidden'); hySiteHeader.classList.remove('has-toolbar', 'has-toolbar--large'); // Close desktop menu panel if it's open. } else { hySiteHeader.classList.add('hy-site-header--sticky-active'); hySiteHeader.classList.remove('hy-site-header--sticky-hidden'); hySiteHeader.classList.add('hy-site-header--sticky-visible'); if (bodyElementClasses.contains('toolbar-horizontal')) { hySiteHeader.classList.add('has-toolbar'); if (bodyElementClasses.contains('toolbar-tray-open')) { hySiteHeader.classList.add('has-toolbar--large'); } } } } this.lastScrollTop = topOffset; } componentDidUnload() { this.ro.disconnect(); } applySizeClasses(size: number) { // Set the menu-type based on the width of the browser. if (size <= 1200 && size > 960) { this.menuType = MenuType.tablet; } else if (size <= 960) { this.menuType = MenuType.mobile; } else { this.menuType = MenuType.desktop; } // Pass the menu type to menu component. this.isMobile = this.menuType === (MenuType.mobile || MenuType.tablet); const menuAttribute = this.menuType === MenuType.tablet ? MenuType.mobile : this.menuType; this.el.children[0].setAttribute('menu-type', menuAttribute); } render() { const logoSize = this.isMobile ? SiteLogoSize.small : SiteLogoSize.big; const logoSizeGroup = this.isMobile ? SiteLogoSize.small : SiteLogoSize.small; const logoColor = ColorVariant.black; const logoColorGroup = ColorVariant.white; let classAttributes = ['hy-site-header', 'hy-site-header--' + this.menuType]; switch (this.menuType) { case MenuType.desktop: // Larger than 1200px screens return [ <header class={classAttributes.join(' ')}> <div class={{'hy-backdrop': true, 'is-active': this.isMenuOpen}} /> {this.isGroup && ( <div class="hy-site-header__content-top"> <div class={'hy-site-header__logo-container group '} role="navigation"> <hy-site-logo is-group={true} size={logoSizeGroup} color={logoColorGroup} url={this.siteUrl} label={this.siteLabel} /> <hy-menu-main-group tabindex="0" is-mobile={false} menu-label={this.groupPages['university_main_menu'] ?? null} data-main-menu={this.dataMainMenuLinks} /> </div> <div class={'menu--secondary menu--secondary--group'}> <hy-menu-language class={'menu--secondary__item is-first group'} is-group={true} is-mobile={false} data-menu-language={this.dataMenuLanguage} labels={this.languageLabels} /> <hy-site-search class={'menu--secondary__item group'} size={16} color={logoColorGroup} is-group={true} show-label={true} labels={this.searchLabels} search-labels={this.dataSiteSearchLabels} search-tools={this.dataSearchTools} /> {this.donateLink.map((i) => { return ( <a class={'menu--secondary__item hy-link__donate group'} href={i.url}> <hy-icon icon={'hy-icon-heart-support'} size={16} fill={logoColorGroup} /> <span class={'hy-link__donate__label group'}>{i.label}</span> </a> ); })} </div> </div> )} <div class={{'hy-site-header__content': true, group: this.isGroup}}> <div class={'hy-site-header__logo-container'}> {this.isGroup ? ( <a class={'group'} href={this.logoUrl}> {this.logoLabel} </a> ) : ( <hy-site-logo size={logoSize} color={logoColor} url={this.logoUrl} label={this.logoLabel} /> )} </div> <hy-desktop-menu-links data-desktop-links={this.dataDesktopLinks}></hy-desktop-menu-links> {!this.isGroup && ( <div class={'menu--secondary'}> <hy-menu-language class={'menu--secondary__item is-first'} is-mobile={false} data-menu-language={this.dataMenuLanguage} labels={this.languageLabels} /> <hy-site-search class={'menu--secondary__item'} size={14} color={ColorVariant.black} show-label={true} labels={this.searchLabels} search-labels={this.dataSiteSearchLabels} search-tools={this.dataSearchTools} /> {this.donateLink.map((i) => { return ( <a class={'menu--secondary__item hy-link__donate'} href={i.url}> <hy-icon icon={'hy-icon-heart-support'} size={14} fill={ColorVariant.black} /> <span class={'hy-link__donate__label'}>{i.label}</span> </a> ); })} </div> )} </div> </header>, ]; case MenuType.tablet: // 960px-1200px screens return ( <header class={classAttributes.join(' ')}> <div class={{'hy-backdrop': true, 'is-active': this.isMenuOpen}} /> {this.isGroup && ( <div class="hy-site-header__content-top"> <div class={'hy-site-header__logo-container group '} role="navigation"> <hy-site-logo is-group={true} size={logoSizeGroup} color={logoColorGroup} url={this.siteUrl} label={this.siteLabel} /> <hy-menu-main-group is-mobile={false} menu-label={this.groupPages['university_main_menu'] ?? ''} data-main-menu={this.dataMainMenuLinks} /> </div> <div class={'menu--secondary menu--secondary--group'}> <hy-menu-language class={'menu--secondary__item is-first group'} is-group={true} is-mobile={false} data-menu-language={this.dataMenuLanguage} labels={this.languageLabels} /> <hy-site-search class={'menu--secondary__item group'} size={16} color={logoColorGroup} show-label={true} is-group={true} labels={this.searchLabels} search-labels={this.dataSiteSearchLabels} search-tools={this.dataSearchTools} /> {this.donateLink.map((i) => { return ( <a class={'menu--secondary__item hy-link__donate group'} href={i.url}> <hy-icon icon={'hy-icon-heart-support'} size={16} fill={logoColorGroup} /> <span class={'hy-link__donate__label group'}>{i.label}</span> </a> ); })} </div> </div> )} <div class="hy-site-header__content"> <div class={'hy-site-header__logo-container'}> {this.isGroup ? ( <a class={'group'} href={this.logoUrl}> {this.logoLabel} </a> ) : ( <hy-site-logo size={logoSize} color={logoColor} url={this.logoUrl} label={this.logoLabel} /> )} </div> {!this.isGroup && ( <div class={'menu--secondary'}> <hy-menu-language class={'menu--secondary__item is-first'} is-mobile={false} data-menu-language={this.dataMenuLanguage} labels={this.languageLabels} /> <hy-site-search class={'menu--secondary__item'} size={14} color={ColorVariant.black} show-label={true} labels={this.searchLabels} search-labels={this.dataSiteSearchLabels} search-tools={this.dataSearchTools} /> {this.donateLink.map((i) => { return ( <a class={'menu--secondary__item hy-link__donate'} href={i.url}> <hy-icon icon={'hy-icon-heart-support'} size={14} fill={ColorVariant.black} /> <span class={'hy-link__donate__label'}>{i.label}</span> </a> ); })} </div> )} <div class={'hy-site-header__menu-container'}> <span class={{ 'hy-site-header__menu-label': true, 'is-visible': this.isMenuOpen, }} > {this.menuLabel} </span> <button onClick={() => this.mobileMenuToggle()} class={{ 'hy-site-header__panel-toggle': true, 'is-open': this.isMenuOpen, }} aria-label={this.isMenuOpen ? this.menuLabels['close'] : this.menuLabels['open']} > {this.isMenuOpen ? ( <span class="hy-site-header__panel-toggle__label"> {this.menuLabelClose} <hy-icon icon={'hy-icon-remove'} size={20} fill={ColorVariant.black} /> </span> ) : ( <span class="hy-site-header__panel-toggle__label"> {this.menuLabelOpen} <hy-icon icon={'hy-icon-hamburger'} size={20} fill={ColorVariant.black} /> </span> )} </button> <div class={{ 'is-open': this.isMenuOpen, 'hy-site-header__panel': true, }} > <slot name={'menu'} /> </div> </div> </div> </header> ); case MenuType.mobile: // Smaller than 960px screens return ( <header class={classAttributes.join(' ')}> <div class={{'hy-backdrop': true, 'is-active': this.isMenuOpen}} /> {this.isGroup && ( <div class="hy-site-header__content-top" is-mobile={true}> <div class={'hy-site-header__logo-container group '}> <hy-site-logo size={logoSizeGroup} color={logoColorGroup} url={this.siteUrl} label={this.siteLabel} /> </div> <div class={'menu--secondary menu--secondary--group'}> <hy-site-search class={'menu--secondary__item group'} size={18} color={logoColorGroup} is-group={true} show-label={true} labels={this.searchLabels} search-labels={this.dataSiteSearchLabels} search-tools={this.dataSearchTools} /> <hy-menu-main-group site-label={this.siteLabel} site-url={this.siteUrl} logo-label={this.logoLabel} logo-url={this.logoUrl} is-mobile={true} donate={this.donateLink} data-main-menu={this.dataMainMenuLinks} /> </div> </div> )} <div class="hy-site-header__content"> <div class={'hy-site-header__logo-container'}> {this.isGroup ? ( <a class={'group'} href={this.logoUrl}> {this.logoLabel} </a> ) : ( <hy-site-logo size={logoSize} color={logoColor} url={this.logoUrl} label={this.logoLabel} /> )} </div> <div class={{'hy-site-header__menu-container': true, 'is-open': this.isMenuOpen}}> {!this.isGroup && ( <hy-site-search class={'menu--secondary__item'} size={14} color={ColorVariant.black} show-label={true} labels={this.searchLabels} search-labels={this.dataSiteSearchLabels} search-tools={this.dataSearchTools} /> )} <button onClick={() => this.mobileMenuToggle()} class={{ 'hy-site-header__panel-toggle': true, 'is-open': this.isMenuOpen, }} aria-label={this.isMenuOpen ? this.menuLabels['close'] : this.menuLabels['open']} > {this.isMenuOpen ? ( <span class="hy-site-header__panel-toggle__label"> {this.menuLabelClose} <hy-icon icon={'hy-icon-remove'} size={20} fill={ColorVariant.black} /> </span> ) : ( <span class="hy-site-header__panel-toggle__label"> {this.menuLabelOpen} <hy-icon icon={'hy-icon-hamburger'} size={20} fill={ColorVariant.black} /> </span> )} </button> <div class={{ 'is-open': this.isMenuOpen, 'hy-site-header__panel': true, }} > <slot name={'menu'} /> </div> </div> </div> </header> ); } } }