UNPKG

@mcmhomes/panorama-viewer

Version:
342 lines (298 loc) 9.08 kB
/*eslint-disable react-compiler/react-compiler*/ import clone from 'clone-deep'; import equals from 'fast-deep-equal'; import equalsReact from 'fast-deep-equal/react'; import {each, FLOAT_LAX, FLOAT_LAX_ANY, IS_ARRAY, IS_OBJECT, ISSET, purgeErrorMessage, setAnimationFrameIntervalRemovable, setAnimationFrameTimeoutRemovable, setIntervalRemovable, setTimeoutRemovable} from './PanoramaUtils.jsx'; import {useRef as reactUseRef, memo as reactMemo, useMemo as reactUseMemo, useCallback as reactUseCallback, useState as reactUseState, useEffect as reactUseEffect} from 'react'; /** * @param {*} [initialValue] * @returns {React.MutableRefObject<*>} */ export const useRef = (initialValue) => reactUseRef(initialValue); export const useState = (initialValue) => reactUseState(initialValue); const fixEqualsComparator = (equalsComparator, errorMessage) => { if(ISSET(equalsComparator)) { if(typeof equalsComparator !== 'function') { console.error(errorMessage); console.error(equalsComparator); return equalsReact; } return equalsComparator; } return equalsReact; }; const useCompareMemoize = (value, equalsComparator) => { const ref = useRef(); if(!equalsComparator(value, ref.current)) { ref.current = value; } return ref.current; }; /** * @param {import('react').FunctionComponent<*>} component * @param {(prevProps:Readonly<*>, nextProps:Readonly<*>) => boolean} [equalsComparator] * @returns {import('react').NamedExoticComponent<*>} */ export const memo = (component, equalsComparator) => { equalsComparator = fixEqualsComparator(equalsComparator, 'memo() was given an invalid comparator:'); return reactMemo(component, equalsComparator); }; export const useMemo = (callable, comparingValues, equalsComparator) => { equalsComparator = fixEqualsComparator(equalsComparator, 'useMemo() was given an invalid comparator:'); return reactUseMemo(callable, useCompareMemoize(comparingValues, equalsComparator)); }; export const useCallback = (callable, comparingValues, equalsComparator) => { equalsComparator = fixEqualsComparator(equalsComparator, 'useCallback() was given an invalid comparator:'); return reactUseCallback(callable, useCompareMemoize(comparingValues, equalsComparator)); }; export const useEffect = (callable, comparingValues, equalsComparator) => { equalsComparator = fixEqualsComparator(equalsComparator, 'useEffect() was given an invalid comparator:'); return reactUseEffect(callable, useCompareMemoize(comparingValues, equalsComparator)); }; export const useEffectInterval = (callable, comparingValues, intervalMs, fireImmediately, equalsComparator) => { return useEffect(() => { return setIntervalRemovable(callable, intervalMs, fireImmediately).remove; // updates are controlled by the given comparingValues //eslint-disable-next-line react-hooks/exhaustive-deps }, [comparingValues, equalsComparator/*, callable, intervalMs, fireImmediately*/]); }; export const useEffectAnimationFrameInterval = (callable, comparingValues, intervalFrames, fireImmediately, equalsComparator) => { return useEffect(() => { return setAnimationFrameIntervalRemovable(callable, intervalFrames, fireImmediately).remove; // updates are controlled by the given comparingValues //eslint-disable-next-line react-hooks/exhaustive-deps }, [comparingValues, equalsComparator/*, callable, intervalFrames, fireImmediately*/]); }; /** * Allows you to easily create a fadeout animation. * * @param {Object} params * @param {boolean|null} [params.visible] * @param {number|null} [params.delay] * @param {number|null} [params.duration] * @param {number|null} [params.decayFactor] * @returns {{opacity:number}} */ export const useFadeoutAnimation = (params) => { const {visible, delay, duration, decayFactor} = params; const opacityRef = useRef(1); const [opacity, setOpacity] = useState(1); const delayTimePassedRef = useRef(0); useEffect(() => { if(visible) { delayTimePassedRef.current = 0; opacityRef.current = 1; setOpacity(1); return; } if(opacityRef.current <= 0) { return; } let timer = null; const startTime = performance.now(); const stopTimer = () => { delayTimePassedRef.current += performance.now() - startTime; try { timer?.remove(); timer = null; } catch(e) { console.error('[PanoramaViewer] fadeout animation timer removal failed:', e); } }; timer = setTimeoutRemovable(() => { if(!timer) { return; } timer = setAnimationFrameIntervalRemovable(deltaTime => { if(ISSET(decayFactor)) { const before = opacityRef.current; const after = before * Math.pow(FLOAT_LAX(decayFactor), deltaTime); const dif = Math.max(before - after, deltaTime / 2); opacityRef.current -= dif; } else { opacityRef.current -= deltaTime / (FLOAT_LAX_ANY(duration, 1000) / 1000); } if(opacityRef.current < 0.001) { opacityRef.current = 0; } setOpacity(opacityRef.current); if(opacityRef.current <= 0) { stopTimer(); } }); }, Math.max(0, FLOAT_LAX(delay) - delayTimePassedRef.current)); return stopTimer; }, [visible, delay, duration, decayFactor]); return {opacity}; }; /** * Allows you to easily convert promises to react hooks. * * The given callable should return promises. The returned promises can be an array, an object, or even a single promise. The returned data of this hook will match the promises it has operated on. * * The given comparingValues can be anything, this is used to detect whether the given promises have changed or not, and so whether new promises have to be generated and executed again. * * @param {function} callable - The callable that returns the promises. * @param {*} comparingValues - The values to compare the promises against. * @return {[*, boolean, *]} - The data, loading state, and error. */ export const usePromises = (callable, comparingValues) => { const comparingValuesClone = clone(comparingValues, true); const comparingValuesRef = useRef(comparingValuesClone); const latestComparingValuesRef = useRef(); latestComparingValuesRef.current = comparingValuesClone; const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { setLoading(true); setData(null); setError(null); try { const promises = callable(); const promisesIsObject = IS_OBJECT(promises) && (typeof promises?.then !== 'function'); const promisesIsArray = IS_ARRAY(promises); let promisesKeyed = []; if(promisesIsObject || promisesIsArray) { each(promises, (promise, key) => { promisesKeyed.push({promise, key}); }); } else { promisesKeyed.push({promise:promises, key:undefined}); } let wrappedPromises = []; each(promisesKeyed, ({promise, key}) => { wrappedPromises.push(promise .then(async result => ({result, key}))); }); Promise.all(wrappedPromises) .then(resultObjects => { if(promisesIsObject) { let results = {}; each(resultObjects, ({result, key}) => { results[key] = result; }); return results; } else if(promisesIsArray) { let results = []; each(resultObjects, ({result, key}) => { results[key] = result; }); return results; } return resultObjects.pop()?.result; }) .then(results => { if(!equals(latestComparingValuesRef.current, comparingValuesClone)) { // canceled return; } comparingValuesRef.current = comparingValuesClone; setLoading(false); setData(results); setError(null); }) .catch(error => { if(!equals(latestComparingValuesRef.current, comparingValuesClone)) { // canceled return; } comparingValuesRef.current = comparingValuesClone; setLoading(false); setData(null); setError(purgeErrorMessage(error)); }); return () => { each(wrappedPromises, promise => { try { promise?.cancel?.(); } catch(e) { console.error('Failed to cancel the given promise:', e); } try { promise?.remove?.(); } catch(e) { console.error('Failed to remove the given promise:', e); } }); }; } catch(error) { setAnimationFrameTimeoutRemovable(() => { if(!equals(latestComparingValuesRef.current, comparingValuesClone)) { // canceled return; } comparingValuesRef.current = comparingValuesClone; setLoading(false); setData(null); setError(purgeErrorMessage(error)); }); } // updates are controlled by the given comparingValues //eslint-disable-next-line react-hooks/exhaustive-deps }, [comparingValuesClone/*, callable*/]); if(!equals(comparingValuesRef.current, comparingValuesClone)) { return [null, true, null]; } return [data, loading, error]; };