import { ExportNs } from '../../esl-utils/environment/export-ns' ; import { attr , jsonAttr , ESLBaseElement } from '../../esl-base-element/core' ; import { afterNextRender } from '../../esl-utils/async/raf' ; import { bind } from '../../esl-utils/decorators/bind' ; import { format } from '../../esl-utils/misc/format' ; import { memoize } from '../../esl-utils/decorators/memoize' ; import { CSSClassUtils } from '../../esl-utils/dom/class' ; import { ESLMediaRuleList } from '../../esl-media-query/core' ; import { TraversingQuery } from '../../esl-traversing-query/core' ; import { ESLPanel } from '../../esl-panel/core' ; import type { PanelActionParams } from '../../esl-panel/core' ; author Julia Murashko *
 * ESLPanelGroup is a custom element that is used as a container for a group of  ESLPanel s
  @ ExportNs ( 'PanelGroup' ) export class ESLPanelGroup extends ESLBaseElement { public static is = 'esl-panel-group' ; /** List of supported modes */ public static supportedModes = [ 'tabs' , 'accordion' , 'open' ] ; /** Rendering mode of the component (takes values from the list of supported modes; 'accordion' by default) */ @ attr ( { defaultValue : 'accordion' } ) public mode : string ; /** Rendering mode class pattern. Uses  format syntax for `mode` placeholder  @ attr ( { defaultValue : 'esl-{mode}-view' } ) public modeCls : string ; /** Element  TraversingQuery selector to add class that identifies the rendering mode (ESLPanelGroup itself by default)  @ attr ( { defaultValue : '' } ) public modeClsTarget : string ; /** Class(es) to be added during animation ('animate' by default) */ @ attr ( { defaultValue : 'animate' } ) public animationClass : string ; /** Time to clear animation common params (max-height style + classes) ('auto' by default) */ @ attr ( { defaultValue : 'auto' } ) public fallbackDuration : number | 'auto' ; /** List of comma-separated "modes" to disable collapse/expand animation (for both Group and Panel animations) */ @ attr ( ) public noCollapse : string ; /**
     * Define accordion behavior
     * `single` allows only one Panel to be open.
     * `multiple` allows any number of open Panels.
     * */ @ attr ( { defaultValue : 'single' } ) public accordionGroup : string ; /** Action params to pass into panels when executing reset action (happens when mode is changed) */ @ jsonAttr ( { defaultValue : { noCollapse : true } } ) public transformParams : PanelActionParams ; /** Height of previous active panel */ protected _previousHeight : number = 0 ; /** Fallback setTimeout timer */ protected _fallbackTimer : number = 0 ; static get observedAttributes ( ) : string [ ] { return [ 'mode' , 'accordion-group' ] ; } protected connectedCallback ( ) { super . connectedCallback ( ) ; this . bindEvents ( ) ; this . modeRules . addListener ( this . _onModeChange ) ; this . updateMode ( ) ; } protected disconnectedCallback ( ) { super . disconnectedCallback ( ) ; this . modeRules . removeListener ( this . _onModeChange ) ; this . unbindEvents ( ) ; } protected attributeChangedCallback ( attrName , oldVal , newVal ) { if ( ! this . connected || oldVal === newVal ) return ; if ( attrName === 'mode' ) { this . modeRules . removeListener ( this . _onModeChange ) ; memoize . clear ( this , 'modeRules' ) ; this . modeRules . addListener ( this . _onModeChange ) ; this . updateMode ( ) ; } if ( attrName === 'accordion-group' ) { if ( newVal !== 'single' && newVal !== 'multiple' ) { this . accordionGroup = oldVal ; return ; } this . reset ( ) ; } } protected bindEvents ( ) { this . addEventListener ( 'esl:before:show' , this . _onBeforeShow ) ; this . addEventListener ( 'esl:show' , this . _onShow ) ; this . addEventListener ( 'esl:before:hide' , this . _onBeforeHide ) ; this . addEventListener ( 'transitionend' , this . _onTransitionEnd ) ; } protected unbindEvents ( ) { this . removeEventListener ( 'esl:before:show' , this . _onBeforeShow ) ; this . removeEventListener ( 'esl:show' , this . _onShow ) ; this . removeEventListener ( 'esl:before:hide' , this . _onBeforeHide ) ; this . removeEventListener ( 'transitionend' , this . _onTransitionEnd ) ; } /** Updates element state according to current mode */ protected updateMode ( ) { const prevMode = this . getAttribute ( 'current-mode' ) ; const currentMode = this . currentMode ; this . setAttribute ( 'current-mode' , currentMode ) ; this . setAttribute ( 'view' , currentMode ) ; this . updateModeCls ( ) ; this . reset ( ) ; if ( prevMode !== currentMode ) { this . $$fire ( 'change:mode' , { detail : { prevMode , currentMode } } ) ; } } /** Updates mode class marker */ protected updateModeCls ( ) { const { modeCls , currentMode } = this ; if ( ! modeCls ) return ; const $target = TraversingQuery . first ( this . modeClsTarget , this ) ; if ( ! $target ) return ; ESLPanelGroup . supportedModes . forEach ( ( mode ) => { const className = format ( modeCls , { mode } ) ; $target . classList . toggle ( className , currentMode === mode ) ; } ) ; } returns @ memoize ( ) public get modeRules ( ) : ESLMediaRuleList < string > { return ESLMediaRuleList . parse ( this . mode ) ; } returns public get currentMode ( ) : string { return this . modeRules . activeValue || '' ; } returns public get $panels ( ) : ESLPanel [ ] { const els = Array . from ( this . querySelectorAll ( ESLPanel . is ) ) ; return els . filter ( ( el ) => this . includesPanel ( el ) ) as ESLPanel [ ] ; } returns public get $activePanels ( ) : ESLPanel [ ] { return this . $panels . filter ( ( el ) => el . open ) ; } returns public get shouldCollapse ( ) : boolean { const noCollapseModes = this . noCollapse . split ( ',' ) . map ( ( mode ) => mode . trim ( ) ) ; return ! noCollapseModes . includes ( 'all' ) && ! noCollapseModes . includes ( this . currentMode ) ; } returns action params config that's used (inherited) by controlled  ESLPanel s  public get panelConfig ( ) : PanelActionParams { return { noCollapse : ! this . shouldCollapse || ( this . currentMode === 'tabs' ) } ; } returns protected mergeActionParams ( ... params ) { return Object . assign ( { initiator : 'group' , activator : this } , ... params ) ; } /** Condition-guard to check if the passed target is a Panel that should be controlled by the Group */ public includesPanel ( target ) { if ( ! ( target instanceof ESLPanel ) ) return false ; return target . $group === this ; } /** Shows all panels besides excluded ones */ public showAll ( excluded = [ ] , params = { } ) { this . $panels . forEach ( ( el ) => ! excluded . includes ( el ) && el . show ( this . mergeActionParams ( params ) ) ) ; } /** Hides all active panels besides excluded ones */ public hideAll ( excluded = [ ] , params = { } ) { this . $activePanels . forEach ( ( el ) => ! excluded . includes ( el ) && el . hide ( this . mergeActionParams ( params ) ) ) ; } /** Toggles all panels by predicate */ public toggleAllBy ( shouldOpen , params = { } ) { this . $panels . forEach ( ( panel ) => panel . toggle ( shouldOpen ( panel ) , this . mergeActionParams ( params ) ) ) ; } /** Resets to default state applicable to the current mode */ public reset ( ) { ESLPanel . registered . then ( ( ) => { if ( this . currentMode === 'open' ) this . toggleAllBy ( ( ) => true , this . transformParams ) ; if ( this . currentMode === 'tabs' || ( this . currentMode === 'accordion' && this . accordionGroup === 'single' ) ) { const $activePanel = this . $panels . find ( ( panel ) => panel . initiallyOpened ) ; this . toggleAllBy ( ( panel ) => panel === $activePanel , this . transformParams ) ; } if ( this . currentMode === 'accordion' && this . accordionGroup === 'multiple' ) { this . toggleAllBy ( ( panel ) => panel . initiallyOpened , this . transformParams ) ; } } ) ; } /** Animates the height of the component */ protected onAnimate ( from , to ) { const hasCurrent = this . style . height && this . style . height !== 'auto' ; if ( hasCurrent ) { this . style . height = `${ to }px` ; this . fallbackAnimate ( ) ; } else { this . style . height = `${ from }px` ; afterNextRender ( ( ) => { this . style . height = `${ to }px` ; this . fallbackAnimate ( ) ; } ) ; } } /** Pre-processing animation action */ protected beforeAnimate ( ) { CSSClassUtils . add ( this , this . animationClass ) ; } /** Post-processing animation action */ protected afterAnimate ( ) { this . style . removeProperty ( 'height' ) ; CSSClassUtils . remove ( this , this . animationClass ) ; } /** Inits a fallback timer to call post-animate action */ protected fallbackAnimate ( ) { const time = + this . fallbackDuration ; if ( isNaN ( time ) || time < 0 ) return ; if ( this . _fallbackTimer ) clearTimeout ( this . _fallbackTimer ) ; this . _fallbackTimer = window . setTimeout ( ( ) => this . afterAnimate ( ) , time ) ; } /** Process  ESLPanel pre-show event  @ bind protected _onBeforeShow ( e ) { const panel = e . target ; if ( ! this . includesPanel ( panel ) ) return ; if ( this . currentMode === 'accordion' && this . accordionGroup === 'multiple' ) return ; this . hideAll ( [ panel ] ) ; } /** Process  ESLPanel show event  @ bind protected _onShow ( e ) { const panel = e . target ; if ( ! this . includesPanel ( panel ) ) return ; if ( this . currentMode !== 'tabs' ) return ; this . beforeAnimate ( ) ; if ( this . shouldCollapse ) { this . onAnimate ( this . _previousHeight , panel . initialHeight ) ; } else { afterNextRender ( ( ) => this . afterAnimate ( ) ) ; } } /** Process  ESLPanel pre-hide event  @ bind protected _onBeforeHide ( e ) { if ( this . currentMode === 'open' ) { e . preventDefault ( ) ; return ; } const panel = e . target ; if ( ! this . includesPanel ( panel ) ) return ; this . _previousHeight = this . offsetHeight ; } /** Catches CSS transition end event to start post-animate processing */ @ bind protected _onTransitionEnd ( e ? ) { if ( ! e || e . propertyName === 'height' ) { this . afterAnimate ( ) ; } } /** Handles mode change */ @ bind protected _onModeChange ( ) { this . updateMode ( ) ; } } declare global { export interface ESLLibrary { PanelGroup : typeof ESLPanelGroup ; } export interface HTMLElementTagNameMap { 'esl-panel-group' : ESLPanelGroup ; } }