import { getProp } from './get-prop'; import { pxToNumber } from './px-to-number'; import { EventStackItem, EventEmitter } from './event-emitter'; import Scrollbar from 'smooth-scrollbar'; import OverscrollPlugin from 'smooth-scrollbar/plugins/overscroll'; import { awaitFrame } from './await-frame'; import { RtSlidePanelProps } from '../../rt-slide-panel.types'; // eslint-disable-next-line Scrollbar.use(OverscrollPlugin); type RvTabsEventStack = { activeIdChange: Array>; }; export interface RvTabsProps extends Pick { activeId?: string | null; updateBadgeWidth?: (width: number) => void; updateBadgeTransform?: (x: number) => void; /** * Сбрасывать ли `activeId` при нажатии на уже активный элемент */ isResettable?: boolean; contentClass: string; slidePanelClass: string; elementClass: string; } // TODO: add Observer on items change export class RvTabs extends EventEmitter { element: HTMLElement; content: HTMLElement; buttons: HTMLElement[] = []; badge: HTMLElement; direction: RtSlidePanelProps['direction']; interactionOnHover: RtSlidePanelProps['interactionOnHover']; elementClass: string; isResettable: boolean; updateBadgeWidth: (width: number) => void; updateBadgeTransform: (x: number) => void; scrollbar: Scrollbar; private _sizeObserver?: ResizeObserver; private _mutationObserver?: MutationObserver; private _activeId: string | null = null; get activeIdx() { if (this.activeId) return this.buttons.findIndex( (b) => b.getAttribute('data-reevo-id') === this._activeId ); return -1; } get activeButton() { if (this.activeId) return this.buttons.find( (b) => b.getAttribute('data-reevo-id') === this._activeId ); return null; } set activeId(v: string | null) { this._activeId = v; awaitFrame(() => { this.computeBadgeTransform(); this._emitEvent('activeIdChange', this._activeId); }); } get activeId(): string | null { return this._activeId; } private getButtons() { if (this.element) { this.buttons = Array.from( this.element.querySelectorAll(`.${this.elementClass}`) ) as HTMLElement[]; } } constructor(element: HTMLElement, props: RvTabsProps) { super({ activeIdChange: [], }); this.element = element; this.scrollbar = Scrollbar.init(element, { continuousScrolling: true, plugins: { overscroll: { effect: 'bounce', }, }, }); // Удаление трекера скролла, // TODO: булевый проп нужен this.element.querySelector('.scrollbar-track')?.remove(); this.elementClass = props.elementClass; this.badge = this.element.querySelector(`.${props.slidePanelClass}`)!; this.content = this.element.querySelector(`.${props.contentClass}`)!; this.direction = getProp(props, 'direction', 'horizontal'); this.isResettable = getProp(props, 'isResettable', false); this.interactionOnHover = getProp( props, 'interactionOnHover', false ); this.getButtons(); this.updateBadgeWidth = getProp( props, 'updateBadgeWidth', (width: number) => { if (this.direction === 'vertical') { this.badge.style.height = `${width}px`; } else { this.badge.style.width = `${width}px`; } } ); this.updateBadgeTransform = getProp( props, 'updateBadgeTransform', (position: number) => { const transform = this.direction === 'vertical' ? `translateY(${position}px)` : `translateX(${position}px)`; this.badge.style.transform = transform; } ); this.addListeners(); this._activeId = props.activeId || ''; this.computeBadgeTransform(); } private addListeners() { if (this._sizeObserver) this._sizeObserver.disconnect(); this._sizeObserver = new ResizeObserver(this.computeBadgeTransform); this._sizeObserver.observe(this.element); if (this._mutationObserver) this._mutationObserver.disconnect(); this._mutationObserver = new MutationObserver(this.computeBadgeTransform); this._mutationObserver.observe(this.content, { childList: true, subtree: false, characterDataOldValue: false, }); this.buttons.forEach((button) => { const id = button.getAttribute('data-reevo-id'); if (!id) console.warn('RvTabs: element must have a "data-reevo-id" attribute'); button.addEventListener( this.interactionOnHover ? 'mouseenter' : 'click', this.onInteraction ); }); } private onInteraction = function (this: RvTabs, e: MouseEvent) { const button = e.currentTarget as HTMLButtonElement; const clickId = button.getAttribute('data-reevo-id') || null; this.activeId = this.isResettable && this.activeId === clickId ? null : clickId; // const isVertical = this.direction === 'vertical'; // const contentSize = isVertical // ? this.content.clientHeight // : this.content.clientWidth; // const btnSize = isVertical ? button.clientHeight : button.clientWidth; // const offset = contentSize / 2 - btnSize / 2; // TODO: center mode // this.scrollbar.scrollIntoView(button, { // [isVertical ? 'offsetTop' : 'offsetLeft']: offset, // }); }.bind(this); destroy() { if (this._sizeObserver) this._sizeObserver.disconnect(); if (this._mutationObserver) this._mutationObserver.disconnect(); this.buttons.forEach((btn) => { const id = btn.getAttribute('data-reevo-id'); if (!id) console.warn('RvTabs: element must have a "data-reevo-id" attribute'); btn.removeEventListener( this.interactionOnHover ? 'mouseenter' : 'click', this.onInteraction ); }); } getBadgeWidthByIdx(idx: number) { const activeBtn = this.buttons[idx]; if (!activeBtn) return 0; return this.direction === 'vertical' ? activeBtn.offsetHeight : activeBtn.offsetWidth; } computeCurrentBadgeWidth = function (this: RvTabs) { this.getButtons(); this.updateBadgeWidth(this.getBadgeWidthByIdx(this.activeIdx)); }.bind(this); computeBadgeTransform = function (this: RvTabs) { this.getButtons(); const isVertical = this.direction === 'vertical'; const contentStyle = window.getComputedStyle(this.content); const gap = pxToNumber(contentStyle.gap); const paddingStart = pxToNumber( isVertical ? contentStyle.paddingTop : contentStyle.paddingLeft ); let position = paddingStart + gap * this.activeIdx; if (this.activeIdx > 0) { for (let i = 0; i < this.activeIdx; i++) { const btn = this.buttons[i]; const style = window.getComputedStyle(btn); const btnSize = isVertical ? btn.offsetHeight : btn.offsetWidth; const marginStart = pxToNumber( isVertical ? style.marginTop : style.marginLeft ); const marginEnd = pxToNumber( isVertical ? style.marginBottom : style.marginRight ); position += btnSize + marginStart + marginEnd + gap; } } if (this.activeButton) { const style = window.getComputedStyle(this.activeButton); position += pxToNumber(isVertical ? style.marginTop : style.marginLeft); } this.updateBadgeTransform(position); this.updateBadgeWidth(this.getBadgeWidthByIdx(this.activeIdx)); }.bind(this); }