UNPKG

design-react-kit

Version:

Componenti React per Bootstrap 5

202 lines 7.44 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ /*-------------------------------------------------------------------------- * This work derives from the React Use Navscroll library * Released under the MIT license by Marco Liberati (@dej611) * Code: https://github.com/dej611/react-use-navscroll * -------------------------------------------------------------------------- * Parts of this code has been modified using Bootstrap Italia source code * -------------------------------------------------------------------------- * Bootstrap Italia (https://italia.github.io/bootstrap-italia/) * Authors: https://github.com/italia/bootstrap-italia/blob/main/AUTHORS * License: BSD-3-Clause (https://github.com/italia/bootstrap-italia/blob/main/LICENSE) * -------------------------------------------------------------------------- */ import { createRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { debounce } from './debounce'; import { useSizeDetector } from './useSizeDetector'; import { v4 as uuidv4 } from 'uuid'; let ticking = false; let callbacks = []; class ScrollCallback { _callback; id; constructor(id, callback) { this.id = id; this._callback = callback; } //Public dispose() { removeCallBack(this.id); } //Private _execute(data) { this._callback(data); } } const removeCallBack = (id) => { callbacks = callbacks.filter((cb) => cb.id !== id); }; const onDocumentScroll = (callback) => { if (typeof document === 'undefined') { return; } if (!callbacks.length) { if (typeof window !== 'undefined' && typeof document !== 'undefined') { document.addEventListener('scroll', (evt) => { if (!ticking) { window.requestAnimationFrame(() => { callbacks.forEach((cbObj) => cbObj.cb._execute(evt)); ticking = false; }); ticking = true; } }); } } if (typeof callback === 'function') { const newCb = new ScrollCallback(uuidv4(), callback); callbacks.push({ id: newCb.id, cb: newCb }); return newCb; } console.error('[onDocumentScroll] the provided data has to be of type function'); return null; }; const hasWindow = typeof window !== 'undefined'; const REGISTER_DELAY = 50; function resolveHierarchyIds(id, lookup) { const newActiveIds = [id]; let lastId = newActiveIds[0]; while (lastId != null && lookup[lastId] != null) { newActiveIds.push(lookup[lastId]); lastId = lookup[lastId]; } // return a list from parent to current child return newActiveIds.reverse(); } /** * This is the main hook: use it in a react function component to track * the state of the passed ids. The function accepts an initial configuration * of type `useNavScrollArgs` to customize the behaviour. */ export function useNavScroll(args = {}) { const { onChange, root, offset = 50, isHorizontal = false } = args; const els = useRef([]); const [counter, setCounter] = useState(0); const [forceRecompute, setForceRecompute] = useState(false); const [activeId, updateActiveId] = useState(null); const [percentageValue, setPercentageValue] = useState(0); const { targetSize, useViewport } = useSizeDetector({ root, isHorizontal, onChange, activeId, setForceRecompute, updateActiveId, hasWindow }); const observerMargin = Math.floor((targetSize * offset) / 100) || 1; const observerOptions = useMemo(() => { const topMargin = observerMargin % 2 === 1 ? observerMargin - 1 : observerMargin; const bottomMargin = targetSize - observerMargin; return { root: useViewport ? null : root, rootMargin: isHorizontal ? `0px ${-topMargin}px 0px ${-bottomMargin}px` : `${-topMargin}px 0px ${-bottomMargin}px 0px` }; }, [root, targetSize, observerMargin, isHorizontal, useViewport]); const elsLookup = useMemo(() => { const lookup = {}; for (const { id, parent } of els.current) { lookup[id] = parent; } return lookup; }, [counter]); const activeIds = useMemo(() => (activeId ? resolveHierarchyIds(activeId, elsLookup) : []), [activeId, elsLookup]); const activeLookups = useMemo(() => new Set(activeIds), [activeIds]); useEffect(() => { if (!hasWindow) { return; } const _onScroll = () => { let intersectionId = null; for (let k = 0; k < els.current.length; k++) { const entry = els.current[k].ref.current; const min = entry?.getBoundingClientRect().top ? entry?.getBoundingClientRect().top : 0; if (!min) { break; } if (min > 0 && k > 0) { const totEls = root?.previousSibling?.firstChild?.parentNode?.querySelectorAll('.it-navscroll-wrapper .nav-link').length || 1; setPercentageValue((k / (totEls / 2)) * 100); intersectionId = els.current[k - 1].ref.current?.id; break; } } if (intersectionId != null) { updateActiveId(intersectionId); if (onChange) { const diffIds = { added: intersectionId, removed: activeId }; onChange(diffIds); } } }; onDocumentScroll(_onScroll); setTimeout(() => { _onScroll(); }, 300); }, [ activeIds, updateActiveId, els, elsLookup, onChange, activeLookups, activeId, observerOptions, isHorizontal, root, forceRecompute ]); const refresh = useCallback(debounce(() => { setCounter(counter + 1); }, REGISTER_DELAY), [counter]); const register = useCallback((id, options = {}) => { if (!hasWindow) { return { id, ref: null }; } const alreadyRegistered = id in elsLookup; const entry = alreadyRegistered ? els.current.find(({ id: existingId }) => existingId === id) : options; const ref = (entry && entry.ref) || createRef(); if (!alreadyRegistered) { els.current = [...els.current, { id, ref, parent: options.parent }]; refresh(); } return { id, ref }; }, [counter]); const unregister = useCallback((idToUnregister) => { els.current = els.current.filter(({ id }) => id !== idToUnregister); }, [counter]); const isActive = useCallback((id) => activeLookups.has(id), [activeLookups]); const percentage = useMemo(() => percentageValue, [percentageValue]); const getActiveRef = useCallback(() => { const entry = els.current.find(({ id }) => id === activeId); return entry ? entry.ref : null; }, [activeId]); return { percentage, register, unregister, activeIds, isActive, getActiveRef }; } //# sourceMappingURL=useNavScroll.js.map