From b61eaf2bedcd5b936f56c6bed7116945df6765ce Mon Sep 17 00:00:00 2001 From: Hunter Johnston <64506580+huntabyte@users.noreply.github.com> Date: Sat, 21 Dec 2024 20:01:09 -0500 Subject: [PATCH] feat: `onClickOutside` (#190) Co-authored-by: Bohdan Sviripa --- .changeset/bright-rabbits-obey.md | 5 + packages/runed/src/lib/internal/types.ts | 2 + packages/runed/src/lib/internal/utils/dom.ts | 26 ++ packages/runed/src/lib/internal/utils/is.ts | 4 + packages/runed/src/lib/utilities/index.ts | 1 + .../src/lib/utilities/onClickOutside/index.ts | 1 + .../onClickOutside/onClickOutside.svelte.ts | 226 ++++++++++++++ .../onClickOutside.test.svelte.ts | 278 ++++++++++++++++++ .../src/content/utilities/on-click-outside.md | 165 +++++++++++ .../demos/on-click-outside-dialog.svelte | 44 +++ .../components/demos/on-click-outside.svelte | 35 +++ .../src/routes/api/search.json/search.json | 2 +- 12 files changed, 788 insertions(+), 1 deletion(-) create mode 100644 .changeset/bright-rabbits-obey.md create mode 100644 packages/runed/src/lib/utilities/onClickOutside/index.ts create mode 100644 packages/runed/src/lib/utilities/onClickOutside/onClickOutside.svelte.ts create mode 100644 packages/runed/src/lib/utilities/onClickOutside/onClickOutside.test.svelte.ts create mode 100644 sites/docs/src/content/utilities/on-click-outside.md create mode 100644 sites/docs/src/lib/components/demos/on-click-outside-dialog.svelte create mode 100644 sites/docs/src/lib/components/demos/on-click-outside.svelte diff --git a/.changeset/bright-rabbits-obey.md b/.changeset/bright-rabbits-obey.md new file mode 100644 index 00000000..15900897 --- /dev/null +++ b/.changeset/bright-rabbits-obey.md @@ -0,0 +1,5 @@ +--- +"runed": minor +--- + +feat: `onClickOutside` diff --git a/packages/runed/src/lib/internal/types.ts b/packages/runed/src/lib/internal/types.ts index e04785f1..fc8b283b 100644 --- a/packages/runed/src/lib/internal/types.ts +++ b/packages/runed/src/lib/internal/types.ts @@ -1,6 +1,8 @@ export type Getter = () => T; export type MaybeGetter = T | Getter; export type MaybeElementGetter = MaybeGetter; +export type MaybeElement = HTMLElement | SVGElement | undefined | null; + export type Setter = (value: T) => void; export type Expand = T extends infer U ? { [K in keyof U]: U[K] } : never; export type WritableProperties = { diff --git a/packages/runed/src/lib/internal/utils/dom.ts b/packages/runed/src/lib/internal/utils/dom.ts index 442e16df..130b09e9 100644 --- a/packages/runed/src/lib/internal/utils/dom.ts +++ b/packages/runed/src/lib/internal/utils/dom.ts @@ -1,3 +1,5 @@ +import { defaultDocument } from "../configurable-globals.js"; + /** * Handles getting the active element in a document or shadow root. * If the active element is within a shadow root, it will traverse the shadow root @@ -18,3 +20,27 @@ export function getActiveElement(document: DocumentOrShadowRoot): HTMLElement | return activeElement; } + +/** + * Returns the owner document of a given element. + * + * @param node The element to get the owner document from. + * @returns + */ +export function getOwnerDocument( + node: Element | null | undefined, + fallback = defaultDocument +): Document | undefined { + return node?.ownerDocument ?? fallback; +} + +/** + * Checks if an element is or is contained by another element. + * + * @param node The element to check if it or its descendants contain the target element. + * @param target The element to check if it is contained by the node. + * @returns + */ +export function isOrContainsTarget(node: Element, target: Element) { + return node === target || node.contains(target); +} diff --git a/packages/runed/src/lib/internal/utils/is.ts b/packages/runed/src/lib/internal/utils/is.ts index 3adbbf09..11e6d965 100644 --- a/packages/runed/src/lib/internal/utils/is.ts +++ b/packages/runed/src/lib/internal/utils/is.ts @@ -5,3 +5,7 @@ export function isFunction(value: unknown): value is (...args: unknown[]) => unk export function isObject(value: unknown): value is Record { return value !== null && typeof value === "object"; } + +export function isElement(value: unknown): value is Element { + return value instanceof Element; +} diff --git a/packages/runed/src/lib/utilities/index.ts b/packages/runed/src/lib/utilities/index.ts index 41264567..ad65facf 100644 --- a/packages/runed/src/lib/utilities/index.ts +++ b/packages/runed/src/lib/utilities/index.ts @@ -1,4 +1,5 @@ export * from "./activeElement/index.js"; +export * from "./onClickOutside/index.js"; export * from "./useDebounce/index.js"; export * from "./ElementSize/index.js"; export * from "./useEventListener/index.js"; diff --git a/packages/runed/src/lib/utilities/onClickOutside/index.ts b/packages/runed/src/lib/utilities/onClickOutside/index.ts new file mode 100644 index 00000000..e5dbf1be --- /dev/null +++ b/packages/runed/src/lib/utilities/onClickOutside/index.ts @@ -0,0 +1 @@ +export * from "./onClickOutside.svelte.js"; diff --git a/packages/runed/src/lib/utilities/onClickOutside/onClickOutside.svelte.ts b/packages/runed/src/lib/utilities/onClickOutside/onClickOutside.svelte.ts new file mode 100644 index 00000000..eee0c46d --- /dev/null +++ b/packages/runed/src/lib/utilities/onClickOutside/onClickOutside.svelte.ts @@ -0,0 +1,226 @@ +import { + defaultWindow, + type ConfigurableDocument, + type ConfigurableWindow, +} from "$lib/internal/configurable-globals.js"; +import type { MaybeElementGetter } from "$lib/internal/types.js"; +import { getActiveElement, getOwnerDocument, isOrContainsTarget } from "$lib/internal/utils/dom.js"; +import { addEventListener } from "$lib/internal/utils/event.js"; +import { noop } from "$lib/internal/utils/function.js"; +import { isElement } from "$lib/internal/utils/is.js"; +import { sleep } from "$lib/internal/utils/sleep.js"; +import { extract } from "../extract/extract.svelte.js"; +import { useDebounce } from "../useDebounce/useDebounce.svelte.js"; +import { watch } from "../watch/watch.svelte.js"; + +export type OnClickOutsideOptions = ConfigurableWindow & + ConfigurableDocument & { + /** + * Whether the click outside handler is enabled by default or not. + * If set to false, the handler will not be active until enabled by + * calling the returned `start` function + * + * @default true + */ + immediate?: boolean; + + /** + * Controls whether focus events from iframes trigger the callback. + * + * Since iframe click events don't bubble to the parent document, + * you may want to enable this if you need to detect when users + * interact with iframe content. + * + * @default false + */ + detectIframe?: boolean; + }; + +/** + * A utility that calls a given callback when a click event occurs outside of + * a specified container element. + * + * @template T - The type of the container element, defaults to HTMLElement. + * @param {MaybeElementGetter} container - The container element or a getter function that returns the container element. + * @param {() => void} callback - The callback function to call when a click event occurs outside of the container. + * @param {OnClickOutsideOptions} [opts={}] - Optional configuration object. + * @param {ConfigurableDocument} [opts.document=defaultDocument] - The document object to use, defaults to the global document. + * @param {boolean} [opts.immediate=true] - Whether the click outside handler is enabled by default or not. + * @param {boolean} [opts.detectIframe=false] - Controls whether focus events from iframes trigger the callback. + * + * @example + * ```svelte + * + * + *
+ * Inside + *
+ * + * + * + * Enabled: {clickOutside.enabled} + *``` + * @see {@link https://runed.dev/docs/utilities/on-click-outside} + */ +export function onClickOutside( + container: MaybeElementGetter, + callback: (event: PointerEvent | FocusEvent) => void, + opts: OnClickOutsideOptions = {} +) { + const { window = defaultWindow, immediate = true, detectIframe = false } = opts; + const document = opts.document ?? window?.document; + const node = $derived(extract(container)); + const nodeOwnerDocument = $derived(getOwnerDocument(node, document)); + + let enabled = $state(immediate); + let pointerDownIntercepted = false; + let removeClickListener = noop; + let removeListeners = noop; + + const handleClickOutside = useDebounce((e: PointerEvent) => { + if (!node || !nodeOwnerDocument) { + removeClickListener(); + return; + } + + if (pointerDownIntercepted === true || !isValidEvent(e, node, nodeOwnerDocument)) { + removeClickListener(); + return; + } + + if (e.pointerType === "touch") { + /** + * If the pointer type is touch, we add a listener to wait for the click + * event that will follow the pointerdown event if the user interacts in a way + * that would trigger a click event. + * + * This prevents us from prematurely calling the callback if the user is simply + * scrolling or dragging the page. + */ + removeClickListener(); + removeClickListener = addEventListener(nodeOwnerDocument, "click", () => callback(e), { + once: true, + }); + } else { + /** + * If the pointer type is not touch, we can directly call the callback function + * as the interaction is likely a mouse or pen input which does not require + * additional handling. + */ + callback(e); + } + }, 10); + + function addListeners() { + if (!nodeOwnerDocument || !window || !node) return noop; + const events = [ + /** + * CAPTURE INTERACTION START + * Mark the pointerdown event as intercepted to indicate that an interaction + * has started. This helps in distinguishing between valid and invalid events. + */ + addEventListener( + nodeOwnerDocument, + "pointerdown", + (e) => { + if (isValidEvent(e, node, nodeOwnerDocument)) { + pointerDownIntercepted = true; + } + }, + true + ), + /** + * BUBBLE INTERACTION START + * Mark the pointerdown event as non-intercepted. Debounce `handleClickOutside` to + * avoid prematurely checking if other events were intercepted. + */ + addEventListener(nodeOwnerDocument, "pointerdown", (e) => { + pointerDownIntercepted = false; + handleClickOutside(e); + }), + ]; + if (detectIframe) { + events.push( + /** + * DETECT IFRAME INTERACTIONS + * + * We add a blur event listener to the window to detect when the user + * interacts with an iframe. If the active element is an iframe and it + * is not a descendant of the container, we call the callback function. + */ + addEventListener(window, "blur", async (e) => { + await sleep(); + const activeElement = getActiveElement(nodeOwnerDocument); + if (activeElement?.tagName === "IFRAME" && !isOrContainsTarget(node, activeElement)) { + callback(e); + } + }) + ); + } + return () => { + for (const event of events) { + event(); + } + }; + } + + function cleanup() { + pointerDownIntercepted = false; + handleClickOutside.cancel(); + removeClickListener(); + removeListeners(); + } + + watch([() => enabled, () => node], ([enabled$, node$]) => { + if (enabled$ && node$) { + removeListeners(); + removeListeners = addListeners(); + } else { + cleanup(); + } + }); + + $effect(() => { + return () => { + cleanup(); + }; + }); + + return { + /** Stop listening for click events outside the container. */ + stop: () => (enabled = false), + /** Start listening for click events outside the container. */ + start: () => (enabled = true), + /** Whether the click outside handler is currently enabled or not. */ + get enabled() { + return enabled; + }, + }; +} + +function isValidEvent(e: PointerEvent, container: Element, defaultDocument: Document): boolean { + if ("button" in e && e.button > 0) return false; + const target = e.target; + if (!isElement(target)) return false; + const ownerDocument = getOwnerDocument(target, defaultDocument); + if (!ownerDocument) return false; + // handle the case where a user may have pressed a pseudo element by + // checking the bounding rect of the container + if (target === container) { + const rect = container.getBoundingClientRect(); + const wasInsideClick = + rect.top <= e.clientY && + e.clientY <= rect.top + rect.height && + rect.left <= e.clientX && + e.clientX <= rect.left + rect.width; + return !wasInsideClick; + } + return ownerDocument.documentElement.contains(target) && !isOrContainsTarget(container, target); +} diff --git a/packages/runed/src/lib/utilities/onClickOutside/onClickOutside.test.svelte.ts b/packages/runed/src/lib/utilities/onClickOutside/onClickOutside.test.svelte.ts new file mode 100644 index 00000000..ecc26ba0 --- /dev/null +++ b/packages/runed/src/lib/utilities/onClickOutside/onClickOutside.test.svelte.ts @@ -0,0 +1,278 @@ +import { describe, expect, vi, beforeEach, afterEach } from "vitest"; +import { tick } from "svelte"; +import { onClickOutside } from "./onClickOutside.svelte.js"; +import { testWithEffect } from "$lib/test/util.svelte.js"; + +describe("onClickOutside", () => { + let container: HTMLDivElement; + let innerButton: HTMLButtonElement; + let outsideButton: HTMLButtonElement; + let callbackFn: ReturnType; + + class MockPointerEvent extends Event { + clientX: number; + clientY: number; + pointerType: string; + button: number; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(type: string, options: any = {}) { + super(type, { bubbles: true, ...options }); + this.clientX = options.clientX ?? 0; + this.clientY = options.clientY ?? 0; + this.pointerType = options.pointerType ?? "mouse"; + this.button = options.button ?? 0; + } + } + + const PointerEvent = globalThis.PointerEvent ?? MockPointerEvent; + + beforeEach(() => { + vi.useFakeTimers(); + + container = document.createElement("div"); + innerButton = document.createElement("button"); + outsideButton = document.createElement("button"); + + document.body.appendChild(container); + document.body.appendChild(outsideButton); + container.appendChild(innerButton); + + container.getBoundingClientRect = vi.fn(() => ({ + height: 100, + width: 100, + top: 50, + left: 50, + bottom: 150, + right: 150, + x: 50, + y: 50, + toJSON: vi.fn(), + })); + + callbackFn = vi.fn(); + }); + + afterEach(() => { + document.body.removeChild(container); + document.body.removeChild(outsideButton); + // Remove any iframes that might have been added + document.querySelectorAll("iframe").forEach((iframe) => iframe.remove()); + vi.clearAllMocks(); + vi.useRealTimers(); + }); + + const createPointerEvent = (type: string, options: Partial = {}) => { + return new PointerEvent(type, { + bubbles: true, + cancelable: true, + clientX: 10, + clientY: 10, + pointerType: "mouse", + button: 0, + ...options, + }); + }; + + const advanceTimers = async () => { + await vi.advanceTimersByTimeAsync(10); // Match the debounce time + await tick(); + }; + + testWithEffect("starts enabled by default", async () => { + onClickOutside(() => container, callbackFn); + await tick(); + + outsideButton.dispatchEvent(createPointerEvent("pointerdown")); + await advanceTimers(); + + expect(callbackFn).toHaveBeenCalledOnce(); + }); + + testWithEffect("respects `immediate` option", async () => { + const controls = onClickOutside(() => container, callbackFn, { immediate: false }); + await tick(); + + outsideButton.dispatchEvent(createPointerEvent("pointerdown")); + await advanceTimers(); + expect(callbackFn).not.toHaveBeenCalled(); + + controls.start(); + await tick(); + + outsideButton.dispatchEvent(createPointerEvent("pointerdown")); + await advanceTimers(); + expect(callbackFn).toHaveBeenCalledOnce(); + }); + + testWithEffect("handles touch events with click confirmation", async () => { + onClickOutside(() => container, callbackFn); + await tick(); + + outsideButton.dispatchEvent(createPointerEvent("pointerdown", { pointerType: "touch" })); + await advanceTimers(); + + // Without the click event, callback shouldn't be called yet + expect(callbackFn).not.toHaveBeenCalled(); + + outsideButton.dispatchEvent(new Event("click", { bubbles: true })); + await advanceTimers(); + + expect(callbackFn).toHaveBeenCalledOnce(); + }); + + testWithEffect("debounces rapid pointer events", async () => { + onClickOutside(() => container, callbackFn); + await tick(); + + outsideButton.dispatchEvent(createPointerEvent("pointerdown")); + outsideButton.dispatchEvent(createPointerEvent("pointerdown")); + outsideButton.dispatchEvent(createPointerEvent("pointerdown")); + + await advanceTimers(); + + // due to debouncing, should only be called once + expect(callbackFn).toHaveBeenCalledOnce(); + }); + + testWithEffect("can be stopped and started", async () => { + const controls = onClickOutside(() => container, callbackFn); + await tick(); + + // Initial state (enabled) + outsideButton.dispatchEvent(createPointerEvent("pointerdown")); + await advanceTimers(); + expect(callbackFn).toHaveBeenCalledTimes(1); + + // Stop listening + controls.stop(); + await tick(); + + outsideButton.dispatchEvent(createPointerEvent("pointerdown")); + await advanceTimers(); + expect(callbackFn).toHaveBeenCalledTimes(1); + + // Start listening again + controls.start(); + await tick(); + + outsideButton.dispatchEvent(createPointerEvent("pointerdown")); + await advanceTimers(); + expect(callbackFn).toHaveBeenCalledTimes(2); + }); + + testWithEffect("respects button type in pointer events", async () => { + onClickOutside(() => container, callbackFn); + await tick(); + + // Right click should be ignored + outsideButton.dispatchEvent(createPointerEvent("pointerdown", { button: 2 })); + await advanceTimers(); + expect(callbackFn).not.toHaveBeenCalled(); + + // Left click should trigger callback + outsideButton.dispatchEvent(createPointerEvent("pointerdown", { button: 0 })); + await advanceTimers(); + expect(callbackFn).toHaveBeenCalledOnce(); + }); + + testWithEffect("handles pointer down interception correctly", async () => { + onClickOutside(() => container, callbackFn); + await tick(); + + // Simulate captured pointerdown (should be intercepted) + container.dispatchEvent( + createPointerEvent("pointerdown", { + clientX: 75, + clientY: 75, + }) + ); + await advanceTimers(); + expect(callbackFn).not.toHaveBeenCalled(); + + // Simulate non-intercepted pointerdown + outsideButton.dispatchEvent(createPointerEvent("pointerdown")); + await advanceTimers(); + expect(callbackFn).toHaveBeenCalledOnce(); + }); + + testWithEffect("handles pseudo-element clicks outside element bounds", async () => { + const dialog = document.createElement("dialog"); + document.body.appendChild(dialog); + + onClickOutside(() => dialog, callbackFn); + await tick(); + + dialog.getBoundingClientRect = vi.fn(() => ({ + height: 200, + width: 200, + top: 100, + left: 100, + bottom: 300, + right: 300, + x: 100, + y: 100, + toJSON: vi.fn(), + })); + + // click inside dialog's actual bounds + const insideDialogClick = createPointerEvent("pointerdown", { + clientX: 150, + clientY: 150, + }); + Object.defineProperty(insideDialogClick, "target", { value: dialog }); + document.dispatchEvent(insideDialogClick); + await advanceTimers(); + + expect(callbackFn).not.toHaveBeenCalled(); + + // click outside dialog's bounds but with dialog as target (simulating pseudo-element) + const pseudoElementClick = createPointerEvent("pointerdown", { + clientX: 400, + clientY: 400, + }); + Object.defineProperty(pseudoElementClick, "target", { value: dialog }); + document.dispatchEvent(pseudoElementClick); + await advanceTimers(); + + expect(callbackFn).toHaveBeenCalledOnce(); + + dialog.remove(); + }); + + testWithEffect("ignores iframe interactions by default (detectIframe: false)", async () => { + let iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + + onClickOutside(() => container, callbackFn); + await tick(); + + window.dispatchEvent(new Event("blur", { bubbles: true })); + await tick(); + vi.spyOn(document, "activeElement", "get").mockReturnValue(iframe); + await vi.runAllTimersAsync(); + + expect(callbackFn).not.toHaveBeenCalled(); + + document.body.removeChild(iframe); + vi.restoreAllMocks(); + }); + + testWithEffect("detects iframe interactions when detectIframe is true", async () => { + let iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + + onClickOutside(() => container, callbackFn, { detectIframe: true }); + await tick(); + + window.dispatchEvent(new Event("blur", { bubbles: true })); + await tick(); + vi.spyOn(document, "activeElement", "get").mockReturnValue(iframe); + await vi.runAllTimersAsync(); + + expect(callbackFn).toHaveBeenCalledOnce(); + + document.body.removeChild(iframe); + vi.restoreAllMocks(); + }); +}); diff --git a/sites/docs/src/content/utilities/on-click-outside.md b/sites/docs/src/content/utilities/on-click-outside.md new file mode 100644 index 00000000..1d66aca1 --- /dev/null +++ b/sites/docs/src/content/utilities/on-click-outside.md @@ -0,0 +1,165 @@ +--- +title: onClickOutside +description: Handle clicks outside of a specified element. +category: Sensors +--- + + + +`onClickOutside` detects clicks that occur outside a specified element's boundaries and executes a +callback function. It's commonly used for dismissible dropdowns, modals, and other interactive +components. + +## Demo + + + +## Basic Usage + +```svelte + + +
+ +
+ +``` + +## Advanced Usage + +### Controlled Listener + +The function returns control methods to programmatically manage the listener, `start` and `stop` and +a reactive read-only property `enabled` to check the current status of the listeners. + +```svelte + + + + +
+ +
+
+``` + +Here's an example of using `onClickOutside` with a ``: + + + +## Options + + + +Whether the click outside handler is enabled by default or not. If set to `false`, the handler will +not be active until enabled by calling the returned `start` function. + + + + + +Controls whether focus events from iframes trigger the callback. Since iframe click events don't +bubble to the parent document, you may want to enable this if you need to detect when users interact +with iframe content. + + + + + +The document object to use, defaults to the global document. + + + + + +The window object to use, defaults to the global window. + + + +## Type Definitions + +```ts +export type OnClickOutsideOptions = ConfigurableWindow & + ConfigurableDocument & { + /** + * Whether the click outside handler is enabled by default or not. + * If set to false, the handler will not be active until enabled by + * calling the returned `start` function + * + * @default true + */ + immediate?: boolean; + /** + * Controls whether focus events from iframes trigger the callback. + * + * Since iframe click events don't bubble to the parent document, + * you may want to enable this if you need to detect when users + * interact with iframe content. + * + * @default false + */ + detectIframe?: boolean; + }; +/** + * A utility that calls a given callback when a click event occurs outside of + * a specified container element. + * + * @template T - The type of the container element, defaults to HTMLElement. + * @param {MaybeElementGetter} container - The container element or a getter function that returns the container element. + * @param {() => void} callback - The callback function to call when a click event occurs outside of the container. + * @param {OnClickOutsideOptions} [opts={}] - Optional configuration object. + * @param {ConfigurableDocument} [opts.document=defaultDocument] - The document object to use, defaults to the global document. + * @param {boolean} [opts.immediate=true] - Whether the click outside handler is enabled by default or not. + * @param {boolean} [opts.detectIframe=false] - Controls whether focus events from iframes trigger the callback. + * + * @see {@link https://runed.dev/docs/utilities/on-click-outside} + */ +export declare function onClickOutside( + container: MaybeElementGetter, + callback: (event: PointerEvent | FocusEvent) => void, + opts?: OnClickOutsideOptions +): { + /** Stop listening for click events outside the container. */ + stop: () => boolean; + /** Start listening for click events outside the container. */ + start: () => boolean; + /** Whether the click outside handler is currently enabled or not. */ + readonly enabled: boolean; +}; +``` diff --git a/sites/docs/src/lib/components/demos/on-click-outside-dialog.svelte b/sites/docs/src/lib/components/demos/on-click-outside-dialog.svelte new file mode 100644 index 00000000..8d7303b5 --- /dev/null +++ b/sites/docs/src/lib/components/demos/on-click-outside-dialog.svelte @@ -0,0 +1,44 @@ + + + + + +
+

This is a dialog.

+

+ Lorem, ipsum dolor sit amet consectetur adipisicing elit. Neque sunt aut sit exercitationem + deleniti doloremque quo quasi, expedita omnis dicta eaque, eveniet nesciunt nobis sint + atque? Praesentium facilis officiis perferendis. +

+ + +
+
+
diff --git a/sites/docs/src/lib/components/demos/on-click-outside.svelte b/sites/docs/src/lib/components/demos/on-click-outside.svelte new file mode 100644 index 00000000..2e433c05 --- /dev/null +++ b/sites/docs/src/lib/components/demos/on-click-outside.svelte @@ -0,0 +1,35 @@ + + + +
+ + container + +

{containerText}

+

+ Status: {clickOutside.enabled ? "Enabled" : "Disabled"} +

+
+ + + +
+
+
diff --git a/sites/docs/src/routes/api/search.json/search.json b/sites/docs/src/routes/api/search.json/search.json index edc7ac12..36670e8e 100644 --- a/sites/docs/src/routes/api/search.json/search.json +++ b/sites/docs/src/routes/api/search.json/search.json @@ -1 +1 @@ -[{"title":"Getting Started","href":"/docs/getting-started","description":"Learn how to install and use Runed in your projects.","content":"Installation Install Runed using your favorite package manager: npm install runed Usage Import one of the utilities you need to either a .svelte or .svelte.js|ts file and start using it: import { activeElement } from \"runed\"; let inputElement = $state(); {#if activeElement.current === inputElement} The input element is active! {/if} or import { activeElement } from \"runed\"; function logActiveElement() { $effect(() => { console.log(\"Active element is \", activeElement.current); }); } logActiveElement(); `"},{"title":"Introduction","href":"/docs/index","description":"Runes are magic, but what good is magic if you don't have a wand?","content":"Runed is a collection of utilities for Svelte 5 that make composing powerful applications and libraries a breeze, leveraging the power of $2. Why Runed? Svelte 5 Runes unlock immense power by providing a set of primitives that allow us to build impressive applications and libraries with ease. However, building complex applications often requires more than just the primitives provided by Svelte Runes. Runed takes those primitives to the next level by providing: Powerful Utilities**: A set of carefully crafted utility functions and classes that simplify common tasks and reduce boilerplate. Collective Efforts**: We often find ourselves writing the same utility functions over and over again. Runed aims to provide a single source of truth for these utilities, allowing the community to contribute, test, and benefit from them. Consistency**: A consistent set of APIs and behaviors across all utilities, so you can focus on building your projects instead of constantly learning new APIs. Reactivity First**: Powered by Svelte 5's new reactivity system, Runed utilities are designed to handle reactive state and side effects with ease. Type Safety**: Full TypeScript support to catch errors early and provide a better developer experience. Ideas and Principles Embrace the Magic of Runes Svelte Runes are a powerful new paradigm. Runed fully embraces this concept and explores its potential. Our goal is to make working with Runes feel as natural and intuitive as possible. Enhance, Don't Replace Runed is not here to replace Svelte's core functionality, but to enhance and extend it. Our utilities should feel like a natural extension of Svelte, not a separate framework. Progressive Complexity Simple things should be simple, complex things should be possible. Runed provides easy-to-use defaults while allowing for advanced customization when needed. Open Source and Community Collaboration Runed is an open-source, MIT licensed project that welcomes all forms of contributions from the community. Whether it's bug reports, feature requests, or code contributions, your input will help make Runed the best it can be."},{"title":"activeElement","href":"/docs/utilities/active-element","description":"Track and access the currently focused DOM element","content":" import Demo from '$lib/components/demos/active-element.svelte'; activeElement provides reactive access to the currently focused DOM element in your application, similar to document.activeElement but with reactive updates. Updates synchronously with DOM focus changes Returns null when no element is focused Safe to use with SSR (Server-Side Rendering) Lightweight alternative to manual focus tracking If you need to provide a custom document / shadowRoot, you can use the $2 utility instead, which provides a more flexible API. Demo Usage import { activeElement } from \"runed\"; Currently active element: {activeElement.current?.localName ?? \"No active element found\"} Type Definition interface ActiveElement { readonly current: Element | null; } `"},{"title":"AnimationFrames","href":"/docs/utilities/animation-frames","description":"A wrapper for requestAnimationFrame with FPS control and frame metrics","content":" import Demo from '$lib/components/demos/animation-frames.svelte'; AnimationFrames provides a declarative API over the browser's $2, offering FPS limiting capabilities and frame metrics while handling cleanup automatically. Demo Usage import { AnimationFrames } from \"runed\"; import { Slider } from \"../ui/slider\"; // Check out shadcn-svelte! let frames = $state(0); let fpsLimit = $state(10); let delta = $state(0); const animation = new AnimationFrames( (args) => { frames++; delta = args.delta; }, { fpsLimit: () => fpsLimit } ); const stats = $derived( Frames: ${frames}\\nFPS: ${animation.fps.toFixed(0)}\\nDelta: ${delta.toFixed(0)}ms ); {stats} {animation.running ? \"Stop\" : \"Start\"} FPS limit: {fpsLimit}{fpsLimit === 0 ? \" (not limited)\" : \"\"} (fpsLimit = value[0] ?? 0)} min={0} max={144} /> `"},{"title":"Context","href":"/docs/utilities/context","description":"A wrapper around Svelte's Context API that provides type safety and improved ergonomics for sharing data between components.","content":" import { Steps, Step, Callout } from '@svecodocs/kit'; Context allows you to pass data through the component tree without explicitly passing props through every level. It's useful for sharing data that many components need, like themes, authentication state, or localization preferences. The Context class provides a type-safe way to define, set, and retrieve context values. Usage Creating a Context First, create a Context instance with the type of value it will hold: import { Context } from \"runed\"; export const myTheme = new Context(\"theme\"); Creating a Context instance only defines the context - it doesn't actually set any value. The value passed to the constructor (\"theme\" in this example) is just an identifier used for debugging and error messages. Think of this step as creating a \"container\" that will later hold your context value. The container is typed (in this case to only accept \"light\" or \"dark\" as values) but remains empty until you explicitly call myTheme.set() during component initialization. This separation between defining and setting context allows you to: Keep context definitions in separate files Reuse the same context definition across different parts of your app Maintain type safety throughout your application Set different values for the same context in different component trees Setting Context Values Set the context value in a parent component during initialization. import { myTheme } from \"./context\"; let { data, children } = $props(); myTheme.set(data.theme); {@render children?.()} Context must be set during component initialization, similar to lifecycle functions like onMount. You cannot set context inside event handlers or callbacks. Reading Context Values Child components can access the context using get() or getOr() import { myTheme } from \"./context\"; const theme = myTheme.get(); // or with a fallback value if the context is not set const theme = myTheme.getOr(\"light\"); Type Definition class Context { /** @param name The name of the context. This is used for generating the context key and error messages. */ constructor(name: string) {} /** The key used to get and set the context. * It is not recommended to use this value directly. Instead, use the methods provided by this class. */ get key(): symbol; /** Checks whether this has been set in the context of a parent component. * Must be called during component initialization. */ exists(): boolean; /** Retrieves the context that belongs to the closest parent component. * Must be called during component initialization. * @throws An error if the context does not exist. */ get(): TContext; /** Retrieves the context that belongs to the closest parent component, or the given fallback value if the context does not exist. * Must be called during component initialization. */ getOr(fallback: TFallback): TContext | TFallback; /** Associates the given value with the current component and returns it. * Must be called during component initialization. */ set(context: TContext): TContext; } `"},{"title":"Debounced","href":"/docs/utilities/debounced","description":"A wrapper over `useDebounce` that returns a debounced state.","content":" import Demo from '$lib/components/demos/debounced.svelte'; Demo Usage This is a simple wrapper over $2 that returns a debounced state. import { Debounced } from \"runed\"; let search = $state(\"\"); const debounced = new Debounced(() => search, 500); You searched for: {debounced.current} You may cancel the pending update, run it immediately, or set a new value. Setting a new value immediately also cancels any pending updates. let count = $state(0); const debounced = new Debounced(() => count, 500); count = 1; debounced.cancel(); // after a while... console.log(debounced.current); // Still 0! count = 2; console.log(debounced.current); // Still 0! debounced.setImmediately(count); console.log(debounced.current); // 2 count = 3; console.log(debounced.current); // 2 await debounced.updateImmediately(); console.log(debounced.current); // 3 `"},{"title":"ElementRect","href":"/docs/utilities/element-rect","description":"Track element dimensions and position reactively","content":" import Demo from '$lib/components/demos/element-rect.svelte'; ElementRect provides reactive access to an element's dimensions and position information, automatically updating when the element's size or position changes. Demo Usage import { ElementRect } from \"runed\"; let el = $state(); const rect = new ElementRect(() => el); Width: {rect.width} Height: {rect.height} {JSON.stringify(rect.current, null, 2)} Type Definition type Rect = Omit; interface ElementRectOptions { initialRect?: DOMRect; } class ElementRect { constructor(node: MaybeGetter, options?: ElementRectOptions); readonly current: Rect; readonly width: number; readonly height: number; readonly top: number; readonly left: number; readonly right: number; readonly bottom: number; readonly x: number; readonly y: number; } `"},{"title":"ElementSize","href":"/docs/utilities/element-size","description":"Track element dimensions reactively","content":" import Demo from '$lib/components/demos/element-size.svelte'; ElementSize provides reactive access to an element's width and height, automatically updating when the element's dimensions change. Similar to ElementRect but focused only on size measurements. Demo Usage import { ElementSize } from \"runed\"; let el = $state() as HTMLElement; const size = new ElementSize(() => el); Width: {size.width} Height: {size.height} Type Definition interface ElementSize { readonly width: number; readonly height: number; } `"},{"title":"FiniteStateMachine","href":"/docs/utilities/finite-state-machine","description":"Defines a strongly-typed finite state machine.","content":" import Demo from '$lib/components/demos/finite-state-machine.svelte'; Demo type MyStates = \"disabled\" | \"idle\" | \"running\"; type MyEvents = \"toggleEnabled\" | \"start\" | \"stop\"; const f = new FiniteStateMachine(\"disabled\", { disabled: { toggleEnabled: \"idle\" }, idle: { toggleEnabled: \"disabled\", start: \"running\" }, running: { _enter: () => { f.debounce(2000, \"stop\"); }, stop: \"idle\", toggleEnabled: \"disabled\" } }); Usage Finite state machines (often abbreviated as \"FSMs\") are useful for tracking and manipulating something that could be in one of many different states. It centralizes the definition of every possible state and the events that might trigger a transition from one state to another. Here is a state machine describing a simple toggle switch: import { FiniteStateMachine } from \"runed\"; type MyStates = \"on\" | \"off\"; type MyEvents = \"toggle\"; const f = new FiniteStateMachine(\"off\", { off: { toggle: \"on\" }, on: { toggle: \"off\" } }); The first argument to the FiniteStateMachine constructor is the initial state. The second argument is an object with one key for each state. Each state then describes which events are valid for that state, and which state that event should lead to. In the above example of a simple switch, there are two states (on and off). The toggle event in either state leads to the other state. You send events to the FSM using f.send. To send the toggle event, invoke f.send('toggle'). Actions Maybe you want fancier logic for an event handler, or you want to conditionally transition into another state. Instead of strings, you can use actions. An action is a function that returns a state. An action can receive parameters, and it can use those parameters to dynamically choose which state should come next. It can also prevent a state transition by returning nothing. type MyStates = \"on\" | \"off\" | \"cooldown\"; const f = new FiniteStateMachine(\"off\", { off: { toggle: () => { if (isTuesday) { // Switch can only turn on during Tuesdays return \"on\"; } // All other days, nothing is returned and state is unchanged. } }, on: { toggle: (heldMillis: number) => { // You can also dynamically return the next state! // Only turn off if switch is depressed for 3 seconds if (heldMillis > 3000) { return \"off\"; } } } }); Lifecycle methods You can define special handlers that are invoked whenever a state is entered or exited: const f = new FiniteStateMachine('off', { off: { toggle: 'on' _enter: (meta) => { console.log('switch is off') } _exit: (meta) => { console.log('switch is no longer off') } }, on: { toggle: 'off' _enter: (meta) => { console.log('switch is on') } _exit: (meta) => { console.log('switch is no longer on') } } }); The lifecycle methods are invoked with a metadata object containing some useful information: from: the name of the event that is being exited to: the name of the event that is being entered event: the name of the event which has triggered the transition args: (optional) you may pass additional metadata when invoking an action with f.send('theAction', additional, params, as, args) The _enter handler for the initial state is called upon creation of the FSM. It is invoked with both the from and event fields set to null. Wildcard handlers There is one special state used as a fallback: *. If you have the fallback state, and you attempt to send() an event that is not handled by the current state, then it will try to find a handler for that event on the * state before discarding the event: const f = new FiniteStateMachine('off', { off: { toggle: 'on' }, on: { toggle: 'off' } '*': { emergency: 'off' } }); // will always result in the switch turning off. f.send('emergency'); Debouncing Frequently, you want to transition to another state after some time has elapsed. To do this, use the debounce method: f.send(\"toggle\"); // turn on immediately f.debounce(5000, \"toggle\"); // turn off in 5000 milliseconds If you re-invoke debounce with the same event, it will cancel the existing timer and start the countdown over: // schedule a toggle in five seconds f.debounce(5000, \"toggle\"); // ... less than 5000ms elapses ... f.debounce(5000, \"toggle\"); // The second call cancels the original timer, and starts a new one You can also use debounce in both actions and lifecycle methods. In both of the following examples, the lightswitch will turn itself off five seconds after it was turned on: const f = new FiniteStateMachine(\"off\", { off: { toggle: () => { f.debounce(5000, \"toggle\"); return \"on\"; } }, on: { toggle: \"off\" } }); const f = new FiniteStateMachine(\"off\", { off: { toggle: \"on\" }, on: { toggle: \"off\", _enter: () => { f.debounce(5000, \"toggle\"); } } }); Notes FiniteStateMachine is a loving rewrite of $2. FSMs are ideal for representing many different kinds of systems and interaction patterns. FiniteStateMachine is an intentionally minimalistic implementation. If you're looking for a more powerful FSM library, $2 is an excellent library with more features — and a steeper learning curve."},{"title":"IsFocusWithin","href":"/docs/utilities/is-focus-within","description":"A utility that tracks whether any descendant element has focus within a specified container element.","content":" import Demo from '$lib/components/demos/is-focus-within.svelte'; IsFocusWithin reactively tracks focus state within a container element, updating automatically when focus changes. Demo Usage import { IsFocusWithin } from \"runed\"; let formElement = $state(); const focusWithinForm = new IsFocusWithin(() => formElement); Focus within form: {focusWithinForm.current} Submit Type Definition class IsFocusWithin { constructor(node: MaybeGetter); readonly current: boolean; } `"},{"title":"IsIdle","href":"/docs/utilities/is-idle","description":"Track if a user is idle and the last time they were active.","content":" import Demo from '$lib/components/demos/is-idle.svelte'; IsIdle tracks user activity and determines if they're idle based on a configurable timeout. It monitors mouse movement, keyboard input, and touch events to detect user interaction. Demo Usage import { AnimationFrames, IsIdle } from \"runed\"; const idle = new IsIdle({ timeout: 1000 }); Idle: {idle.current} Last active: {new Date(idle.lastActive).toLocaleTimeString()} Type Definitions interface IsIdleOptions { /** The events that should set the idle state to true * @default ['mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel'] */ events?: MaybeGetter; /** The timeout in milliseconds before the idle state is set to true. Defaults to 60 seconds. * @default 60000 */ timeout?: MaybeGetter; /** Detect document visibility changes * @default true */ detectVisibilityChanges?: MaybeGetter; /** The initial state of the idle property * @default false */ initialState?: boolean; } class IsIdle { constructor(options?: IsIdleOptions); readonly current: boolean; readonly lastActive: number; } `"},{"title":"IsInViewport","href":"/docs/utilities/is-in-viewport","description":"Track if an element is visible within the current viewport.","content":" import Demo from '$lib/components/demos/is-in-viewport.svelte'; IsInViewport uses the $2 utility to track if an element is visible within the current viewport. It accepts an element or getter that returns an element and an optional options object that aligns with the $2 utility options. Demo Usage import { IsInViewport } from \"runed\"; let targetNode = $state()!; const inViewport = new IsInViewport(() => targetNode); Target node Target node in viewport: {inViewport.current} Type Definition import { type UseIntersectionObserverOptions } from \"runed\"; export type IsInViewportOptions = UseIntersectionObserverOptions; export declare class IsInViewport { constructor(node: MaybeGetter, options?: IsInViewportOptions); get current(): boolean; } "},{"title":"IsMounted","href":"/docs/utilities/is-mounted","description":"A class that returns the mounted state of the component it's called in.","content":" import Demo from '$lib/components/demos/is-mounted.svelte'; Demo Usage import { IsMounted } from \"runed\"; const isMounted = new IsMounted(); Which is a shorthand for one of the following: import { onMount } from \"svelte\"; const isMounted = $state({ current: false }); onMount(() => { isMounted.current = true; }); or import { untrack } from \"svelte\"; const isMounted = $state({ current: false }); $effect(() => { untrack(() => (isMounted.current = true)); }); `"},{"title":"IsSupported","href":"/docs/utilities/is-supported","description":"Determine if a feature is supported by the environment before using it.","content":"Usage import { IsSupported } from \"runed\"; const isSupported = new IsSupported(() => navigator && \"geolocation\" in navigator); if (isSupported.current) { // Do something with the geolocation API } Type Definition class IsSupported { readonly current: boolean; } `"},{"title":"PersistedState","href":"/docs/utilities/persisted-state","description":"A reactive state manager that persists and synchronizes state across browser sessions and tabs using Web Storage APIs.","content":" import Demo from '$lib/components/demos/persisted-state.svelte'; import { Callout } from '@svecodocs/kit' PersistedState provides a reactive state container that automatically persists data to browser storage and optionally synchronizes changes across browser tabs in real-time. Demo You can refresh this page and/or open it in another tab to see the count state being persisted and synchronized across sessions and tabs. Usage Initialize PersistedState by providing a unique key and an initial value for the state. import { PersistedState } from \"runed\"; const count = new PersistedState(\"count\", 0); count.current++}>Increment count.current--}>Decrement (count.current = 0)}>Reset Count: {count.current} Configuration Options PersistedState includes an options object that allows you to customize the behavior of the state manager. const state = new PersistedState(\"user-preferences\", initialValue, { // Use sessionStorage instead of localStorage (default: 'local') storage: \"session\", // Disable cross-tab synchronization (default: true) syncTabs: false, // Custom serialization handlers serializer: { serialize: superjson.stringify, deserialize: superjson.parse } }); Storage Options 'local': Data persists until explicitly cleared 'session': Data persists until the browser session ends Cross-Tab Synchronization When syncTabs is enabled (default), changes are automatically synchronized across all browser tabs using the storage event. Custom Serialization Provide custom serialize and deserialize functions to handle complex data types: import superjson from \"superjson\"; // Example with Date objects const lastAccessed = new PersistedState(\"last-accessed\", new Date(), { serializer: { serialize: superjson.stringify, deserialize: superjson.parse } }); `"},{"title":"PressedKeys","href":"/docs/utilities/pressed-keys","description":"Tracks which keys are currently pressed","content":" import Demo from '$lib/components/demos/pressed-keys.svelte'; Demo Usage With an instance of PressedKeys, you can use the has method. const keys = new PressedKeys(); const isArrowDownPressed = $derived(keys.has(\"ArrowDown\")); const isCtrlAPressed = $derived(keys.has(\"Control\", \"a\")); Or get all of the currently pressed keys: const keys = new PressedKeys(); console.log(keys.all()); `"},{"title":"Previous","href":"/docs/utilities/previous","description":"A utility that tracks and provides access to the previous value of a reactive getter.","content":" import Demo from '$lib/components/demos/previous.svelte'; The Previous utility creates a reactive wrapper that maintains the previous value of a getter function. This is particularly useful when you need to compare state changes or implement transition effects. Demo Usage import { Previous } from \"runed\"; let count = $state(0); const previous = new Previous(() => count); count++}>Count: {count} Previous: {${previous.current}} Type Definition class Previous { constructor(getter: () => T); readonly current: T; // Previous value } `"},{"title":"StateHistory","href":"/docs/utilities/state-history","description":"Track state changes with undo/redo capabilities","content":" import Demo from '$lib/components/demos/state-history.svelte'; Demo Usage StateHistory tracks a getter's return value, logging each change into an array. A setter is also required to use the undo and redo functions. import { StateHistory } from \"runed\"; let count = $state(0); const history = new StateHistory(() => count, (c) => (count = c)); history.log[0]; // { snapshot: 0, timestamp: ... } Besides log, the returned object contains undo and redo functionality. import { useStateHistory } from \"runed\"; let count = $state(0); const history = new StateHistory(() => count, (c) => (count = c)); function format(ts: number) { return new Date(ts).toLocaleString(); } {count} count++}>Increment count--}>Decrement Undo Redo `"},{"title":"useActiveElement","href":"/docs/utilities/use-active-element","description":"Get a reactive reference to the currently focused element in the document.","content":" import Demo from '$lib/components/demos/use-active-element.svelte'; import { PropField } from '@svecodocs/kit' useActiveElement is used to get the currently focused element in the document. If you don't need to provide a custom document / shadowRoot, you can use the $2 state instead, as it provides a simpler API. This utility behaves similarly to document.activeElement but with additional features such as: Updates synchronously with DOM focus changes Returns null when no element is focused Safe to use with SSR (Server-Side Rendering) Lightweight alternative to manual focus tracking Demo Usage import { useActiveElement } from \"runed\"; const activeElement = useActiveElement(); {#if activeElement.current} The active element is: {activeElement.current.localName} {:else} No active element found {/if} Options The following options can be passed via the first argument to useActiveElement: The document or shadow root to track focus within. The window to use for focus tracking. "},{"title":"useDebounce","href":"/docs/utilities/use-debounce","description":"A higher-order function that debounces the execution of a function.","content":" import Demo from '$lib/components/demos/use-debounce.svelte'; useDebounce is a utility function that creates a debounced version of a callback function. Debouncing prevents a function from being called too frequently by delaying its execution until after a specified duration of inactivity. Demo Usage import { useDebounce } from \"runed\"; let count = $state(0); let logged = $state(\"\"); let isFirstTime = $state(true); let debounceDuration = $state(1000); const logCount = useDebounce( () => { if (isFirstTime) { isFirstTime = false; logged = You pressed the button ${count} times!; } else { logged = You pressed the button ${count} times since last time!; } count = 0; }, () => debounceDuration ); function ding() { count++; logCount(); } DING DING DING Run now Cancel message {logged || \"Press the button!\"} `"},{"title":"useEventListener","href":"/docs/utilities/use-event-listener","description":"A function that attaches an automatically disposed event listener.","content":" import Demo from '$lib/components/demos/use-event-listener.svelte'; Demo Usage The useEventListener function is particularly useful for attaching event listeners to elements you don't directly control. For instance, if you need to listen for events on the document body or window and can't use ``, or if you receive an element reference from a parent component. Example: Tracking Clicks on the Document // ClickLogger.ts import { useEventListener } from \"runed\"; export class ClickLogger { #clicks = $state(0); constructor() { useEventListener( () => document.body, \"click\", () => this.#clicks++ ); } get clicks() { return this.#clicks; } } This ClickLogger class tracks the number of clicks on the document body using the useEventListener function. Each time a click occurs, the internal counter increments. Svelte Component Usage import { ClickLogger } from \"./ClickLogger.ts\"; const logger = new ClickLogger(); You've clicked the document {logger.clicks} {logger.clicks === 1 ? \"time\" : \"times\"} In the component above, we create an instance of the ClickLogger class to monitor clicks on the document. The displayed text updates dynamically based on the recorded click count. Key Points Automatic Cleanup:** The event listener is removed automatically when the component is destroyed or when the element reference changes. Lazy Initialization:** The target element can be defined using a function, enabling flexible and dynamic behavior. Convenient for Global Listeners:** Ideal for scenarios where attaching event listeners directly to the DOM elements is cumbersome or impractical."},{"title":"useGeolocation","href":"/docs/utilities/use-geolocation","description":"Reactive access to the browser's Geolocation API.","content":" import Demo from '$lib/components/demos/use-geolocation.svelte'; useGeolocation is a reactive wrapper around the browser's $2. Demo Usage import { useGeolocation } from \"runed\"; const location = useGeolocation(); Coords: {JSON.stringify(location.coords, null, 2)} Located at: {location.locatedAt} Error: {JSON.stringify(location.error, null, 2)} Is Supported: {location.isSupported} Pause Resume Type Definitions type UseGeolocationOptions = Partial & { /** Whether to start the watcher immediately upon creation. If set to false, the watcher will only start tracking the position when resume() is called. * @defaultValue true */ immediate?: boolean; }; type UseGeolocationReturn = { readonly isSupported: boolean; readonly coords: Omit; readonly locatedAt: number | null; readonly error: GeolocationPositionError | null; readonly isPaused: boolean; pause: () => void; resume: () => void; }; `"},{"title":"useIntersectionObserver","href":"/docs/utilities/use-intersection-observer","description":"Watch for intersection changes of a target element.","content":" import Demo from '$lib/components/demos/use-intersection-observer.svelte'; import { Callout } from '@svecodocs/kit' Demo Usage With a reference to an element, you can use the useIntersectionObserver utility to watch for intersection changes of the target element. import { useIntersectionObserver } from \"runed\"; let target = $state(null); let root = $state(null); let isIntersecting = $state(false); useIntersectionObserver( () => target, (entries) => { const entry = entries[0]; if (!entry) return; isIntersecting = entry.isIntersecting; }, { root: () => root } ); {#if isIntersecting} Target is intersecting {:else} Target is not intersecting {/if} Pause You can pause the intersection observer at any point by calling the pause method. const observer = useIntersectionObserver(/* ... */); observer.pause(); Resume You can resume the intersection observer at any point by calling the resume method. const observer = useIntersectionObserver(/* ... */); observer.resume(); Stop You can stop the intersection observer at any point by calling the stop method. const observer = useIntersectionObserver(/* ... */); observer.stop(); isActive You can check if the intersection observer is active by checking the isActive property. This property cannot be destructured as it is a getter. You must access it directly from the observer. const observer = useIntersectionObserver(/* ... */); if (observer.isActive) { // do something } `"},{"title":"useMutationObserver","href":"/docs/utilities/use-mutation-observer","description":"Observe changes in an element","content":" import Demo from '$lib/components/demos/use-mutation-observer.svelte'; Demo Usage With a reference to an element, you can use the useMutationObserver hook to observe changes in the element. import { useMutationObserver } from \"runed\"; let el = $state(null); const messages = $state([]); let className = $state(\"\"); let style = $state(\"\"); useMutationObserver( () => el, (mutations) => { const mutation = mutations[0]; if (!mutation) return; messages.push(mutation.attributeName!); }, { attributes: true } ); setTimeout(() => { className = \"text-brand\"; }, 1000); setTimeout(() => { style = \"font-style: italic;\"; }, 1500); {#each messages as text} Mutation Attribute: {text} {:else} No mutations yet {/each} You can stop the mutation observer at any point by calling the stop method. const { stop } = useMutationObserver(/* ... */); stop(); `"},{"title":"useResizeObserver","href":"/docs/utilities/use-resize-observer","description":"Detects changes in the size of an element","content":" import Demo from '$lib/components/demos/use-resize-observer.svelte'; Demo Usage With a reference to an element, you can use the useResizeObserver utility to detect changes in the size of an element. import { useResizeObserver } from \"runed\"; let el = $state(null); let text = $state(\"\"); useResizeObserver( () => el, (entries) => { const entry = entries[0]; if (!entry) return; const { width, height } = entry.contentRect; text = width: ${width};\\nheight: ${height};; } ); You can stop the resize observer at any point by calling the stop method. const { stop } = useResizeObserver(/* ... */); stop(); `"},{"title":"watch","href":"/docs/utilities/watch","description":"Watch for changes and run a callback","content":"Runes provide a handy way of running a callback when reactive values change: $2. It automatically detects when inner values change, and re-runs the callback. $effect is great, but sometimes you want to manually specify which values should trigger the callback. Svelte provides an untrack function, allowing you to specify that a dependency shouldn't be tracked, but it doesn't provide a way to say that only certain values should be tracked. watch does exactly that. It accepts a getter function, which returns the dependencies of the effect callback. Usage watch Runs a callback whenever one of the sources change. import { watch } from \"runed\"; let count = $state(0); watch(() => count, () => { console.log(count); } ); The callback receives two arguments: The current value of the sources, and the previous value. let count = $state(0); watch(() => count, (curr, prev) => { console.log(count is ${curr}, was ${prev}); } ); You can also send in an array of sources: let age = $state(20); let name = $state(\"bob\"); watch([() => age, () => name], ([age, name], [prevAge, prevName]) => { // ... } watch also accepts an options object. watch(sources, callback, { // First run will only happen after sources change when set to true. // By default, its false. lazy: true }); watch.pre watch.pre is similar to watch, but it uses $2 under the hood. watchOnce In case you want to run the callback only once, you can use watchOnce and watchOnce.pre. It functions identically to the watch and watch.pre otherwise, but it does not accept any options object."}] \ No newline at end of file +[{"title":"Getting Started","href":"/docs/getting-started","description":"Learn how to install and use Runed in your projects.","content":"Installation Install Runed using your favorite package manager: npm install runed Usage Import one of the utilities you need to either a .svelte or .svelte.js|ts file and start using it: import { activeElement } from \"runed\"; let inputElement = $state(); {#if activeElement.current === inputElement} The input element is active! {/if} or import { activeElement } from \"runed\"; function logActiveElement() { $effect(() => { console.log(\"Active element is \", activeElement.current); }); } logActiveElement(); `"},{"title":"Introduction","href":"/docs/index","description":"Runes are magic, but what good is magic if you don't have a wand?","content":"Runed is a collection of utilities for Svelte 5 that make composing powerful applications and libraries a breeze, leveraging the power of $2. Why Runed? Svelte 5 Runes unlock immense power by providing a set of primitives that allow us to build impressive applications and libraries with ease. However, building complex applications often requires more than just the primitives provided by Svelte Runes. Runed takes those primitives to the next level by providing: Powerful Utilities**: A set of carefully crafted utility functions and classes that simplify common tasks and reduce boilerplate. Collective Efforts**: We often find ourselves writing the same utility functions over and over again. Runed aims to provide a single source of truth for these utilities, allowing the community to contribute, test, and benefit from them. Consistency**: A consistent set of APIs and behaviors across all utilities, so you can focus on building your projects instead of constantly learning new APIs. Reactivity First**: Powered by Svelte 5's new reactivity system, Runed utilities are designed to handle reactive state and side effects with ease. Type Safety**: Full TypeScript support to catch errors early and provide a better developer experience. Ideas and Principles Embrace the Magic of Runes Svelte Runes are a powerful new paradigm. Runed fully embraces this concept and explores its potential. Our goal is to make working with Runes feel as natural and intuitive as possible. Enhance, Don't Replace Runed is not here to replace Svelte's core functionality, but to enhance and extend it. Our utilities should feel like a natural extension of Svelte, not a separate framework. Progressive Complexity Simple things should be simple, complex things should be possible. Runed provides easy-to-use defaults while allowing for advanced customization when needed. Open Source and Community Collaboration Runed is an open-source, MIT licensed project that welcomes all forms of contributions from the community. Whether it's bug reports, feature requests, or code contributions, your input will help make Runed the best it can be."},{"title":"activeElement","href":"/docs/utilities/active-element","description":"Track and access the currently focused DOM element","content":" import Demo from '$lib/components/demos/active-element.svelte'; activeElement provides reactive access to the currently focused DOM element in your application, similar to document.activeElement but with reactive updates. Updates synchronously with DOM focus changes Returns null when no element is focused Safe to use with SSR (Server-Side Rendering) Lightweight alternative to manual focus tracking If you need to provide a custom document / shadowRoot, you can use the $2 utility instead, which provides a more flexible API. Demo Usage import { activeElement } from \"runed\"; Currently active element: {activeElement.current?.localName ?? \"No active element found\"} Type Definition interface ActiveElement { readonly current: Element | null; } `"},{"title":"AnimationFrames","href":"/docs/utilities/animation-frames","description":"A wrapper for requestAnimationFrame with FPS control and frame metrics","content":" import Demo from '$lib/components/demos/animation-frames.svelte'; AnimationFrames provides a declarative API over the browser's $2, offering FPS limiting capabilities and frame metrics while handling cleanup automatically. Demo Usage import { AnimationFrames } from \"runed\"; import { Slider } from \"../ui/slider\"; // Check out shadcn-svelte! let frames = $state(0); let fpsLimit = $state(10); let delta = $state(0); const animation = new AnimationFrames( (args) => { frames++; delta = args.delta; }, { fpsLimit: () => fpsLimit } ); const stats = $derived( Frames: ${frames}\\nFPS: ${animation.fps.toFixed(0)}\\nDelta: ${delta.toFixed(0)}ms ); {stats} {animation.running ? \"Stop\" : \"Start\"} FPS limit: {fpsLimit}{fpsLimit === 0 ? \" (not limited)\" : \"\"} (fpsLimit = value[0] ?? 0)} min={0} max={144} /> `"},{"title":"Context","href":"/docs/utilities/context","description":"A wrapper around Svelte's Context API that provides type safety and improved ergonomics for sharing data between components.","content":" import { Steps, Step, Callout } from '@svecodocs/kit'; Context allows you to pass data through the component tree without explicitly passing props through every level. It's useful for sharing data that many components need, like themes, authentication state, or localization preferences. The Context class provides a type-safe way to define, set, and retrieve context values. Usage Creating a Context First, create a Context instance with the type of value it will hold: import { Context } from \"runed\"; export const myTheme = new Context(\"theme\"); Creating a Context instance only defines the context - it doesn't actually set any value. The value passed to the constructor (\"theme\" in this example) is just an identifier used for debugging and error messages. Think of this step as creating a \"container\" that will later hold your context value. The container is typed (in this case to only accept \"light\" or \"dark\" as values) but remains empty until you explicitly call myTheme.set() during component initialization. This separation between defining and setting context allows you to: Keep context definitions in separate files Reuse the same context definition across different parts of your app Maintain type safety throughout your application Set different values for the same context in different component trees Setting Context Values Set the context value in a parent component during initialization. import { myTheme } from \"./context\"; let { data, children } = $props(); myTheme.set(data.theme); {@render children?.()} Context must be set during component initialization, similar to lifecycle functions like onMount. You cannot set context inside event handlers or callbacks. Reading Context Values Child components can access the context using get() or getOr() import { myTheme } from \"./context\"; const theme = myTheme.get(); // or with a fallback value if the context is not set const theme = myTheme.getOr(\"light\"); Type Definition class Context { /** @param name The name of the context. This is used for generating the context key and error messages. */ constructor(name: string) {} /** The key used to get and set the context. * It is not recommended to use this value directly. Instead, use the methods provided by this class. */ get key(): symbol; /** Checks whether this has been set in the context of a parent component. * Must be called during component initialization. */ exists(): boolean; /** Retrieves the context that belongs to the closest parent component. * Must be called during component initialization. * @throws An error if the context does not exist. */ get(): TContext; /** Retrieves the context that belongs to the closest parent component, or the given fallback value if the context does not exist. * Must be called during component initialization. */ getOr(fallback: TFallback): TContext | TFallback; /** Associates the given value with the current component and returns it. * Must be called during component initialization. */ set(context: TContext): TContext; } `"},{"title":"Debounced","href":"/docs/utilities/debounced","description":"A wrapper over `useDebounce` that returns a debounced state.","content":" import Demo from '$lib/components/demos/debounced.svelte'; Demo Usage This is a simple wrapper over $2 that returns a debounced state. import { Debounced } from \"runed\"; let search = $state(\"\"); const debounced = new Debounced(() => search, 500); You searched for: {debounced.current} You may cancel the pending update, run it immediately, or set a new value. Setting a new value immediately also cancels any pending updates. let count = $state(0); const debounced = new Debounced(() => count, 500); count = 1; debounced.cancel(); // after a while... console.log(debounced.current); // Still 0! count = 2; console.log(debounced.current); // Still 0! debounced.setImmediately(count); console.log(debounced.current); // 2 count = 3; console.log(debounced.current); // 2 await debounced.updateImmediately(); console.log(debounced.current); // 3 `"},{"title":"ElementRect","href":"/docs/utilities/element-rect","description":"Track element dimensions and position reactively","content":" import Demo from '$lib/components/demos/element-rect.svelte'; ElementRect provides reactive access to an element's dimensions and position information, automatically updating when the element's size or position changes. Demo Usage import { ElementRect } from \"runed\"; let el = $state(); const rect = new ElementRect(() => el); Width: {rect.width} Height: {rect.height} {JSON.stringify(rect.current, null, 2)} Type Definition type Rect = Omit; interface ElementRectOptions { initialRect?: DOMRect; } class ElementRect { constructor(node: MaybeGetter, options?: ElementRectOptions); readonly current: Rect; readonly width: number; readonly height: number; readonly top: number; readonly left: number; readonly right: number; readonly bottom: number; readonly x: number; readonly y: number; } `"},{"title":"ElementSize","href":"/docs/utilities/element-size","description":"Track element dimensions reactively","content":" import Demo from '$lib/components/demos/element-size.svelte'; ElementSize provides reactive access to an element's width and height, automatically updating when the element's dimensions change. Similar to ElementRect but focused only on size measurements. Demo Usage import { ElementSize } from \"runed\"; let el = $state() as HTMLElement; const size = new ElementSize(() => el); Width: {size.width} Height: {size.height} Type Definition interface ElementSize { readonly width: number; readonly height: number; } `"},{"title":"FiniteStateMachine","href":"/docs/utilities/finite-state-machine","description":"Defines a strongly-typed finite state machine.","content":" import Demo from '$lib/components/demos/finite-state-machine.svelte'; Demo type MyStates = \"disabled\" | \"idle\" | \"running\"; type MyEvents = \"toggleEnabled\" | \"start\" | \"stop\"; const f = new FiniteStateMachine(\"disabled\", { disabled: { toggleEnabled: \"idle\" }, idle: { toggleEnabled: \"disabled\", start: \"running\" }, running: { _enter: () => { f.debounce(2000, \"stop\"); }, stop: \"idle\", toggleEnabled: \"disabled\" } }); Usage Finite state machines (often abbreviated as \"FSMs\") are useful for tracking and manipulating something that could be in one of many different states. It centralizes the definition of every possible state and the events that might trigger a transition from one state to another. Here is a state machine describing a simple toggle switch: import { FiniteStateMachine } from \"runed\"; type MyStates = \"on\" | \"off\"; type MyEvents = \"toggle\"; const f = new FiniteStateMachine(\"off\", { off: { toggle: \"on\" }, on: { toggle: \"off\" } }); The first argument to the FiniteStateMachine constructor is the initial state. The second argument is an object with one key for each state. Each state then describes which events are valid for that state, and which state that event should lead to. In the above example of a simple switch, there are two states (on and off). The toggle event in either state leads to the other state. You send events to the FSM using f.send. To send the toggle event, invoke f.send('toggle'). Actions Maybe you want fancier logic for an event handler, or you want to conditionally transition into another state. Instead of strings, you can use actions. An action is a function that returns a state. An action can receive parameters, and it can use those parameters to dynamically choose which state should come next. It can also prevent a state transition by returning nothing. type MyStates = \"on\" | \"off\" | \"cooldown\"; const f = new FiniteStateMachine(\"off\", { off: { toggle: () => { if (isTuesday) { // Switch can only turn on during Tuesdays return \"on\"; } // All other days, nothing is returned and state is unchanged. } }, on: { toggle: (heldMillis: number) => { // You can also dynamically return the next state! // Only turn off if switch is depressed for 3 seconds if (heldMillis > 3000) { return \"off\"; } } } }); Lifecycle methods You can define special handlers that are invoked whenever a state is entered or exited: const f = new FiniteStateMachine('off', { off: { toggle: 'on' _enter: (meta) => { console.log('switch is off') } _exit: (meta) => { console.log('switch is no longer off') } }, on: { toggle: 'off' _enter: (meta) => { console.log('switch is on') } _exit: (meta) => { console.log('switch is no longer on') } } }); The lifecycle methods are invoked with a metadata object containing some useful information: from: the name of the event that is being exited to: the name of the event that is being entered event: the name of the event which has triggered the transition args: (optional) you may pass additional metadata when invoking an action with f.send('theAction', additional, params, as, args) The _enter handler for the initial state is called upon creation of the FSM. It is invoked with both the from and event fields set to null. Wildcard handlers There is one special state used as a fallback: *. If you have the fallback state, and you attempt to send() an event that is not handled by the current state, then it will try to find a handler for that event on the * state before discarding the event: const f = new FiniteStateMachine('off', { off: { toggle: 'on' }, on: { toggle: 'off' } '*': { emergency: 'off' } }); // will always result in the switch turning off. f.send('emergency'); Debouncing Frequently, you want to transition to another state after some time has elapsed. To do this, use the debounce method: f.send(\"toggle\"); // turn on immediately f.debounce(5000, \"toggle\"); // turn off in 5000 milliseconds If you re-invoke debounce with the same event, it will cancel the existing timer and start the countdown over: // schedule a toggle in five seconds f.debounce(5000, \"toggle\"); // ... less than 5000ms elapses ... f.debounce(5000, \"toggle\"); // The second call cancels the original timer, and starts a new one You can also use debounce in both actions and lifecycle methods. In both of the following examples, the lightswitch will turn itself off five seconds after it was turned on: const f = new FiniteStateMachine(\"off\", { off: { toggle: () => { f.debounce(5000, \"toggle\"); return \"on\"; } }, on: { toggle: \"off\" } }); const f = new FiniteStateMachine(\"off\", { off: { toggle: \"on\" }, on: { toggle: \"off\", _enter: () => { f.debounce(5000, \"toggle\"); } } }); Notes FiniteStateMachine is a loving rewrite of $2. FSMs are ideal for representing many different kinds of systems and interaction patterns. FiniteStateMachine is an intentionally minimalistic implementation. If you're looking for a more powerful FSM library, $2 is an excellent library with more features — and a steeper learning curve."},{"title":"IsFocusWithin","href":"/docs/utilities/is-focus-within","description":"A utility that tracks whether any descendant element has focus within a specified container element.","content":" import Demo from '$lib/components/demos/is-focus-within.svelte'; IsFocusWithin reactively tracks focus state within a container element, updating automatically when focus changes. Demo Usage import { IsFocusWithin } from \"runed\"; let formElement = $state(); const focusWithinForm = new IsFocusWithin(() => formElement); Focus within form: {focusWithinForm.current} Submit Type Definition class IsFocusWithin { constructor(node: MaybeGetter); readonly current: boolean; } `"},{"title":"IsIdle","href":"/docs/utilities/is-idle","description":"Track if a user is idle and the last time they were active.","content":" import Demo from '$lib/components/demos/is-idle.svelte'; IsIdle tracks user activity and determines if they're idle based on a configurable timeout. It monitors mouse movement, keyboard input, and touch events to detect user interaction. Demo Usage import { AnimationFrames, IsIdle } from \"runed\"; const idle = new IsIdle({ timeout: 1000 }); Idle: {idle.current} Last active: {new Date(idle.lastActive).toLocaleTimeString()} Type Definitions interface IsIdleOptions { /** The events that should set the idle state to true * @default ['mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel'] */ events?: MaybeGetter; /** The timeout in milliseconds before the idle state is set to true. Defaults to 60 seconds. * @default 60000 */ timeout?: MaybeGetter; /** Detect document visibility changes * @default true */ detectVisibilityChanges?: MaybeGetter; /** The initial state of the idle property * @default false */ initialState?: boolean; } class IsIdle { constructor(options?: IsIdleOptions); readonly current: boolean; readonly lastActive: number; } `"},{"title":"IsInViewport","href":"/docs/utilities/is-in-viewport","description":"Track if an element is visible within the current viewport.","content":" import Demo from '$lib/components/demos/is-in-viewport.svelte'; IsInViewport uses the $2 utility to track if an element is visible within the current viewport. It accepts an element or getter that returns an element and an optional options object that aligns with the $2 utility options. Demo Usage import { IsInViewport } from \"runed\"; let targetNode = $state()!; const inViewport = new IsInViewport(() => targetNode); Target node Target node in viewport: {inViewport.current} Type Definition import { type UseIntersectionObserverOptions } from \"runed\"; export type IsInViewportOptions = UseIntersectionObserverOptions; export declare class IsInViewport { constructor(node: MaybeGetter, options?: IsInViewportOptions); get current(): boolean; } "},{"title":"IsMounted","href":"/docs/utilities/is-mounted","description":"A class that returns the mounted state of the component it's called in.","content":" import Demo from '$lib/components/demos/is-mounted.svelte'; Demo Usage import { IsMounted } from \"runed\"; const isMounted = new IsMounted(); Which is a shorthand for one of the following: import { onMount } from \"svelte\"; const isMounted = $state({ current: false }); onMount(() => { isMounted.current = true; }); or import { untrack } from \"svelte\"; const isMounted = $state({ current: false }); $effect(() => { untrack(() => (isMounted.current = true)); }); `"},{"title":"IsSupported","href":"/docs/utilities/is-supported","description":"Determine if a feature is supported by the environment before using it.","content":"Usage import { IsSupported } from \"runed\"; const isSupported = new IsSupported(() => navigator && \"geolocation\" in navigator); if (isSupported.current) { // Do something with the geolocation API } Type Definition class IsSupported { readonly current: boolean; } `"},{"title":"onClickOutside","href":"/docs/utilities/on-click-outside","description":"Handle clicks outside of a specified element.","content":" import Demo from '$lib/components/demos/on-click-outside.svelte'; onClickOutside detects clicks that occur outside a specified element's boundaries and executes a callback function. It's commonly used for dismissible dropdowns, modals, and other interactive components. Demo Basic Usage import { onClickOutside } from \"runed\"; let container = $state()!; onClickOutside( () => container, () => console.log(\"clicked outside\") ); I'm outside the container Advanced Usage Controlled Listener The function returns control methods to programmatically manage the listener, start and stop and a reactive read-only property enabled to check the current status of the listeners. import { onClickOutside } from \"runed\"; let container = $state(); const clickOutside = onClickOutside( () => container, () => console.log(\"Clicked outside\") ); Status: {clickOutside.enabled ? \"Enabled\" : \"Disabled\"} Disable Enable Immediate By default, onClickOutside will start listening for clicks outside the element immediately. You can opt to disabled this behavior by passing { immediate: false } to the options argument. const clickOutside = onClickOutside( () => container, () => console.log(\"clicked outside\"), { immediate: false } ); // later when you want to start the listener clickOutside.start(); `"},{"title":"PersistedState","href":"/docs/utilities/persisted-state","description":"A reactive state manager that persists and synchronizes state across browser sessions and tabs using Web Storage APIs.","content":" import Demo from '$lib/components/demos/persisted-state.svelte'; import { Callout } from '@svecodocs/kit' PersistedState provides a reactive state container that automatically persists data to browser storage and optionally synchronizes changes across browser tabs in real-time. Demo You can refresh this page and/or open it in another tab to see the count state being persisted and synchronized across sessions and tabs. Usage Initialize PersistedState by providing a unique key and an initial value for the state. import { PersistedState } from \"runed\"; const count = new PersistedState(\"count\", 0); count.current++}>Increment count.current--}>Decrement (count.current = 0)}>Reset Count: {count.current} Configuration Options PersistedState includes an options object that allows you to customize the behavior of the state manager. const state = new PersistedState(\"user-preferences\", initialValue, { // Use sessionStorage instead of localStorage (default: 'local') storage: \"session\", // Disable cross-tab synchronization (default: true) syncTabs: false, // Custom serialization handlers serializer: { serialize: superjson.stringify, deserialize: superjson.parse } }); Storage Options 'local': Data persists until explicitly cleared 'session': Data persists until the browser session ends Cross-Tab Synchronization When syncTabs is enabled (default), changes are automatically synchronized across all browser tabs using the storage event. Custom Serialization Provide custom serialize and deserialize functions to handle complex data types: import superjson from \"superjson\"; // Example with Date objects const lastAccessed = new PersistedState(\"last-accessed\", new Date(), { serializer: { serialize: superjson.stringify, deserialize: superjson.parse } }); `"},{"title":"PressedKeys","href":"/docs/utilities/pressed-keys","description":"Tracks which keys are currently pressed","content":" import Demo from '$lib/components/demos/pressed-keys.svelte'; Demo Usage With an instance of PressedKeys, you can use the has method. const keys = new PressedKeys(); const isArrowDownPressed = $derived(keys.has(\"ArrowDown\")); const isCtrlAPressed = $derived(keys.has(\"Control\", \"a\")); Or get all of the currently pressed keys: const keys = new PressedKeys(); console.log(keys.all()); `"},{"title":"Previous","href":"/docs/utilities/previous","description":"A utility that tracks and provides access to the previous value of a reactive getter.","content":" import Demo from '$lib/components/demos/previous.svelte'; The Previous utility creates a reactive wrapper that maintains the previous value of a getter function. This is particularly useful when you need to compare state changes or implement transition effects. Demo Usage import { Previous } from \"runed\"; let count = $state(0); const previous = new Previous(() => count); count++}>Count: {count} Previous: {${previous.current}} Type Definition class Previous { constructor(getter: () => T); readonly current: T; // Previous value } `"},{"title":"StateHistory","href":"/docs/utilities/state-history","description":"Track state changes with undo/redo capabilities","content":" import Demo from '$lib/components/demos/state-history.svelte'; Demo Usage StateHistory tracks a getter's return value, logging each change into an array. A setter is also required to use the undo and redo functions. import { StateHistory } from \"runed\"; let count = $state(0); const history = new StateHistory(() => count, (c) => (count = c)); history.log[0]; // { snapshot: 0, timestamp: ... } Besides log, the returned object contains undo and redo functionality. import { useStateHistory } from \"runed\"; let count = $state(0); const history = new StateHistory(() => count, (c) => (count = c)); function format(ts: number) { return new Date(ts).toLocaleString(); } {count} count++}>Increment count--}>Decrement Undo Redo `"},{"title":"useActiveElement","href":"/docs/utilities/use-active-element","description":"Get a reactive reference to the currently focused element in the document.","content":" import Demo from '$lib/components/demos/use-active-element.svelte'; import { PropField } from '@svecodocs/kit' useActiveElement is used to get the currently focused element in the document. If you don't need to provide a custom document / shadowRoot, you can use the $2 state instead, as it provides a simpler API. This utility behaves similarly to document.activeElement but with additional features such as: Updates synchronously with DOM focus changes Returns null when no element is focused Safe to use with SSR (Server-Side Rendering) Lightweight alternative to manual focus tracking Demo Usage import { useActiveElement } from \"runed\"; const activeElement = useActiveElement(); {#if activeElement.current} The active element is: {activeElement.current.localName} {:else} No active element found {/if} Options The following options can be passed via the first argument to useActiveElement: The document or shadow root to track focus within. The window to use for focus tracking. "},{"title":"useDebounce","href":"/docs/utilities/use-debounce","description":"A higher-order function that debounces the execution of a function.","content":" import Demo from '$lib/components/demos/use-debounce.svelte'; useDebounce is a utility function that creates a debounced version of a callback function. Debouncing prevents a function from being called too frequently by delaying its execution until after a specified duration of inactivity. Demo Usage import { useDebounce } from \"runed\"; let count = $state(0); let logged = $state(\"\"); let isFirstTime = $state(true); let debounceDuration = $state(1000); const logCount = useDebounce( () => { if (isFirstTime) { isFirstTime = false; logged = You pressed the button ${count} times!; } else { logged = You pressed the button ${count} times since last time!; } count = 0; }, () => debounceDuration ); function ding() { count++; logCount(); } DING DING DING Run now Cancel message {logged || \"Press the button!\"} `"},{"title":"useEventListener","href":"/docs/utilities/use-event-listener","description":"A function that attaches an automatically disposed event listener.","content":" import Demo from '$lib/components/demos/use-event-listener.svelte'; Demo Usage The useEventListener function is particularly useful for attaching event listeners to elements you don't directly control. For instance, if you need to listen for events on the document body or window and can't use ``, or if you receive an element reference from a parent component. Example: Tracking Clicks on the Document // ClickLogger.ts import { useEventListener } from \"runed\"; export class ClickLogger { #clicks = $state(0); constructor() { useEventListener( () => document.body, \"click\", () => this.#clicks++ ); } get clicks() { return this.#clicks; } } This ClickLogger class tracks the number of clicks on the document body using the useEventListener function. Each time a click occurs, the internal counter increments. Svelte Component Usage import { ClickLogger } from \"./ClickLogger.ts\"; const logger = new ClickLogger(); You've clicked the document {logger.clicks} {logger.clicks === 1 ? \"time\" : \"times\"} In the component above, we create an instance of the ClickLogger class to monitor clicks on the document. The displayed text updates dynamically based on the recorded click count. Key Points Automatic Cleanup:** The event listener is removed automatically when the component is destroyed or when the element reference changes. Lazy Initialization:** The target element can be defined using a function, enabling flexible and dynamic behavior. Convenient for Global Listeners:** Ideal for scenarios where attaching event listeners directly to the DOM elements is cumbersome or impractical."},{"title":"useGeolocation","href":"/docs/utilities/use-geolocation","description":"Reactive access to the browser's Geolocation API.","content":" import Demo from '$lib/components/demos/use-geolocation.svelte'; useGeolocation is a reactive wrapper around the browser's $2. Demo Usage import { useGeolocation } from \"runed\"; const location = useGeolocation(); Coords: {JSON.stringify(location.position.coords, null, 2)} Located at: {location.position.timestamp} Error: {JSON.stringify(location.error, null, 2)} Is Supported: {location.isSupported} Pause Resume Type Definitions type UseGeolocationOptions = Partial & { /** Whether to start the watcher immediately upon creation. If set to false, the watcher will only start tracking the position when resume() is called. * @defaultValue true */ immediate?: boolean; }; type UseGeolocationReturn = { readonly isSupported: boolean; readonly position: Omit; readonly error: GeolocationPositionError | null; readonly isPaused: boolean; pause: () => void; resume: () => void; }; `"},{"title":"useIntersectionObserver","href":"/docs/utilities/use-intersection-observer","description":"Watch for intersection changes of a target element.","content":" import Demo from '$lib/components/demos/use-intersection-observer.svelte'; import { Callout } from '@svecodocs/kit' Demo Usage With a reference to an element, you can use the useIntersectionObserver utility to watch for intersection changes of the target element. import { useIntersectionObserver } from \"runed\"; let target = $state(null); let root = $state(null); let isIntersecting = $state(false); useIntersectionObserver( () => target, (entries) => { const entry = entries[0]; if (!entry) return; isIntersecting = entry.isIntersecting; }, { root: () => root } ); {#if isIntersecting} Target is intersecting {:else} Target is not intersecting {/if} Pause You can pause the intersection observer at any point by calling the pause method. const observer = useIntersectionObserver(/* ... */); observer.pause(); Resume You can resume the intersection observer at any point by calling the resume method. const observer = useIntersectionObserver(/* ... */); observer.resume(); Stop You can stop the intersection observer at any point by calling the stop method. const observer = useIntersectionObserver(/* ... */); observer.stop(); isActive You can check if the intersection observer is active by checking the isActive property. This property cannot be destructured as it is a getter. You must access it directly from the observer. const observer = useIntersectionObserver(/* ... */); if (observer.isActive) { // do something } `"},{"title":"useMutationObserver","href":"/docs/utilities/use-mutation-observer","description":"Observe changes in an element","content":" import Demo from '$lib/components/demos/use-mutation-observer.svelte'; Demo Usage With a reference to an element, you can use the useMutationObserver hook to observe changes in the element. import { useMutationObserver } from \"runed\"; let el = $state(null); const messages = $state([]); let className = $state(\"\"); let style = $state(\"\"); useMutationObserver( () => el, (mutations) => { const mutation = mutations[0]; if (!mutation) return; messages.push(mutation.attributeName!); }, { attributes: true } ); setTimeout(() => { className = \"text-brand\"; }, 1000); setTimeout(() => { style = \"font-style: italic;\"; }, 1500); {#each messages as text} Mutation Attribute: {text} {:else} No mutations yet {/each} You can stop the mutation observer at any point by calling the stop method. const { stop } = useMutationObserver(/* ... */); stop(); `"},{"title":"useResizeObserver","href":"/docs/utilities/use-resize-observer","description":"Detects changes in the size of an element","content":" import Demo from '$lib/components/demos/use-resize-observer.svelte'; Demo Usage With a reference to an element, you can use the useResizeObserver utility to detect changes in the size of an element. import { useResizeObserver } from \"runed\"; let el = $state(null); let text = $state(\"\"); useResizeObserver( () => el, (entries) => { const entry = entries[0]; if (!entry) return; const { width, height } = entry.contentRect; text = width: ${width};\\nheight: ${height};; } ); You can stop the resize observer at any point by calling the stop method. const { stop } = useResizeObserver(/* ... */); stop(); `"},{"title":"watch","href":"/docs/utilities/watch","description":"Watch for changes and run a callback","content":"Runes provide a handy way of running a callback when reactive values change: $2. It automatically detects when inner values change, and re-runs the callback. $effect is great, but sometimes you want to manually specify which values should trigger the callback. Svelte provides an untrack function, allowing you to specify that a dependency shouldn't be tracked, but it doesn't provide a way to say that only certain values should be tracked. watch does exactly that. It accepts a getter function, which returns the dependencies of the effect callback. Usage watch Runs a callback whenever one of the sources change. import { watch } from \"runed\"; let count = $state(0); watch(() => count, () => { console.log(count); } ); The callback receives two arguments: The current value of the sources, and the previous value. let count = $state(0); watch(() => count, (curr, prev) => { console.log(count is ${curr}, was ${prev}); } ); You can also send in an array of sources: let age = $state(20); let name = $state(\"bob\"); watch([() => age, () => name], ([age, name], [prevAge, prevName]) => { // ... } watch also accepts an options object. watch(sources, callback, { // First run will only happen after sources change when set to true. // By default, its false. lazy: true }); watch.pre watch.pre is similar to watch, but it uses $2 under the hood. watchOnce In case you want to run the callback only once, you can use watchOnce and watchOnce.pre. It functions identically to the watch and watch.pre otherwise, but it does not accept any options object."}] \ No newline at end of file