import {classMap} from "lit/directives/class-map.js"; import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit'; import {HasSlotController} from "../../internal/slot"; import {property, state} from 'lit/decorators.js'; import {Store} from "../../internal/storage"; import ZincElement from '../../internal/zinc-element'; import type {ZnInputEvent} from "../../events/zn-input"; import type ZnToggle from "../toggle"; import styles from './collapsible.scss'; /** * @summary Toggles between showing and hiding content when clicked * @documentation https://zinc.style/components/collapsible * @status experimental * @since 1.0 * * @dependency zn-icon - The icon element * * @slot header - Clicking will toggle the show state of the data * */ export default class ZnCollapsible extends ZincElement { static styles: CSSResultGroup = unsafeCSS(styles); @property({reflect: true}) caption = ''; @property({reflect: true}) description: string; @property({reflect: true}) label = ''; @property({type: Boolean, attribute: 'show-number', reflect: true}) showNumber: boolean = false; // what element name to count @property({type: String, attribute: 'count-element'}) countElement: string = '*'; @property({type: Boolean, reflect: true}) expanded: boolean = false; @property({attribute: 'default'}) defaultState: 'open' | 'closed'; @property({attribute: 'local-storage', type: Boolean, reflect: true}) localStorage: boolean; @property({attribute: 'store-key', reflect: true}) storeKey: string = ""; @property({attribute: 'store-ttl', type: Number, reflect: true}) storeTtl = 0; @property({attribute: 'flush', type: Boolean, reflect: true}) flush: boolean = false; @state() numberOfItems: number = 0; protected _store: Store; private readonly hasSlotController = new HasSlotController(this, '[default]', 'header', 'caption', 'label'); private observer: MutationObserver; private showArrow: boolean = true; async connectedCallback() { super.connectedCallback(); this._store = new Store(this.localStorage ? window.localStorage : window.sessionStorage, "zncla:", this.storeTtl); this.expanded = this.defaultState === 'open'; if (this.storeKey) { const hasPref = this._store.get(this.storeKey); if (hasPref) { this.expanded = hasPref === "true"; } } if (this.showNumber) { // add a mutation observer to the default slot to recalculate the number of items when it changes const slot = this as HTMLElement; this.observer = new MutationObserver(() => { this.recalculateNumberOfItems(); }); this.observer.observe(slot, {childList: true, subtree: true}); } await this.updateComplete; const captionSlot = this.hasSlotController.getSlot('caption') as HTMLDivElement; if (captionSlot) { const toggles = captionSlot.querySelectorAll('zn-toggle'); // remove the drop arrow this.showArrow = false; toggles.forEach(toggle => { toggle.removeEventListener('zn-input', this.handleCaptionToggle); toggle.addEventListener('zn-input', this.handleCaptionToggle); }); } } // this is for handling global toggles public handleCaptionToggle = (e: ZnInputEvent) => { const toggle = e.target as ZnToggle; if (toggle) { this.expanded = toggle.checked; } } protected updated(changedProperties: PropertyValues) { super.updated(changedProperties); if (changedProperties.has('expanded') && this.storeKey) { this._store.set(this.storeKey, this.expanded.toString()); } } disconnectedCallback() { super.disconnectedCallback(); if (this.observer) { this.observer.disconnect(); } } handleCollapse = (e: MouseEvent) => { const target = e.target as HTMLElement; if (target.tagName.toLowerCase() === 'input' && (target as HTMLInputElement).type === 'checkbox') { return; } if (this.expanded) { this.expanded = false; e.stopPropagation(); } } public recalculateNumberOfItems = () => { const assignedElements = this.hasSlotController.getDefaultSlot() .filter(node => node.nodeType === Node.ELEMENT_NODE) as HTMLElement[]; // query all direct children that match the countElement tag name if (this.countElement === '*') { this.numberOfItems = assignedElements.length; return; } const children = assignedElements.flatMap(el => Array.from(el.querySelectorAll(this.countElement))); this.numberOfItems = children.length; } render() { this.recalculateNumberOfItems(); return html`

${this.caption}

${this.description}

${this.label}

${this.showNumber && !this.expanded ? html` ${this.numberOfItems}` : ''} ${this.showArrow ? html` ` : ''}
`; } }