UNPKG

@exadel/esl

Version:

Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components

361 lines (360 loc) 15.3 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var ESLPanelGroup_1; import { ExportNs } from '../../esl-utils/environment/export-ns'; import { defined } from '../../esl-utils/misc/object/utils'; import { ESLBaseElement } from '../../esl-base-element/core'; import { afterNextRender } from '../../esl-utils/async/raf'; import { debounce } from '../../esl-utils/async/debounce'; import { decorate, memoize, attr, boolAttr, jsonAttr, prop, listen } from '../../esl-utils/decorators'; import { format } from '../../esl-utils/misc/format'; import { CSSClassUtils } from '../../esl-utils/dom/class'; import { ESLMediaQuery, ESLMediaRuleList } from '../../esl-media-query/core'; import { ESLTraversingQuery } from '../../esl-traversing-query/core'; import { ESLPanel } from '../../esl-panel/core'; /** Converts special 'all' value to positive infinity */ const parseCount = (value) => value === 'all' ? Number.POSITIVE_INFINITY : parseInt(value, 10); /** * ESLPanelGroup component * @author Julia Murashko, Anastasia Lesun, Alexey Stsefanovich (ala'n) * * ESLPanelGroup is a custom element that is used as a container for a group of {@link ESLPanel}s */ let ESLPanelGroup = ESLPanelGroup_1 = class ESLPanelGroup extends ESLBaseElement { constructor() { super(...arguments); /** Height of previous active panel */ this._previousHeight = 0; } connectedCallback() { ESLPanel.registered.then(() => { super.connectedCallback(); this.refresh(); }); } attributeChangedCallback(attrName, oldVal, newVal) { if (!this.connected || oldVal === newVal) return; if (attrName === 'mode' || attrName === 'min-open-items' || attrName === 'max-open-items') { this.$$off(this._onConfigChange); memoize.clear(this, 'modeRules'); memoize.clear(this, 'minValueRules'); memoize.clear(this, 'maxValueRules'); this.$$on(this._onConfigChange); this.refresh(); } if (attrName === 'refresh-strategy') { memoize.clear(this, 'refreshRules'); } } /** Updates element state according to current mode */ refresh() { const prevMode = this.getAttribute('current-mode'); const currentMode = this.currentMode; this.setAttribute('current-mode', currentMode); this.updateModeCls(); this.reset(); this.updateMarkers(); if (prevMode !== currentMode) this.$$fire(this.MODE_CHANGE_EVENT, { detail: { prevMode, currentMode } }); } updateMarkers() { this.$$attr('has-opened', this.$activePanels.length > 0); } /** Updates mode class marker */ updateModeCls() { const { modeCls, currentMode } = this; if (!modeCls) return; const $target = ESLTraversingQuery.first(this.modeClsTarget, this); if (!$target) return; ESLPanelGroup_1.supportedModes.forEach((mode) => { const className = format(modeCls, { mode }); $target.classList.toggle(className, currentMode === mode); }); } /** @returns ESLMediaRuleList instance of the mode mapping */ get modeRules() { return ESLMediaRuleList.parseQuery(this.mode); } /** @returns ESLMediaRuleList instance of the min-open-items mapping */ get minValueRules() { return ESLMediaRuleList.parseQuery(this.minOpenItems, parseCount); } /** @returns ESLMediaRuleList instance of the max-open-items mapping */ get maxValueRules() { return ESLMediaRuleList.parseQuery(this.maxOpenItems, parseCount); } /** @returns ESLMediaRuleList instance of the refresh-strategy mapping */ get refreshRules() { return ESLMediaRuleList.parseQuery(this.refreshStrategy); } /** @returns current mode */ get currentMode() { return this.modeRules.activeValue || ''; } /** @returns current value of min-open-items */ get currentMinItems() { const min = 0; const val = defined(this.minValueRules.activeValue, 1); const max = this.currentMode === 'tabs' ? 1 : this.$panels.length; return Math.min(max, Math.max(min, val)); // minmax } /** @returns current value of max-open-items */ get currentMaxItems() { const min = this.currentMinItems; const val = defined(this.maxValueRules.activeValue, 1); const max = this.currentMode === 'tabs' ? 1 : this.$panels.length; return Math.min(max, Math.max(min, val)); // minmax } /** @returns panels that are processed by the current panel group */ get $panels() { const els = Array.from(this.querySelectorAll(this.panelSel)); return els.filter((el) => this.includesPanel(el)); } /** @returns panels that are active */ get $activePanels() { return this.$panels.filter((el) => el.open); } /** @returns panels that was initially opened */ get $initialPanels() { return this.$panels.filter((el) => el.initiallyOpened); } /** @returns panels that requested to be opened on refresh */ get $resetStatePanels() { switch (this.refreshRules.activeValue) { case 'close': return []; case 'open': return this.$panels; case 'initial': return this.$initialPanels; default: return this.$activePanels; } } /** @returns whether the collapse/expand animation should be handheld by the breakpoints */ get shouldAnimate() { return !ESLMediaQuery.for(this.noAnimate).matches; } /** @returns action params config that's used (inherited) by controlled {@link ESLPanel}s */ get panelConfig() { return { capturedBy: this.currentMode === 'tabs' ? this : undefined, noAnimate: !this.shouldAnimate || (this.currentMode === 'tabs') }; } /** @returns merged panel action params for show/hide requests from the group */ 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 */ includesPanel(target) { if (!(target instanceof ESLPanel)) return false; return target.$group === this; } /** Shows all panels besides excluded ones */ showAll(excluded = [], params = {}) { this.$panels.forEach((el) => !excluded.includes(el) && el.show(this.mergeActionParams(params))); } /** Hides all active panels besides excluded ones */ hideAll(excluded = [], params = {}) { this.$activePanels.forEach((el) => !excluded.includes(el) && el.hide(this.mergeActionParams(params))); } /** Resets to default state applicable to the current panel group configuration */ reset() { // $activePanels - collection of items to open (ideally; without normalization) const $activePanels = this.$resetStatePanels; // $orderedPanels = $activePanels U ($panels / $activePanels) - the list of ordered panels const $orderedPanels = $activePanels.concat(this.$panels.filter((item) => !$activePanels.includes(item))); // we use current open active panels count but normalized in range of minmax const activeCount = Math.min(this.currentMaxItems, Math.max($activePanels.length, this.currentMinItems)); const params = this.mergeActionParams(this.transformParams); $orderedPanels.forEach((panel, index) => panel.toggle(index < activeCount, params)); } /** Animates the height of the component */ onAnimate(from, to) { // overrides initial value if animation is currently in progress if (from < 0 || this.style.height && this.style.height !== 'auto') { from = this.clientHeight; } // sets the initial height this.style.height = `${from}px`; // makes sure browser realized the height change afterNextRender(() => { this.style.height = `${to}px`; this.fallbackAnimate(); }); } /** Checks if transition happens and runs afterAnimate step if transition is not presented */ fallbackAnimate() { afterNextRender(() => { const distance = parseFloat(this.style.height) - this.clientHeight; if (Math.abs(distance) <= 1) this.afterAnimate(); }); } /** Pre-processing animation action */ beforeAnimate() { this.$$attr('animating', true); CSSClassUtils.add(this, this.animationClass); } /** Post-processing animation action */ afterAnimate(silent) { this.style.removeProperty('height'); CSSClassUtils.remove(this, this.animationClass); this.$$attr('animating', false); if (silent) return; this.$$fire(this.AFTER_ANIMATE_EVENT, { bubbles: false }); } /** Process {@link ESLPanel} pre-show event */ _onBeforeShow(e) { var _a, _b, _c; const panel = e.target; if (!this.includesPanel(panel)) return; const max = this.currentMaxItems; const params = this.mergeActionParams({ event: e }); const balanceMarker = ((_c = (_b = (_a = e.detail) === null || _a === void 0 ? void 0 : _a.params) === null || _b === void 0 ? void 0 : _b.event) === null || _c === void 0 ? void 0 : _c.type) !== 'esl:before:hide'; // all currently active panels, except the one that requested to be open const $activePanels = this.$activePanels.filter((el) => el !== panel); // overflow = pretended to be active (current active + balanceMarker (1 if nothing hides)) - limit const overflow = Math.max(0, $activePanels.length + Number(balanceMarker) - max); // close all extra active panels (not includes requested one) $activePanels.slice(0, overflow).forEach((el) => el.hide(params)); if (max <= 0) return e.preventDefault(); this._previousHeight = this.clientHeight; } /** Process {@link ESLPanel} show event */ _onStateChanged(e) { const panel = e.target; if (!this.includesPanel(panel)) return; this.updateMarkers(); if (this.currentMode !== 'tabs') return; const targetHeight = panel.open ? panel.initialHeight : 0; this.beforeAnimate(); if (this.shouldAnimate) { this.onAnimate(this._previousHeight, targetHeight); } else { afterNextRender(() => this.afterAnimate(true)); } } /** Process {@link ESLPanel} pre-hide event */ _onBeforeHide(e) { var _a, _b; const { target: panel, detail } = e; if (!this.includesPanel(panel)) return; const min = this.currentMinItems; // checks if the hide event was produced by the show event const balanceMarker = ((_b = (_a = detail === null || detail === void 0 ? void 0 : detail.params) === null || _a === void 0 ? void 0 : _a.event) === null || _b === void 0 ? void 0 : _b.type) === 'esl:before:show'; // activePanels = currentActivePanels - 1 (hide) + 1 if the event produced by 'before:show' const activeNumber = this.$activePanels.length - 1 + Number(balanceMarker); if (activeNumber < min) { const $firstInactive = this.$panels.find(($panel) => !$panel.open); if (min === 1 || !$firstInactive) return e.preventDefault(); const params = this.mergeActionParams({ event: e }); $firstInactive.show(params); } this._previousHeight = this.clientHeight; } /** Catches CSS transition end event to start post-animate processing */ _onTransitionEnd(e) { if (!e || (e.propertyName === 'height' && e.target === this)) { this.afterAnimate(); } } /** Handles configuration change */ _onConfigChange() { this.refresh(); } }; ESLPanelGroup.is = 'esl-panel-group'; ESLPanelGroup.observedAttributes = ['mode', 'refresh-strategy', 'min-open-items', 'max-open-items']; /** List of supported modes */ ESLPanelGroup.supportedModes = ['accordion', 'tabs']; __decorate([ prop('esl:change:mode') ], ESLPanelGroup.prototype, "MODE_CHANGE_EVENT", void 0); __decorate([ prop('esl:after:animate') ], ESLPanelGroup.prototype, "AFTER_ANIMATE_EVENT", void 0); __decorate([ attr({ defaultValue: ESLPanel.is }) ], ESLPanelGroup.prototype, "panelSel", void 0); __decorate([ attr({ defaultValue: 'accordion' }) ], ESLPanelGroup.prototype, "mode", void 0); __decorate([ attr({ defaultValue: 'esl-{mode}-view' }) ], ESLPanelGroup.prototype, "modeCls", void 0); __decorate([ attr({ defaultValue: '' }) ], ESLPanelGroup.prototype, "modeClsTarget", void 0); __decorate([ attr({ defaultValue: 'not all' }) ], ESLPanelGroup.prototype, "noAnimate", void 0); __decorate([ attr({ defaultValue: 'animate' }) ], ESLPanelGroup.prototype, "animationClass", void 0); __decorate([ attr({ defaultValue: '1' }) ], ESLPanelGroup.prototype, "minOpenItems", void 0); __decorate([ attr({ defaultValue: '1' }) ], ESLPanelGroup.prototype, "maxOpenItems", void 0); __decorate([ attr({ defaultValue: 'last' }) ], ESLPanelGroup.prototype, "refreshStrategy", void 0); __decorate([ jsonAttr({ defaultValue: { noAnimate: true } }) ], ESLPanelGroup.prototype, "transformParams", void 0); __decorate([ boolAttr({ readonly: true }) ], ESLPanelGroup.prototype, "hasOpened", void 0); __decorate([ boolAttr({ readonly: true }) ], ESLPanelGroup.prototype, "animating", void 0); __decorate([ memoize() ], ESLPanelGroup.prototype, "modeRules", null); __decorate([ memoize() ], ESLPanelGroup.prototype, "minValueRules", null); __decorate([ memoize() ], ESLPanelGroup.prototype, "maxValueRules", null); __decorate([ memoize() ], ESLPanelGroup.prototype, "refreshRules", null); __decorate([ listen('esl:before:show') ], ESLPanelGroup.prototype, "_onBeforeShow", null); __decorate([ listen('esl:show esl:hide') ], ESLPanelGroup.prototype, "_onStateChanged", null); __decorate([ listen('esl:before:hide') ], ESLPanelGroup.prototype, "_onBeforeHide", null); __decorate([ listen('transitionend') ], ESLPanelGroup.prototype, "_onTransitionEnd", null); __decorate([ listen({ event: 'change', target: (group) => [group.modeRules, group.minValueRules, group.maxValueRules] }), decorate(debounce, 0) ], ESLPanelGroup.prototype, "_onConfigChange", null); ESLPanelGroup = ESLPanelGroup_1 = __decorate([ ExportNs('PanelGroup') ], ESLPanelGroup); export { ESLPanelGroup };