vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
188 lines (141 loc) • 4.29 kB
text/typescript
/* 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<T extends TResponsiveSource> {
/** 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<T>;
/** Current props */
private _props: TResponsiveProps<T>;
/** Current props */
get props() {
return this._props;
}
constructor(
private _source: T,
private _rules: TResponsiveRule<T>[],
private _onChange?: (props: TResponsiveProps<T>) => 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.
*/
public destroy() {
this._isDestroyed = true;
this._destructors.forEach((destructor) => destructor());
}
}