/* eslint-disable no-underscore-dangle */ import { initVevet } from '@/global/initVevet'; import { noopIfDestroyed } from '@/internal/noopIfDestroyed'; import { Module } from '../Module'; import { TResponsiveProps, TResponsiveRule, TResponsiveSource } from './types'; export * from './types'; export class Responsive { /** Tracks whether the instance has been destroyed */ private _isDestroyed = false; /** Destroyable actions */ private _destructors: (() => void)[] = []; /** Previously active breakpoints */ private _prevBreakpoints = '[]'; /** Initial props */ private _initProps!: TResponsiveProps; /** Current props */ private _props: TResponsiveProps; /** Current props */ get props() { return this._props; } constructor( private _source: T, private _rules: TResponsiveRule[], private _onChange?: (props: TResponsiveProps) => void, ) { const source = _source; const app = initVevet(); const sourceName = source instanceof Module ? source.name : 'Object'; // Fetch initial props this._fetchInitProps(); // Save current props this._props = { ...this._initProps }; // Override Module's `updateProps` if (source instanceof Module) { source.on('destroy', () => this.destroy(), { name: this.constructor.name, protected: true, }); const saveUpdateProps = source.updateProps.bind(source); source.updateProps = (p) => { saveUpdateProps(p); this._initProps = { ...this._initProps, ...p }; }; Object.defineProperty(source, '_$_responseProps', { value: (p: any) => { saveUpdateProps(p); }, }); } // Update Props this._handleUpdate(); // Add viewport listener this._destructors.push( app.onResize('any', () => this._handleUpdate(), { name: `${this.constructor.name} / ${sourceName}`, }), ); } /** Set initial props */ private _fetchInitProps() { const source = this._source; if (source instanceof Module) { this._initProps = {} as any; const mutableKeys = Object.keys(source._getMutable()); mutableKeys.forEach((key) => { // @ts-ignore this._initProps[key] = source.props[key]; }); return; } this._initProps = this._source as any; } /** Get active rules */ private _getActiveRules() { const app = initVevet(); const rules = this._rules.filter(({ at }) => { if (at === 'tablet' && app.tablet) { return true; } if (at === 'phone' && app.phone) { return true; } if (at === 'mobile' && app.mobile) { return true; } if (at === 'non_mobile' && !app.mobile) { return true; } if (at === 'portrait' && app.portrait) { return true; } if (at === 'landscape' && app.landscape) { return true; } if (at.startsWith('@media')) { const isMediaActive = window.matchMedia( at.replace('@media', ''), ).matches; return isMediaActive; } return false; }); return rules; } /** Get responsive props */ private _getResponsiveProps() { const rules = this._getActiveRules(); let newProps = {}; rules.forEach(({ props }) => { newProps = { ...newProps, ...props }; }); return newProps; } /** Update properties */ private _handleUpdate() { const activeRules = this._getActiveRules(); const activeBreakpoints = activeRules.map(({ at }) => at); const json = JSON.stringify(activeBreakpoints); if (this._prevBreakpoints === json) { return; } this._prevBreakpoints = json; this._props = { ...this._initProps, ...this._getResponsiveProps() }; if (this._source instanceof Module) { // @ts-ignore this._source._$_responseProps(this._props); } this._onChange?.(this.props); } /** * Destroy the instance and clean up resources. * * The instance is destroyed automatically when it is used to mutate Module's props. */ @noopIfDestroyed public destroy() { this._isDestroyed = true; this._destructors.forEach((destructor) => destructor()); } }