UNPKG

@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
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 };