import React from 'react'; import { BrowserRouter, RouteObject, Params, useParams, Location, useLocation, NavigateFunction, useNavigate, Navigate, } from 'react-router-dom' import { Loading as LoadingComponent } from '../layout/loading'; import Localization from '../providers/localization'; import Themes, { Theme } from '../providers/theme'; import { NoData } from '../services'; export namespace Hook { /*************************************** PROVIDER ***************************************/ interface ProviderProps { routes?: Routes, dialogs?: DialogComponent[], children?: React.ReactNode | React.ReactNode[] | any, } export const Provider: React.FC = ({ routes, dialogs, children }) => { if (!dialogs) { dialogs = [] } const ProviderContent: React.FC = () => { const location = useLocation(); const navigate = useNavigate(); const setupRouteGuard = React.useCallback(() => { if (routes && routes.guard) { const path = routes.guard(location.pathname); if (typeof path === 'string') { if (location.pathname !== path) { navigate(path); } } else { path.then(path => { if (location.pathname !== path) { navigate(path); } }) } } }, [location, navigate]) React.useEffect((setupGuard = setupRouteGuard) => { setupGuard(); }); return ( <> {children} { dialogs && dialogs.map((component, key) => ( )) } ) } return ( ); } /**************************************** DIALOG ****************************************/ type DialogComponentProps = P & { open?: boolean, data: D, onClose?: DialogCallback, } export interface DialogCallbackEvent { data: D, rejected?: boolean } export type DialogCallback = (event: DialogCallbackEvent) => void; export type DialogComponent = React.ComponentType>; const DialogHook: React.FC<{ component: DialogComponent }> = ({ component: Component }) => { const [open, setOpen] = React.useState(false); const [data, setData] = React.useState(null); const [callback, setCallback] = React.useState<{ func: DialogCallback }>({ func: () => { } }); (Component as any).open = (data: any, callback: DialogCallback) => { setData(data); setOpen(true); setCallback({ func: callback }); } (Component as any).close = () => { setData(null); setOpen(false); setCallback({ func: () => { } }); } const handleClose = (event: DialogCallbackEvent) => { setOpen(false); if (callback && callback.func) { callback.func(event); } } return ( <> { open && } ); } export function Dialog(dialog: (props: DialogComponentProps, context?: any) => React.ReactElement | null): { (props: DialogComponentProps, context?: any): React.ReactElement | null; open: (data: D, callback?: DialogCallback) => void; close: () => void; } { return dialog as any } /**************************************** EVENTS ****************************************/ class EventHandler { private subscriptions: { [key: string]: (value: any) => void } = {} public register(key: string, callback: (value: any) => void) { this.subscriptions[key] = callback; } public unregister(key: string) { delete this.subscriptions[key]; } public send(value: any) { for (const key in this.subscriptions) { if (Object.prototype.hasOwnProperty.call(this.subscriptions, key)) { const subscription = this.subscriptions[key]; if (subscription) { subscription(value) } } } } } const eventHandlers: { [key: string]: EventHandler } = {} export interface Event { id: T, } export function createEvent(): Event { const id = guid() as any; eventHandlers[id] = new EventHandler(); const result: Event = { id, } return result; } export function sendEvent(context: Event, value: T) { if (context) { const handler = eventHandlers[context.id as any]; if (handler) { handler.send(value) } } } export function useEvent(context: Event, callback: (value: T) => void) { React.useEffect(() => { const handler = crossStateHandlers[context.id as any]; if (handler) { const id = guid(); handler.register(id, callback) return () => { handler.unregister(id); } } }) } /************************************* CROSS STATES *************************************/ class CrossStateHandler { private subscriptions: { [key: string]: (value: any) => void } = {} private currentValue: any; constructor(defaultValue: any) { this.currentValue = defaultValue; } public register(key: string, callback: (value: any) => void) { this.subscriptions[key] = callback; } public unregister(key: string) { delete this.subscriptions[key]; } public get value(): any { return this.currentValue; } public set value(val: any) { if (this.currentValue !== val && (!Object.is(this.currentValue, NaN) || !Object.is(val, NaN))) { this.currentValue = val; for (const key in this.subscriptions) { if (Object.prototype.hasOwnProperty.call(this.subscriptions, key)) { const subscription = this.subscriptions[key]; if (subscription) { subscription(this.currentValue) } } } } } } const crossStateHandlers: { [key: string]: CrossStateHandler } = {} export interface CrossStateContext { id: T } export function createCrossState(defaultValue?: T): CrossStateContext { const id = guid() as any; crossStateHandlers[id] = new CrossStateHandler(defaultValue); const result: CrossStateContext = { id } return result; } export function crossState(context: CrossStateContext): { getValue: () => T, setValue: (newValue: T) => void } { const handler = crossStateHandlers[context.id as any]; return handler ? { getValue: () => handler.value, setValue: (newValue: T) => { handler.value = newValue } } : { getValue: () => NoData, setValue: () => { } } } export function useCrossState(context: CrossStateContext): [T, (value: T) => void] { const handler = crossStateHandlers[context.id as any]; const [value, setValue] = React.useState( context && crossStateHandlers[context.id as any] ? crossStateHandlers[context.id as any].value : NoData ); const handleSetValue = React.useCallback((newValue: T) => { if (handler) { handler.value = newValue; if (value !== newValue) { setValue(handler.value); } } }, [value, setValue]) React.useEffect(() => { if (handler) { const id = guid(); if (value !== handler.value) { setValue(handler.value); } handler.register(id, handleSetValue) return () => { handler.unregister(id); } } }) return [value, handleSetValue] } export function useRefresh(): [() => void] { const [forceUpdate, setForceUpdate] = React.useState(false); return [() => setForceUpdate(!forceUpdate)] } /**************************************** THEMES ****************************************/ export function useTheme(): [Theme, (name: string) => void] { const { theme, setTheme } = React.useContext(Themes.Context); return [theme, setTheme]; } /************************************* LOCALIZATION *************************************/ export function useLanguage(): [string, (lan: string) => void] { const { language, setLanguage } = React.useContext(Localization.Context); return [language, setLanguage]; } export function useLocalization(texts: { [language: string]: T }): [T, string, (language: string) => void] { const { language, setLanguage } = React.useContext(Localization.Context); return [texts[language], language, setLanguage]; } /**************************************** LOADING ***************************************/ class LoadingHandler { private subscriptions: { [key: string]: (value: boolean) => void } = {} private currentValue: number = 0; public register(key: string, callback: (value: boolean) => void) { this.subscriptions[key] = callback; } public unregister(key: string) { delete this.subscriptions[key]; } public increase() { this.currentValue++; if (this.currentValue === 1) { for (const key in this.subscriptions) { if (Object.prototype.hasOwnProperty.call(this.subscriptions, key)) { const subscription = this.subscriptions[key]; if (subscription) { subscription(true) } } } } } public decrease() { if (this.currentValue > 0) { this.currentValue--; if (this.currentValue === 0) { for (const key in this.subscriptions) { if (Object.prototype.hasOwnProperty.call(this.subscriptions, key)) { const subscription = this.subscriptions[key]; if (subscription) { subscription(false) } } } } } } public get value(): boolean { return !!this.currentValue; } } const loadingHandler = new LoadingHandler(); export const Loading: React.FC<{}> = () => { const [loading, setLoading] = React.useState(loadingHandler.value); const handleSetValue = React.useCallback((newValue: boolean) => { if (loading !== newValue) { setLoading(newValue); } }, [loading, setLoading]) React.useEffect(() => { const id = guid(); if (loading !== loadingHandler.value) { setLoading(loadingHandler.value); } loadingHandler.register(id, handleSetValue) return () => { loadingHandler.unregister(id); } }) return ( <> {loading && } ) } export function loading(): { start: () => void, stop: () => void } { return { start: () => loadingHandler.increase(), stop: () => loadingHandler.decrease() } } /**************************************** ROUTES ****************************************/ export interface RouteMenu { space?: boolean, icon: React.FC, sidebar?: React.FC, } export interface Route { menu?: RouteMenu, admin?: boolean, component: React.FC, children?: { [path: string]: Route }, } export interface Routes { guard?: (path: string) => Promise | string, labels?: { [lan: string]: { [path: string]: string } }, children?: { [path: string]: Route }, } export function createRoutes(routes: Routes) { return routes ? convertToBrowserRoutes(routes.children) : [] } function convertToBrowserRoutes(routes?: { [path: string]: Route }, nested?: boolean): RouteObject[] { const result: RouteObject[] = []; if (routes) { let defaultPath = ''; let needDefault = true; for (const path in routes) { if (Object.prototype.hasOwnProperty.call(routes, path)) { const route = routes[path]; result.push({ path: nested && path && path.length > 0 && path[0] === '/' ? path.substring(1) : path, caseSensitive: false, element: , children: convertToBrowserRoutes(route.children, true) }) if (path === '/' || path === '*') { needDefault = false; } else if (!defaultPath) { defaultPath = path; } } } if (needDefault) { result.push({ path: '*', element: }) } } return result; } export function getVisibleRoutes(routes: Routes, isVisible: (path: string, route?: Route) => boolean): Routes { return routes ? { guard: routes.guard, labels: routes.labels, children: findVisibleRoutes(routes.children, '', isVisible) } : {} } function findVisibleRoutes(routes: { [path: string]: Route } | undefined, globalPath: string, isVisible: (path: string, route?: Route) => boolean): { [path: string]: Route } { const result: { [path: string]: Route } = {} if (routes) { for (const path in routes) { if (Object.prototype.hasOwnProperty.call(routes, path)) { const route = routes[path]; const children = findVisibleRoutes(route.children, globalPath + path, isVisible); if (Object.keys(children).length === 0 && !isVisible(globalPath + path, route)) { continue; } result[path] = { menu: route.menu, component: route.component, children } } } } return result; } export function isValidPath(routes: Routes, path: string) { if (routes) { const splitPath = path.split('/').map(x => x.trim()).filter(x => x); let route: any = { children: !!routes.children && routes.children['/'] ? routes.children['/'].children : routes.children, } for (let i = 0; !!route && i < splitPath.length; i++) { const splitKey = `/${splitPath[i]}`; if (route.children) { route = route.children[splitKey]; } } return !!route; } return false; } export function getCrumbroad(routes: Routes) { if (routes && routes.labels) { const language = (localStorage && localStorage.getItem("app-localization")) || "en"; if (routes.labels[language]) { let result = ''; const splitPath = window.location.pathname .split('/') .map((x) => x.trim()) .filter((x) => x) .map(x => `/${x}`); let currentPath = ''; for (let i = 0; i < splitPath.length; i++) { const path = splitPath[i]; currentPath += path; if (!routes.labels[language][currentPath]) { return ''; } result = result ? `${result} > ${routes.labels[language][currentPath]}` : routes.labels[language][currentPath] } return result; } } return ''; } export function useRouter(): [Location & { params: Readonly> }, NavigateFunction] { const params = useParams(); const location = useLocation(); const navigate = useNavigate(); return [{ ...location, params }, navigate] } /************************************** DIMENSIONS **************************************/ export const useDimensions = (excludePadding = false) => { const ref = React.useRef(null); const [dimensions, setDimensions] = React.useState({ width: 0, height: 0 }); const handleSetDimensions = React.useCallback(() => { if (ref?.current) { const style = window.getComputedStyle(ref.current, null); const innerSize = { height: style.height, width: style.width }; const outterSize = ref.current.getBoundingClientRect().toJSON(); setDimensions(excludePadding ? innerSize : outterSize); } }, [excludePadding]); React.useLayoutEffect(() => { handleSetDimensions(); }, [handleSetDimensions]); React.useEffect(() => { const listener = () => { handleSetDimensions(); }; window.addEventListener("resize", listener); return () => { window.removeEventListener("resize", listener); }; }, [handleSetDimensions]); return [ref, dimensions]; } /*********************************** SCROLL POSITION ************************************/ export type ScrollPosition = { x: number, y: number, w: number, h: number, c: { w: number, h: number } } export const DefaultScrollPosition: ScrollPosition = { x: 0, y: 0, w: 0, h: 0, c: { w: 0, h: 0 } }; type ScrollPositionEffect = (params: { previous: ScrollPosition, current: ScrollPosition }) => void export const useScrollPosition = (element: any, handler?: ScrollPositionEffect, debounce: number = 100, deps?: any[]) => { const position = React.useRef(getScrollPosition()) const throttleTimeout = React.useRef(null); const callback = React.useCallback(() => { const currPos = getScrollPosition(element); if (handler) { handler({ previous: position.current, current: currPos }); } position.current = currPos; throttleTimeout.current = null; }, [handler, element]) React.useLayoutEffect(() => { const handleScroll = () => { if (debounce) { if (throttleTimeout.current === null) { throttleTimeout.current = setTimeout(callback, debounce); } } else { callback() } } if (element && element.current) { element.current.addEventListener('scroll', handleScroll); } else { window.addEventListener('scroll', handleScroll); } return () => { if (element && element.current) { element.current.removeEventListener('scroll', handleScroll); } else { window.removeEventListener('scroll', handleScroll); } } }, [callback, handler, element, deps, debounce]) React.useLayoutEffect(() => { const handleResize = () => { if (debounce) { if (throttleTimeout.current === null) { throttleTimeout.current = setTimeout(callback, debounce); } } else { callback() } } const currPos = getScrollPosition(element); if (currPos.x !== position.current.x || currPos.y !== position.current.y || currPos.w !== position.current.w || currPos.h !== position.current.h) { handleResize() } window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); } }, [callback, handler, element, deps, debounce]) if (!handler) { return position.current; } } function getScrollPosition(element?: any): ScrollPosition { const isBrowser = typeof window !== `undefined`; if (!isBrowser) return DefaultScrollPosition; const target = element ? element.current : document.body return { x: target.scrollLeft, y: target.scrollTop, w: target.clientWidth, h: target.clientHeight, c: { w: target.scrollWidth, h: target.scrollHeight, } } } /*************************************** DEBOUNCE ***************************************/ export function useDebounce void>( callback: T, delay: number, deps: React.DependencyList, ): T { const context = React.useRef({ disposed: false as boolean, timeoutId: undefined as number | undefined, callbackWrapper: undefined as Function | undefined }) React.useLayoutEffect(() => { return () => { context.current.disposed = true if (context.current.callbackWrapper && context.current.timeoutId) { context.current.callbackWrapper() clearTimeout(context.current.timeoutId) } } }, deps) return React.useCallback(((...args) => { if (context.current.disposed) { throw new Error( [ 'Trying to call an already disposed callback.', 'In theory you should never call a disposed callback.', 'This is probably a bug.', ].join(' '), ) } clearTimeout(context.current.timeoutId) context.current.callbackWrapper = () => { context.current.timeoutId = undefined; return callback(...args) } context.current.timeoutId = setTimeout(context.current.callbackWrapper, delay) }) as T, deps) } /****************************************************************************************/ function guid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = Math.random() * 16 | 0, v = c === 'x' ? r : ((r & 0x3) | 0x8); return v.toString(16); }); } } export default Hook;