UNPKG

blade

Version:
282 lines (280 loc) • 9.47 kB
import { _ as RootClientContext, a as useLocation, c as usePopulatePathname, l as useRedirect, n as usePrivateLocationRef, r as useReduce, t as usePageTransition } from "./hooks-BCUm8YC_.js"; import { t as generateUniqueId } from "./utils-BIjILkWh.js"; import { v as isStorableObject, y as processStorableObjects } from "./utils-BeDKhSpT.js"; import { r as QUERY_SYMBOLS } from "./dist-C7NzDGd9.js"; import { n as getSyntaxProxy, t as getBatchProxy } from "./queries-CO8sEzJ9.js"; import { t as deserializeError } from "./errors-B8jB1JUF.js"; import { useCallback, useContext, useEffect, useMemo, useRef } from "react"; //#region public/client/hooks.ts const useMutation = () => { const clientContext = useContext(RootClientContext); if (!clientContext) throw new Error("Missing client context in `useMutation`"); const { deferredPromises, collected } = clientContext; const privateLocationRef = usePrivateLocationRef(); const { transitionPage } = usePageTransition(); const populatePathname = usePopulatePathname(); for (const [promiseID, promise] of Object.entries(deferredPromises.current)) { let rejection; const results = collected.queries.filter(({ hookHash, result, error }) => { if (hookHash !== promiseID) return false; if (typeof error !== "undefined") rejection = error; return typeof result !== "undefined"; }).map(({ result }) => result); if (rejection) promise.reject(deserializeError(rejection)); else if (results.length > 0) promise.resolve(results); } const queryHandler = async (queries, options) => { const privateLocation = privateLocationRef.current; const currentPathnameWithQuery = privateLocation.pathname + privateLocation.search + privateLocation.hash; const destination = options?.redirect ? populatePathname(options.redirect) : currentPathnameWithQuery; const hookHash = generateUniqueId(); const files = /* @__PURE__ */ new Map(); const updatedQueries = await processStorableObjects(queries, (objects) => { return objects.map(({ value: file }) => { const id = generateUniqueId(); files.set(id, file); return { name: file.name, key: id, src: file.name, meta: { size: file.size, type: file.type }, placeholder: null }; }); }); transitionPage(destination, { queries: queries.map((_, index) => ({ query: JSON.stringify(updatedQueries[index]), type: "write", database: options?.database, hookHash })), errorFallback: currentPathnameWithQuery, files }); return new Promise((resolve, reject) => { const clear = () => { delete deferredPromises.current[hookHash]; }; deferredPromises.current[hookHash] = { resolve: (result) => { resolve(result); clear(); }, reject: (error) => { reject(error); clear(); } }; }); }; const callback = async (defaultQuery, options) => { return (await queryHandler([defaultQuery[QUERY_SYMBOLS.QUERY]], options))[0]; }; const replacer = (value) => isStorableObject(value) ? value : void 0; return { add: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.add`, callback, replacer }), set: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.set`, callback, replacer }), remove: getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.remove`, callback, replacer }), batch: (operations, options) => { return queryHandler(getBatchProxy(operations).map(({ structure }) => structure), options); } }; }; const useLinkEvents = (destination) => { const { primePageCache, transitionPage } = usePageTransition(); const populatePathname = usePopulatePathname(); const activeTransition = useRef(); if (!destination) return { onClick: void 0, onMouseEnter: void 0, onTouchStart: void 0 }; const populatedPathname = populatePathname(destination); const primePage = () => { primePageCache(populatedPathname); }; return { onMouseEnter: () => primePage(), onTouchStart: () => primePage(), onClick: (event) => { if (event.defaultPrevented) return; if (event.metaKey) return; event.preventDefault(); if (!activeTransition.current) primePage(); transitionPage(populatedPathname, { acceptCache: true }); } }; }; /** * Hook for paginating a list of records intelligently. * * @param moreAfter - The pagination identifier provided for a list of records by `use`. * In the case of `const accounts = use.accounts();`, for example, `accounts.moreAfter` * should be passed. * @param options - A list of options for customizing the pagination behavior. * @param options.updateAddressBar - By default, a `?page` parameter will be added to the * URL, which allows for sharing the current pagination status of the page with other * people. Setting this argument to `false` will avoid that. * * @returns A function that can be invoked to load the next page. */ const usePagination = (moreAfter, options) => { const { transitionPage } = usePageTransition(); const privateLocationRef = usePrivateLocationRef(); const loadingMore = useRef(false); useEffect(() => { if (!loadingMore.current) return; loadingMore.current = false; }, [moreAfter]); const resetPagination = () => { const privateLocation = privateLocationRef.current; if (!privateLocation.searchParams.has("page")) { console.debug(`Cannot reset pagination because it isn't active.`); return; } console.debug("Pagination was reset"); const newSearchParams = new URLSearchParams(privateLocation.searchParams); newSearchParams.delete("page"); const params = newSearchParams.toString(); transitionPage(privateLocation.pathname + (params ? `?${params}` : "")); }; if (!moreAfter) return { paginate: () => { console.debug("Pagination did not occur because no further records are available."); }, resetPagination }; const paginate = () => { const privateLocation = privateLocationRef.current; if (loadingMore.current) { console.debug("Pagination did not occur again because it is already ongoing."); return; } const newSearchParams = new URLSearchParams(privateLocation.searchParams); newSearchParams.set("page", moreAfter); const newPath = `${privateLocation.pathname}?${newSearchParams.toString()}`; loadingMore.current = true; transitionPage(newPath, { updateAddressBar: options?.updateAddressBar }); }; return { paginate, resetPagination }; }; /** * Concatenates arrays based on pagination. Whenever the current paginated page changes, * the provided items will be concatenated with the previously provided list of items. * * @param items - An array of items (of any type) that should be accumulated. For example, * this could be a list of React children. * @param options.moreBefore - The `moreBefore` property of the record list that * should be paginated. * @param options.allowUpdates - Controls whether changes to the provided items should be * allowed, or if they should instead just be ignored. * * @returns The concatenated array of items, and a function for updating it. */ const usePaginationBuffer = (items, options) => { const { allowUpdates = true, moreBefore: page } = options; const [renderedChildren, setRenderedChildren] = useReduce((existing) => { const storedPreviousPage = existing?.page; if (!storedPreviousPage && page || storedPreviousPage && page && storedPreviousPage !== page) return { buffered: true, items: [...existing?.items || [], ...items], page }; if (!allowUpdates && existing?.buffered) return existing; if (storedPreviousPage && page && storedPreviousPage === page) { const buffered = existing?.buffered || false; return { buffered, items: buffered ? existing?.items || [] : items, page }; } return { buffered: false, items, page }; }, [items]); const setValue = (factory) => { setRenderedChildren((prevState) => ({ ...prevState, items: factory(prevState.items) })); }; return [renderedChildren.items, setValue]; }; /** * A hook for search / query parameter state management. * * @param key - The key of the query parameter to manage. * @param options - Options for the query parameter. * * @returns A tuple containing the current value of the query parameter and a function to set it. * * @example * ```tsx * import { useQueryState } from 'blade/client/hooks'; * * export default () => { * const [hello, setHello] = useQueryState('hello'); * * return ( * <> * <input * onChange={(e) => setHello(e.target.value)} * value={hello} * /> * <p>Hello, {hello || 'world'}!</p> * </> * ) * } * ``` */ const useQueryState = (key, options) => { const { defaultValue = null, parse } = options ?? {}; const location = useLocation(); const redirect = useRedirect(); const queryState = useMemo(() => { const rawParam = location.searchParams.get(key); if (rawParam === null) return defaultValue; if (parse) return parse({ defaultValue, value: rawParam }); return rawParam; }, [location.searchParams]); return [queryState, useCallback((value) => { if (value === null) { location.searchParams.delete(key); redirect(location.href, { immediatelyUpdateQueryParams: true }); return; } location.searchParams.set(key, String(value)); redirect(location.href, { immediatelyUpdateQueryParams: true }); }, [ queryState, location.searchParams, redirect ])]; }; //#endregion export { useQueryState as a, usePaginationBuffer as i, useMutation as n, usePagination as r, useLinkEvents as t };