@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
227 lines (226 loc) • 8.6 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 ESLTabs_1;
import { ExportNs } from '../../esl-utils/environment/export-ns';
import { ESLBaseElement } from '../../esl-base-element/core';
import { rafDecorator } from '../../esl-utils/async/raf';
import { memoize, attr, listen, decorate, ready } from '../../esl-utils/decorators';
import { isRTL } from '../../esl-utils/dom/rtl';
import { debounce } from '../../esl-utils/async/debounce';
import { ESLResizeObserverTarget } from '../../esl-event-listener/core';
import { ESLMediaRuleList } from '../../esl-media-query/core/esl-media-rule-list';
import { ESLTab } from './esl-tab';
/**
* ESlTabs component
* @author Julia Murashko
*
* Tabs container component for Tabs trigger group.
* Uses {@link ESLTab} as an item.
* Each individual {@link ESLTab} can control {@link ESLToggleable} or, usually, {@link ESLPanel}
*/
let ESLTabs = ESLTabs_1 = class ESLTabs extends ESLBaseElement {
constructor() {
super(...arguments);
this._deferredUpdateArrows = debounce(this.updateArrows, 100, this);
this._deferredFitToViewport = debounce(this.fitToViewport, 100, this);
}
/** ESLMediaRuleList instance of the scrollable type mapping */
get scrollableTypeRules() {
return ESLMediaRuleList.parseQuery(this.scrollable);
}
/** @returns current scrollable type */
get currentScrollableType() {
return this.scrollableTypeRules.activeValue || 'side';
}
connectedCallback() {
super.connectedCallback();
this.updateScrollableType();
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (!this.connected || oldVal === newVal)
return;
if (attrName === 'scrollable') {
memoize.clear(this, 'scrollableTypeRules');
this.$$on(this._onScrollableTypeChange);
this.updateScrollableType();
}
}
bindScrollableEvents() {
this.$$on(this._onScroll);
this.$$on(this._onResize);
}
unbindScrollableEvents() {
this.$$off(this._onScroll);
this.$$off(this._onResize);
}
/** Collection of inner {@link ESLTab} items */
get $tabs() {
const els = this.querySelectorAll(ESLTab.is);
return els ? Array.from(els) : [];
}
/** Active {@link ESLTab} item */
get $current() {
return this.$tabs.find((el) => el.active) || null;
}
/** Container element to scroll */
get $scrollableTarget() {
return this.querySelector(this.scrollableTarget);
}
/** Is the scrollable mode enabled ? */
get isScrollable() {
return this.currentScrollableType !== 'disabled';
}
/** Move scroll to the next/previous item */
moveTo(direction, behavior = 'smooth') {
const $scrollableTarget = this.$scrollableTarget;
if (!$scrollableTarget)
return;
const { offsetWidth, scrollWidth, scrollLeft } = $scrollableTarget;
const max = scrollWidth - offsetWidth;
const invert = direction === 'left' || isRTL(this);
const offset = invert ? -offsetWidth : offsetWidth;
const left = Math.max(0, Math.min(max, scrollLeft + offset));
$scrollableTarget.scrollTo({ left, behavior });
}
/** Scroll tab to the view */
fitToViewport($trigger, behavior = 'smooth') {
this.updateMarkers();
const $scrollableTarget = this.$scrollableTarget;
if (!$scrollableTarget || !$trigger)
return;
const areaRect = $scrollableTarget.getBoundingClientRect();
const itemRect = $trigger.getBoundingClientRect();
$scrollableTarget.scrollBy({
left: this.calcScrollOffset(itemRect, areaRect),
behavior
});
this.updateArrows();
}
/** Get scroll offset position from the selected item rectangle */
calcScrollOffset(itemRect, areaRect) {
if (this.currentScrollableType === 'center') {
return itemRect.left + itemRect.width / 2 - (areaRect.left + areaRect.width / 2);
}
// item is out of area from the right side
// else item out is of area from the left side
if (itemRect.right > areaRect.right) {
return Math.ceil(itemRect.right - areaRect.right);
}
else if (itemRect.left < areaRect.left) {
return Math.floor(itemRect.left - areaRect.left);
}
}
updateArrows() {
const $scrollableTarget = this.$scrollableTarget;
if (!$scrollableTarget)
return;
const scrollStart = Math.abs($scrollableTarget.scrollLeft) > 1;
const scrollEnd = Math.abs($scrollableTarget.scrollLeft) + $scrollableTarget.clientWidth + 1 < $scrollableTarget.scrollWidth;
const $rightArrow = this.querySelector('[data-tab-direction="right"]');
const $leftArrow = this.querySelector('[data-tab-direction="left"]');
$leftArrow && $leftArrow.toggleAttribute('disabled', !scrollStart);
$rightArrow && $rightArrow.toggleAttribute('disabled', !scrollEnd);
}
updateMarkers() {
const $scrollableTarget = this.$scrollableTarget;
if (!$scrollableTarget)
return;
const hasScroll = this.isScrollable && ($scrollableTarget.scrollWidth > this.clientWidth);
this.toggleAttribute('has-scroll', hasScroll);
}
/** Update element state according to scrollable type */
updateScrollableType() {
ESLTabs_1.supportedScrollableTypes.forEach((type) => {
this.$$cls(`scrollable-${type}`, this.currentScrollableType === type);
});
this._deferredFitToViewport(this.$current);
if (this.currentScrollableType === 'disabled') {
this.unbindScrollableEvents();
}
else {
this.bindScrollableEvents();
}
}
_onTriggerStateChange({ detail }) {
if (!detail.active)
return;
this._deferredFitToViewport(this.$current);
}
_onClick(event) {
const eventTarget = event.target;
const target = eventTarget.closest('[data-tab-direction]');
const direction = target && target.dataset.tabDirection;
if (!direction)
return;
this.moveTo(direction);
}
_onFocus(e) {
const target = e.target;
if (target instanceof ESLTab)
this._deferredFitToViewport(target);
}
_onScroll() {
this._deferredUpdateArrows();
}
_onResize() {
this._deferredFitToViewport(this.$current, 'auto');
}
/** Handles scrollable type change */
_onScrollableTypeChange() {
this.updateScrollableType();
}
};
ESLTabs.is = 'esl-tabs';
ESLTabs.observedAttributes = ['scrollable'];
/** List of supported scrollable types */
ESLTabs.supportedScrollableTypes = ['disabled', 'side', 'center'];
__decorate([
attr({ defaultValue: 'disabled' })
], ESLTabs.prototype, "scrollable", void 0);
__decorate([
attr({ defaultValue: '.esl-tab-container' })
], ESLTabs.prototype, "scrollableTarget", void 0);
__decorate([
memoize()
], ESLTabs.prototype, "scrollableTypeRules", null);
__decorate([
ready
], ESLTabs.prototype, "connectedCallback", null);
__decorate([
listen('esl:change:active')
], ESLTabs.prototype, "_onTriggerStateChange", null);
__decorate([
listen('click')
], ESLTabs.prototype, "_onClick", null);
__decorate([
listen('focusin')
], ESLTabs.prototype, "_onFocus", null);
__decorate([
listen({
auto: false,
event: 'scroll',
target: (el) => el.$scrollableTarget
})
], ESLTabs.prototype, "_onScroll", null);
__decorate([
listen({
auto: false,
event: 'resize',
target: ESLResizeObserverTarget.for
}),
decorate(rafDecorator)
], ESLTabs.prototype, "_onResize", null);
__decorate([
listen({
event: 'change',
target: (el) => el.scrollableTypeRules
})
], ESLTabs.prototype, "_onScrollableTypeChange", null);
ESLTabs = ESLTabs_1 = __decorate([
ExportNs('Tabs')
], ESLTabs);
export { ESLTabs };