@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
360 lines (359 loc) • 15.2 kB
JavaScript
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 { 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 = this.$$find(this.modeClsTarget);
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 };