UNPKG

@svelte-use/core

Version:

Collection of essential Svelte Utilities

405 lines (387 loc) 12.3 kB
import { isClient, isString, noop, toReadable, tryOnDestroy, unstore, writable, promiseTimeout, toWritable } from '@svelte-use/shared'; export * from '@svelte-use/shared'; import { listen } from 'svelte/internal'; import { get } from 'svelte/store'; const defaultWindow = isClient ? window : undefined; const defaultDocument = isClient ? window.document : undefined; function useEventListener(...args) { let target; let event; let listener; let options; if (isString(args[0])) { [event, listener, options] = args; target = defaultWindow; } else { [target, event, listener, options] = args; } if (!target) { return noop; } let cleanup = noop; const store = toReadable(target); store.subscribe((el) => { cleanup(); if (!el) { return; } const dispose = listen(el, event, listener, options); cleanup = () => { dispose(); cleanup = noop; }; }); const stop = () => { cleanup(); }; tryOnDestroy(stop); return stop; } /** * Listen for clicks outside of an element * * @see https://svelte-use.vercel.app/core/onClickOutside * @param target * @param handler * @param options */ function onClickOutside(target, handler, options = {}) { const { window = defaultWindow, event = 'pointerdown' } = options; if (!window) { return; } const listener = (event) => { const el = unstore(target); if (!el) { return; } if (el === event.target || event.composedPath().includes(el)) { return; } handler(event); }; return useEventListener(window, event, listener, { passive: true }); } /** * Reactive async state. Will not block your setup function and will triggers changes once * the promise is ready. * * @see https://svelte-use.vercel.app/core/useAsyncState * @param promise The promise / async function to be resolved * @param initialState The initial state, used until the first evaluation finishes * @param options */ function useAsyncState(promise, initialState, options = {}) { const { immediate = true, delay = 0, onError = noop, resetOnExecute = true, } = options; const state = writable(initialState); const isReady = writable(false); const error = writable(undefined); async function execute(delay = 0, ...args) { if (resetOnExecute) { state.set(initialState); } error.set(undefined); isReady.set(false); if (delay > 0) { await promiseTimeout(delay); } const _promise = typeof promise === 'function' ? promise(...args) : promise; try { const data = await _promise; state.set(data); isReady.set(true); } catch (e) { error.set(e); onError(e); } } if (immediate) { execute(delay); } return { state, isReady, error, execute, }; } const StorageSerializers = { boolean: { read: (v) => v === 'true', write: (v) => String(v), }, object: { read: (v) => JSON.parse(v), write: (v) => JSON.stringify(v), }, number: { read: (v) => Number.parseFloat(v), write: (v) => String(v), }, string: { read: (v) => v, write: (v) => String(v), }, any: { read: (v) => v, write: (v) => String(v), }, }; /** * Reactive LocalStorage/SessionStorage * * @see https://svelte-use.vercel.app/core/useStorage * @param key * @param initialValue * @param storage * @param options */ function useStorage(key, initialValue, storage = defaultWindow === null || defaultWindow === void 0 ? void 0 : defaultWindow.localStorage, options = {}) { var _a; const { listenToStorageChanges = true, window = defaultWindow, onError = (e) => { console.error(e); }, } = options; const rawInit = unstore(initialValue); // https://github.com/vueuse/vueuse/blob/706a04921c6bb81a5fc3687c2b2748a81f5e9a21/packages/core/useStorage/index.ts#L98 const type = rawInit == null ? 'any' : typeof rawInit === 'boolean' ? 'boolean' : typeof rawInit === 'string' ? 'string' : typeof rawInit === 'object' ? 'object' : Array.isArray(rawInit) ? 'object' : !Number.isNaN(rawInit) ? 'number' : 'any'; const data = toWritable(initialValue); const serializer = (_a = options.serializer) !== null && _a !== void 0 ? _a : StorageSerializers[type]; function read(event) { if (!storage || (event && event.key !== key)) { return; } try { const rawValue = event ? event.newValue : storage.getItem(key); if (rawValue == null) { data.set(rawInit); if (rawInit !== null) { storage.setItem(key, serializer.write(rawInit)); } } else { data.set(serializer.read(rawValue)); } } catch (e) { onError(e); } } read(); if (window && listenToStorageChanges) { useEventListener(window, 'storage', read); } if (storage) { data.subscribe((value) => { try { if (value == null) { storage.removeItem(key); } else { storage.setItem(key, serializer.write(value)); } } catch (e) { onError(e); } }); } return data; } /** * Reactive LocalStorage * * @see https://svelte-use.vercel.app/core/useLocalStorage * @param key * @param initialValue * @param options */ function useLocalStorage(key, initialValue, options = {}) { const { window = defaultWindow } = options; return useStorage(key, initialValue, window === null || window === void 0 ? void 0 : window.localStorage, options); } /** * Reactive Media Query * * @param query * @param options */ function useMediaQuery(query, options = {}) { const matches = writable(false); const { window = defaultWindow } = options; if (!window) { matches.set(false); return matches; } const mediaQuery = window.matchMedia(query); matches.set(mediaQuery.matches); const handler = (event) => { matches.set(event.matches); }; mediaQuery.addEventListener('change', handler); tryOnDestroy(() => { mediaQuery.removeEventListener('change', handler); }); return toReadable(matches); } /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } /** * Watch for changes being made to the DOM tree. * * @see https://svelte-use.vercel.app/core/useMutationObserver * @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver MutationObserver MDN * @param target * @param callback * @param options */ function useMutationObserver(target, callback, options = {}) { const { window = defaultWindow } = options, mutationOptions = __rest(options, ["window"]); let observer; const isSupported = window && 'IntersectionObserver' in window; const cleanup = () => { if (observer) { observer.disconnect(); observer = undefined; } }; const unsubscribe = toReadable(target).subscribe((el) => { cleanup(); if (isSupported && window && el) { // @ts-expect-error missing type observer = new window.MutationObserver(callback); observer.observe(el, mutationOptions); } }); const stop = () => { cleanup(); unsubscribe(); }; tryOnDestroy(stop); return { isSupported, stop, }; } /** * Reactive dark theme preference. * * @param [options] */ function usePreferredDark(options) { return useMediaQuery('(prefers-color-scheme: dark)', options); } /** * Call function on every `requestAnimationFrame`. With controls of pausing and resuming. * * @see https://svelte-use.vercel.app/core/useRafFn * @param fn * @param options */ function useRafFn(fn, options = {}) { const { immediate = true, window = defaultWindow } = options; const isActive = writable(false); function loop() { if (!get(isActive)) { return; } fn(); if (window) { window.requestAnimationFrame(loop); } } function resume() { if (!get(isActive)) { isActive.set(true); loop(); } } function pause() { isActive.set(false); } if (immediate) { resume(); } tryOnDestroy(pause); return { isActive, resume, pause, }; } /** * Reactive SessionStorage * * @see https://svelte-use.vercel.app/core/useSessionStorage * @param key * @param initialValue * @param options */ function useSessionStorage(key, initialValue, options = {}) { const { window = defaultWindow } = options; return useStorage(key, initialValue, window === null || window === void 0 ? void 0 : window.sessionStorage, options); } /** * Reactive document title. * * @see https://svelte-use.vercel.app/core/useTitle * @param newTitle * @param options */ function useTitle(newTitle = null, options = {}) { var _a, _b; const { document = defaultDocument, observe = false } = options; const title = toWritable((_a = newTitle !== null && newTitle !== void 0 ? newTitle : document === null || document === void 0 ? void 0 : document.title) !== null && _a !== void 0 ? _a : null); let oldTitle = document === null || document === void 0 ? void 0 : document.title; title.subscribe((t) => { if (isString(t) && t !== oldTitle && document) { document.title = t; } oldTitle = t; }); if (observe && document) { useMutationObserver((_b = document.head) === null || _b === void 0 ? void 0 : _b.querySelector('title'), () => { if (document && document.title !== get(title)) { title.set(document.title); } }, { childList: true, }); } return title; } export { StorageSerializers, onClickOutside, useAsyncState, useEventListener, useLocalStorage, useMediaQuery, useMutationObserver, usePreferredDark, useRafFn, useSessionStorage, useStorage, useTitle };