INIT
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Hunter Johnston <https://github.com/huntabyte>
|
||||
Copyright (c) 2024 Thomas G. Lopes <https://github.com/tglide>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# Runed
|
||||
|
||||
<!-- automd:badges license name="runed" color="green" github="svecosystem/runed" -->
|
||||
|
||||
[](https://npmjs.com/package/runed)
|
||||
[](https://npmjs.com/package/runed)
|
||||
[](https://github.com/svecosystem/runed/blob/main/LICENSE)
|
||||
|
||||
<!-- /automd -->
|
||||
|
||||
Runed provides utilities to power your applications using the magic of
|
||||
[Svelte Runes](https://svelte.dev/blog/runes).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install runed
|
||||
```
|
||||
|
||||
Check out the [documentation](https://runed.dev) for more information.
|
||||
|
||||
## Sponsors
|
||||
|
||||
This project is supported by the following beautiful people/organizations:
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/sponsors/huntabyte">
|
||||
<img src='https://cdn.jsdelivr.net/gh/huntabyte/static/sponsors.svg' alt="Logos from Sponsors" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## License
|
||||
|
||||
<!-- automd:contributors license=MIT author="huntabyte" -->
|
||||
|
||||
Published under the [MIT](https://github.com/svecosystem/runed/blob/main/LICENSE) license. Made by
|
||||
[@TGlide](https://github.com/tglide), [@huntabyte](https://github.com/huntabyte) and
|
||||
[community](https://github.com/svecosystem/runed/graphs/contributors) 💛 <br><br>
|
||||
<a href="https://github.com/svecosystem/runed/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=svecosystem/runed" /> </a>
|
||||
|
||||
<!-- /automd -->
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export * from "./utilities/index.js";
|
||||
export type { MaybeGetter, Getter, Setter } from "./internal/types.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./utilities/index.js";
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
export type ConfigurableWindow = {
|
||||
/** Provide a custom `window` object to use in place of the global `window` object. */
|
||||
window?: typeof globalThis & Window;
|
||||
};
|
||||
export type ConfigurableDocument = {
|
||||
/** Provide a custom `document` object to use in place of the global `document` object. */
|
||||
document?: Document;
|
||||
};
|
||||
export type ConfigurableDocumentOrShadowRoot = {
|
||||
document?: DocumentOrShadowRoot;
|
||||
};
|
||||
export type ConfigurableNavigator = {
|
||||
/** Provide a custom `navigator` object to use in place of the global `navigator` object. */
|
||||
navigator?: Navigator;
|
||||
};
|
||||
export type ConfigurableLocation = {
|
||||
/** Provide a custom `location` object to use in place of the global `location` object. */
|
||||
location?: Location;
|
||||
};
|
||||
export declare const defaultWindow: (Window & typeof globalThis) | undefined;
|
||||
export declare const defaultDocument: Document | undefined;
|
||||
export declare const defaultNavigator: Navigator | undefined;
|
||||
export declare const defaultLocation: Location | undefined;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { BROWSER } from "esm-env";
|
||||
export const defaultWindow = BROWSER && typeof window !== "undefined" ? window : undefined;
|
||||
export const defaultDocument = BROWSER && typeof window !== "undefined" ? window.document : undefined;
|
||||
export const defaultNavigator = BROWSER && typeof window !== "undefined" ? window.navigator : undefined;
|
||||
export const defaultLocation = BROWSER && typeof window !== "undefined" ? window.location : undefined;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export type Getter<T> = () => T;
|
||||
export type MaybeGetter<T> = T | Getter<T>;
|
||||
export type MaybeElementGetter<T extends Element = HTMLElement> = MaybeGetter<T | null | undefined>;
|
||||
export type MaybeElement = HTMLElement | SVGElement | undefined | null;
|
||||
export type Setter<T> = (value: T) => void;
|
||||
export type Expand<T> = T extends infer U ? {
|
||||
[K in keyof U]: U[K];
|
||||
} : never;
|
||||
export type WritableProperties<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Get nth item of Array. Negative for backward
|
||||
*/
|
||||
export declare function at<T>(array: readonly T[], index: number): T | undefined;
|
||||
export declare function last<T>(array: readonly T[]): T | undefined;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Get nth item of Array. Negative for backward
|
||||
*/
|
||||
export function at(array, index) {
|
||||
const len = array.length;
|
||||
if (!len)
|
||||
return undefined;
|
||||
if (index < 0)
|
||||
index += len;
|
||||
return array[index];
|
||||
}
|
||||
export function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { BROWSER as browser } from "esm-env";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { BROWSER as browser } from "esm-env";
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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
|
||||
* to find the active element.
|
||||
* If not, it will return the active element in the document.
|
||||
*
|
||||
* @param document A document or shadow root to get the active element from.
|
||||
* @returns The active element in the document or shadow root.
|
||||
*/
|
||||
export declare function getActiveElement(document: DocumentOrShadowRoot): Element | null;
|
||||
/**
|
||||
* Returns the owner document of a given element.
|
||||
*
|
||||
* @param node The element to get the owner document from.
|
||||
* @returns
|
||||
*/
|
||||
export declare function getOwnerDocument(node: Element | null | undefined, fallback?: Document | undefined): Document | undefined;
|
||||
/**
|
||||
* 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 declare function isOrContainsTarget(node: Element, target: Element): boolean;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
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
|
||||
* to find the active element.
|
||||
* If not, it will return the active element in the document.
|
||||
*
|
||||
* @param document A document or shadow root to get the active element from.
|
||||
* @returns The active element in the document or shadow root.
|
||||
*/
|
||||
export function getActiveElement(document) {
|
||||
let activeElement = document.activeElement;
|
||||
while (activeElement?.shadowRoot) {
|
||||
const node = activeElement.shadowRoot.activeElement;
|
||||
if (node === activeElement)
|
||||
break;
|
||||
else
|
||||
activeElement = node;
|
||||
}
|
||||
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, fallback = defaultDocument) {
|
||||
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, target) {
|
||||
return node === target || node.contains(target);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function noop(): void;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export function noop() { }
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import type { MaybeGetter } from "../types.js";
|
||||
export declare function get<T>(value: MaybeGetter<T>): T;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { isFunction } from "./is.js";
|
||||
export function get(value) {
|
||||
if (isFunction(value)) {
|
||||
return value();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export declare function isFunction(value: unknown): value is (...args: unknown[]) => unknown;
|
||||
export declare function isObject(value: unknown): value is Record<PropertyKey, unknown>;
|
||||
export declare function isElement(value: unknown): value is Element;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export function isFunction(value) {
|
||||
return typeof value === "function";
|
||||
}
|
||||
export function isObject(value) {
|
||||
return value !== null && typeof value === "object";
|
||||
}
|
||||
export function isElement(value) {
|
||||
return value instanceof Element;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function sleep(ms?: number): Promise<void>;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export async function sleep(ms = 0) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Runed Kit Utilities
|
||||
|
||||
This subpath contains utilities that require SvelteKit to be installed.
|
||||
|
||||
## Installation
|
||||
|
||||
Make sure you have SvelteKit installed:
|
||||
|
||||
```bash
|
||||
npm install @sveltejs/kit
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
// Import SvelteKit-dependent utilities from the kit subpath
|
||||
import { useSearchParams } from "runed/kit";
|
||||
|
||||
// Regular utilities are still available from the main export
|
||||
import { isMounted, useEventListener } from "runed";
|
||||
```
|
||||
|
||||
## Available Utilities
|
||||
|
||||
- `useSearchParams` - Reactive URL search parameters management (requires SvelteKit)
|
||||
|
||||
## Why a separate subpath?
|
||||
|
||||
The main `runed` package is designed to work with Svelte alone, without requiring SvelteKit.
|
||||
However, some utilities like `useSearchParams` need SvelteKit's routing and navigation features. By
|
||||
providing these utilities through a separate import path, we ensure:
|
||||
|
||||
1. The main package remains lightweight and SvelteKit-free
|
||||
2. SvelteKit users can access additional utilities when needed
|
||||
3. Clear separation of dependencies and requirements
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "../utilities/use-search-params/index.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "../utilities/use-search-params/index.js";
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "runed-kit",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"peerDependencies": {
|
||||
"@sveltejs/kit": "^2.21.0",
|
||||
"svelte": "^5.7.0"
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export declare function testWithEffect(name: string, fn: () => void | Promise<void>): void;
|
||||
export declare function effectRootScope(fn: () => void | Promise<void>): void | Promise<void>;
|
||||
export declare function vitestSetTimeoutWrapper(fn: () => void, timeout: number): void;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { test, vi } from "vitest";
|
||||
export function testWithEffect(name, fn) {
|
||||
test(name, () => effectRootScope(fn));
|
||||
}
|
||||
export function effectRootScope(fn) {
|
||||
let promise;
|
||||
const cleanup = $effect.root(() => {
|
||||
promise = fn();
|
||||
});
|
||||
if (promise instanceof Promise) {
|
||||
return promise.finally(cleanup);
|
||||
}
|
||||
else {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
export function vitestSetTimeoutWrapper(fn, timeout) {
|
||||
setTimeout(() => {
|
||||
fn();
|
||||
}, timeout);
|
||||
vi.advanceTimersByTime(timeout);
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type ConfigurableDocumentOrShadowRoot, type ConfigurableWindow } from "../../internal/configurable-globals.js";
|
||||
export interface ActiveElementOptions extends ConfigurableDocumentOrShadowRoot, ConfigurableWindow {
|
||||
}
|
||||
export declare class ActiveElement {
|
||||
#private;
|
||||
constructor(options?: ActiveElementOptions);
|
||||
get current(): Element | null;
|
||||
}
|
||||
/**
|
||||
* An object holding a reactive value that is equal to `document.activeElement`.
|
||||
* It automatically listens for changes, keeping the reference up to date.
|
||||
*
|
||||
* If you wish to use a custom document or shadowRoot, you should use
|
||||
* [useActiveElement](https://runed.dev/docs/utilities/active-element) instead.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/active-element}
|
||||
*/
|
||||
export declare const activeElement: ActiveElement;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { defaultWindow, } from "../../internal/configurable-globals.js";
|
||||
import { getActiveElement } from "../../internal/utils/dom.js";
|
||||
import { on } from "svelte/events";
|
||||
import { createSubscriber } from "svelte/reactivity";
|
||||
export class ActiveElement {
|
||||
#document;
|
||||
#subscribe;
|
||||
constructor(options = {}) {
|
||||
const { window = defaultWindow, document = window?.document } = options;
|
||||
if (window === undefined)
|
||||
return;
|
||||
this.#document = document;
|
||||
this.#subscribe = createSubscriber((update) => {
|
||||
const cleanupFocusIn = on(window, "focusin", update);
|
||||
const cleanupFocusOut = on(window, "focusout", update);
|
||||
return () => {
|
||||
cleanupFocusIn();
|
||||
cleanupFocusOut();
|
||||
};
|
||||
});
|
||||
}
|
||||
get current() {
|
||||
this.#subscribe?.();
|
||||
if (!this.#document)
|
||||
return null;
|
||||
return getActiveElement(this.#document);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* An object holding a reactive value that is equal to `document.activeElement`.
|
||||
* It automatically listens for changes, keeping the reference up to date.
|
||||
*
|
||||
* If you wish to use a custom document or shadowRoot, you should use
|
||||
* [useActiveElement](https://runed.dev/docs/utilities/active-element) instead.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/active-element}
|
||||
*/
|
||||
export const activeElement = new ActiveElement();
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./active-element.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./active-element.svelte.js";
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import type { MaybeGetter } from "../../internal/types.js";
|
||||
import { type ConfigurableWindow } from "../../internal/configurable-globals.js";
|
||||
type RafCallbackParams = {
|
||||
/** The number of milliseconds since the last frame. */
|
||||
delta: number;
|
||||
/**
|
||||
* Time elapsed since the creation of the web page.
|
||||
* See {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#the_time_origin Time origin}.
|
||||
*/
|
||||
timestamp: DOMHighResTimeStamp;
|
||||
};
|
||||
export type AnimationFramesOptions = ConfigurableWindow & {
|
||||
/**
|
||||
* Start calling requestAnimationFrame immediately.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
immediate?: boolean;
|
||||
/**
|
||||
* Limit the number of frames per second.
|
||||
* Set to `0` to disable
|
||||
*
|
||||
* @default 0
|
||||
*/
|
||||
fpsLimit?: MaybeGetter<number>;
|
||||
};
|
||||
/**
|
||||
* Wrapper over {@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame requestAnimationFrame},
|
||||
* with controls for pausing and resuming the animation, reactive tracking and optional limiting of fps, and utilities.
|
||||
*/
|
||||
export declare class AnimationFrames {
|
||||
#private;
|
||||
constructor(callback: (params: RafCallbackParams) => void, options?: AnimationFramesOptions);
|
||||
start(): void;
|
||||
stop(): void;
|
||||
toggle(): void;
|
||||
get fps(): number;
|
||||
get running(): boolean;
|
||||
}
|
||||
export {};
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { untrack } from "svelte";
|
||||
import { extract } from "../extract/index.js";
|
||||
import { defaultWindow } from "../../internal/configurable-globals.js";
|
||||
/**
|
||||
* Wrapper over {@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame requestAnimationFrame},
|
||||
* with controls for pausing and resuming the animation, reactive tracking and optional limiting of fps, and utilities.
|
||||
*/
|
||||
export class AnimationFrames {
|
||||
#callback;
|
||||
#fpsLimitOption = 0;
|
||||
#fpsLimit = $derived(extract(this.#fpsLimitOption) ?? 0);
|
||||
#previousTimestamp = null;
|
||||
#frame = null;
|
||||
#fps = $state(0);
|
||||
#running = $state(false);
|
||||
#window = defaultWindow;
|
||||
constructor(callback, options = {}) {
|
||||
if (options.window)
|
||||
this.#window = options.window;
|
||||
this.#fpsLimitOption = options.fpsLimit;
|
||||
this.#callback = callback;
|
||||
this.start = this.start.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
this.toggle = this.toggle.bind(this);
|
||||
$effect(() => {
|
||||
if (options.immediate ?? true) {
|
||||
untrack(this.start);
|
||||
}
|
||||
return this.stop;
|
||||
});
|
||||
}
|
||||
#loop(timestamp) {
|
||||
if (!this.#running || !this.#window)
|
||||
return;
|
||||
if (this.#previousTimestamp === null) {
|
||||
this.#previousTimestamp = timestamp;
|
||||
}
|
||||
const delta = timestamp - this.#previousTimestamp;
|
||||
const fps = 1000 / delta;
|
||||
if (this.#fpsLimit && fps > this.#fpsLimit) {
|
||||
this.#frame = this.#window.requestAnimationFrame(this.#loop.bind(this));
|
||||
return;
|
||||
}
|
||||
this.#fps = fps;
|
||||
this.#previousTimestamp = timestamp;
|
||||
this.#callback({ delta, timestamp });
|
||||
this.#frame = this.#window.requestAnimationFrame(this.#loop.bind(this));
|
||||
}
|
||||
start() {
|
||||
if (!this.#window)
|
||||
return;
|
||||
this.#running = true;
|
||||
this.#previousTimestamp = 0;
|
||||
this.#frame = this.#window.requestAnimationFrame(this.#loop.bind(this));
|
||||
}
|
||||
stop() {
|
||||
if (!this.#frame || !this.#window)
|
||||
return;
|
||||
this.#running = false;
|
||||
this.#window.cancelAnimationFrame(this.#frame);
|
||||
this.#frame = null;
|
||||
}
|
||||
toggle() {
|
||||
this.#running ? this.stop() : this.start();
|
||||
}
|
||||
get fps() {
|
||||
return !this.#running ? 0 : this.#fps;
|
||||
}
|
||||
get running() {
|
||||
return this.#running;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./animation-frames.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./animation-frames.svelte.js";
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Transforms any value into `""` (empty string) or `undefined` for use with HTML attributes where presence indicates truth.
|
||||
*
|
||||
* Unlike directly assigning a boolean (e.g., `data-active={true}`), which often renders the string "true" as the attribute's value,
|
||||
* this utility produces an empty string for truthy inputs (resulting in `<div data-active>`) or `undefined` for falsy inputs
|
||||
* (resulting in the attribute being omitted entirely: `<div>`). This pattern is standard for native boolean attributes
|
||||
* and essential for custom ones used as flags.
|
||||
*
|
||||
* This is particularly useful for custom `data-*` attributes that drive conditional styling (e.g., CSS selectors like `[data-state="active"]` or `[data-loading]`)
|
||||
* or JavaScript behavior. It simplifies applying conditional states for CSS frameworks like Tailwind CSS, enabling more terse attribute-based variants
|
||||
* such as `data-[active]:opacity-100` or `group-data-[loading]:pointer-events-none`, instead of needing to target the attribute's value (`data[active='true']:opacity-100`).
|
||||
*/
|
||||
export declare function boolAttr(value: unknown): "" | undefined;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Transforms any value into `""` (empty string) or `undefined` for use with HTML attributes where presence indicates truth.
|
||||
*
|
||||
* Unlike directly assigning a boolean (e.g., `data-active={true}`), which often renders the string "true" as the attribute's value,
|
||||
* this utility produces an empty string for truthy inputs (resulting in `<div data-active>`) or `undefined` for falsy inputs
|
||||
* (resulting in the attribute being omitted entirely: `<div>`). This pattern is standard for native boolean attributes
|
||||
* and essential for custom ones used as flags.
|
||||
*
|
||||
* This is particularly useful for custom `data-*` attributes that drive conditional styling (e.g., CSS selectors like `[data-state="active"]` or `[data-loading]`)
|
||||
* or JavaScript behavior. It simplifies applying conditional states for CSS frameworks like Tailwind CSS, enabling more terse attribute-based variants
|
||||
* such as `data-[active]:opacity-100` or `group-data-[loading]:pointer-events-none`, instead of needing to target the attribute's value (`data[active='true']:opacity-100`).
|
||||
*/
|
||||
export function boolAttr(value) {
|
||||
return value ? "" : undefined;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { boolAttr } from "./bool-attr.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { boolAttr } from "./bool-attr.js";
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
export declare class Context<TContext> {
|
||||
#private;
|
||||
/**
|
||||
* @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 initialisation.
|
||||
*/
|
||||
exists(): boolean;
|
||||
/**
|
||||
* Retrieves the context that belongs to the closest parent component.
|
||||
*
|
||||
* Must be called during component initialisation.
|
||||
*
|
||||
* @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 initialisation.
|
||||
*/
|
||||
getOr<TFallback>(fallback: TFallback): TContext | TFallback;
|
||||
/**
|
||||
* Associates the given value with the current component and returns it.
|
||||
*
|
||||
* Must be called during component initialisation.
|
||||
*/
|
||||
set(context: TContext): TContext;
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { getContext, hasContext, setContext } from "svelte";
|
||||
export class Context {
|
||||
#name;
|
||||
#key;
|
||||
/**
|
||||
* @param name The name of the context.
|
||||
* This is used for generating the context key and error messages.
|
||||
*/
|
||||
constructor(name) {
|
||||
this.#name = name;
|
||||
this.#key = Symbol(name);
|
||||
}
|
||||
/**
|
||||
* 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() {
|
||||
return this.#key;
|
||||
}
|
||||
/**
|
||||
* Checks whether this has been set in the context of a parent component.
|
||||
*
|
||||
* Must be called during component initialisation.
|
||||
*/
|
||||
exists() {
|
||||
return hasContext(this.#key);
|
||||
}
|
||||
/**
|
||||
* Retrieves the context that belongs to the closest parent component.
|
||||
*
|
||||
* Must be called during component initialisation.
|
||||
*
|
||||
* @throws An error if the context does not exist.
|
||||
*/
|
||||
get() {
|
||||
const context = getContext(this.#key);
|
||||
if (context === undefined) {
|
||||
throw new Error(`Context "${this.#name}" not found`);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
/**
|
||||
* 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 initialisation.
|
||||
*/
|
||||
getOr(fallback) {
|
||||
const context = getContext(this.#key);
|
||||
if (context === undefined) {
|
||||
return fallback;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
/**
|
||||
* Associates the given value with the current component and returns it.
|
||||
*
|
||||
* Must be called during component initialisation.
|
||||
*/
|
||||
set(context) {
|
||||
return setContext(this.#key, context);
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { Context } from "./context.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { Context } from "./context.js";
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import type { Getter, MaybeGetter } from "../../internal/types.js";
|
||||
/**
|
||||
* A wrapper over {@link useDebounce} that creates a debounced state.
|
||||
* It takes a "getter" function which returns the state you want to debounce.
|
||||
* Every time this state changes a timer (re)starts, the length of which is
|
||||
* configurable with the `wait` arg. When the timer ends the `current` value
|
||||
* is updated.
|
||||
*
|
||||
* @see https://runed.dev/docs/utilities/debounced
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* <script lang="ts">
|
||||
* import { Debounced } from "runed";
|
||||
*
|
||||
* let search = $state("");
|
||||
* const debounced = new Debounced(() => search, 500);
|
||||
* </script>
|
||||
*
|
||||
* <div>
|
||||
* <input bind:value={search} />
|
||||
* <p>You searched for: <b>{debounced.current}</b></p>
|
||||
* </div>
|
||||
*/
|
||||
export declare class Debounced<T> {
|
||||
#private;
|
||||
/**
|
||||
* @param getter A function that returns the state to watch.
|
||||
* @param wait The length of time to wait in ms, defaults to 250.
|
||||
*/
|
||||
constructor(getter: Getter<T>, wait?: MaybeGetter<number>);
|
||||
/**
|
||||
* Get the debounced value.
|
||||
*/
|
||||
get current(): T;
|
||||
/**
|
||||
* Whether a timer is currently pending.
|
||||
*/
|
||||
get pending(): boolean;
|
||||
/**
|
||||
* Cancel the latest timer.
|
||||
*/
|
||||
cancel(): void;
|
||||
/**
|
||||
* Run the debounced function immediately.
|
||||
*/
|
||||
updateImmediately(): Promise<void>;
|
||||
/**
|
||||
* Set the `current` value without waiting.
|
||||
*/
|
||||
setImmediately(v: T): void;
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { useDebounce } from "../use-debounce/use-debounce.svelte.js";
|
||||
import { watch } from "../watch/watch.svelte.js";
|
||||
import { noop } from "../../internal/utils/function.js";
|
||||
/**
|
||||
* A wrapper over {@link useDebounce} that creates a debounced state.
|
||||
* It takes a "getter" function which returns the state you want to debounce.
|
||||
* Every time this state changes a timer (re)starts, the length of which is
|
||||
* configurable with the `wait` arg. When the timer ends the `current` value
|
||||
* is updated.
|
||||
*
|
||||
* @see https://runed.dev/docs/utilities/debounced
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* <script lang="ts">
|
||||
* import { Debounced } from "runed";
|
||||
*
|
||||
* let search = $state("");
|
||||
* const debounced = new Debounced(() => search, 500);
|
||||
* </script>
|
||||
*
|
||||
* <div>
|
||||
* <input bind:value={search} />
|
||||
* <p>You searched for: <b>{debounced.current}</b></p>
|
||||
* </div>
|
||||
*/
|
||||
export class Debounced {
|
||||
#current = $state();
|
||||
#debounceFn;
|
||||
/**
|
||||
* @param getter A function that returns the state to watch.
|
||||
* @param wait The length of time to wait in ms, defaults to 250.
|
||||
*/
|
||||
constructor(getter, wait = 250) {
|
||||
this.#current = getter(); // immediately set the initial value
|
||||
this.cancel = this.cancel.bind(this);
|
||||
this.setImmediately = this.setImmediately.bind(this);
|
||||
this.updateImmediately = this.updateImmediately.bind(this);
|
||||
this.#debounceFn = useDebounce(() => {
|
||||
this.#current = getter();
|
||||
}, wait);
|
||||
watch(getter, () => {
|
||||
this.#debounceFn().catch(noop);
|
||||
});
|
||||
}
|
||||
// isn't the name "current" slightly misleading? it sounds like the latest, raw value
|
||||
/**
|
||||
* Get the debounced value.
|
||||
*/
|
||||
get current() {
|
||||
return this.#current;
|
||||
}
|
||||
/**
|
||||
* Whether a timer is currently pending.
|
||||
*/
|
||||
get pending() {
|
||||
return this.#debounceFn.pending;
|
||||
}
|
||||
/**
|
||||
* Cancel the latest timer.
|
||||
*/
|
||||
cancel() {
|
||||
this.#debounceFn.cancel();
|
||||
}
|
||||
/**
|
||||
* Run the debounced function immediately.
|
||||
*/
|
||||
updateImmediately() {
|
||||
return this.#debounceFn.runScheduledNow();
|
||||
}
|
||||
/**
|
||||
* Set the `current` value without waiting.
|
||||
*/
|
||||
setImmediately(v) {
|
||||
this.cancel();
|
||||
this.#current = v;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./debounced.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./debounced.svelte.js";
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import type { MaybeElementGetter, WritableProperties } from "../../internal/types.js";
|
||||
import type { ConfigurableWindow } from "../../internal/configurable-globals.js";
|
||||
type Rect = WritableProperties<Omit<DOMRect, "toJSON">>;
|
||||
export type ElementRectOptions = ConfigurableWindow & {
|
||||
initialRect?: DOMRect;
|
||||
};
|
||||
/**
|
||||
* Returns a reactive value holding the size of `node`.
|
||||
*
|
||||
* Accepts an `options` object with the following properties:
|
||||
* - `initialSize`: The initial size of the element. Defaults to `{ width: 0, height: 0 }`.
|
||||
* - `box`: The box model to use. Can be either `"content-box"` or `"border-box"`. Defaults to `"border-box"`.
|
||||
*
|
||||
* @returns an object with `width` and `height` properties.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/element-size}
|
||||
*/
|
||||
export declare class ElementRect {
|
||||
#private;
|
||||
constructor(node: MaybeElementGetter, options?: ElementRectOptions);
|
||||
get x(): number;
|
||||
get y(): number;
|
||||
get width(): number;
|
||||
get height(): number;
|
||||
get top(): number;
|
||||
get right(): number;
|
||||
get bottom(): number;
|
||||
get left(): number;
|
||||
get current(): Rect;
|
||||
}
|
||||
export {};
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { extract } from "../extract/extract.svelte.js";
|
||||
import { useMutationObserver } from "../use-mutation-observer/use-mutation-observer.svelte.js";
|
||||
import { useResizeObserver } from "../use-resize-observer/use-resize-observer.svelte.js";
|
||||
/**
|
||||
* Returns a reactive value holding the size of `node`.
|
||||
*
|
||||
* Accepts an `options` object with the following properties:
|
||||
* - `initialSize`: The initial size of the element. Defaults to `{ width: 0, height: 0 }`.
|
||||
* - `box`: The box model to use. Can be either `"content-box"` or `"border-box"`. Defaults to `"border-box"`.
|
||||
*
|
||||
* @returns an object with `width` and `height` properties.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/element-size}
|
||||
*/
|
||||
export class ElementRect {
|
||||
#rect = $state({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
});
|
||||
constructor(node, options = {}) {
|
||||
this.#rect = {
|
||||
width: options.initialRect?.width ?? 0,
|
||||
height: options.initialRect?.height ?? 0,
|
||||
x: options.initialRect?.x ?? 0,
|
||||
y: options.initialRect?.y ?? 0,
|
||||
top: options.initialRect?.top ?? 0,
|
||||
right: options.initialRect?.right ?? 0,
|
||||
bottom: options.initialRect?.bottom ?? 0,
|
||||
left: options.initialRect?.left ?? 0,
|
||||
};
|
||||
const el = $derived(extract(node));
|
||||
const update = () => {
|
||||
if (!el)
|
||||
return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
this.#rect.width = rect.width;
|
||||
this.#rect.height = rect.height;
|
||||
this.#rect.x = rect.x;
|
||||
this.#rect.y = rect.y;
|
||||
this.#rect.top = rect.top;
|
||||
this.#rect.right = rect.right;
|
||||
this.#rect.bottom = rect.bottom;
|
||||
this.#rect.left = rect.left;
|
||||
};
|
||||
useResizeObserver(() => el, update, { window: options.window });
|
||||
$effect(update);
|
||||
useMutationObserver(() => el, update, {
|
||||
attributeFilter: ["style", "class"],
|
||||
window: options.window,
|
||||
});
|
||||
}
|
||||
get x() {
|
||||
return this.#rect.x;
|
||||
}
|
||||
get y() {
|
||||
return this.#rect.y;
|
||||
}
|
||||
get width() {
|
||||
return this.#rect.width;
|
||||
}
|
||||
get height() {
|
||||
return this.#rect.height;
|
||||
}
|
||||
get top() {
|
||||
return this.#rect.top;
|
||||
}
|
||||
get right() {
|
||||
return this.#rect.right;
|
||||
}
|
||||
get bottom() {
|
||||
return this.#rect.bottom;
|
||||
}
|
||||
get left() {
|
||||
return this.#rect.left;
|
||||
}
|
||||
get current() {
|
||||
return this.#rect;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { ElementRect } from "./element-rect.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { ElementRect } from "./element-rect.svelte.js";
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { type ConfigurableWindow } from "../../internal/configurable-globals.js";
|
||||
import type { MaybeElementGetter } from "../../internal/types.js";
|
||||
export type ElementSizeOptions = ConfigurableWindow & {
|
||||
box?: "content-box" | "border-box";
|
||||
};
|
||||
/**
|
||||
* Returns a reactive value holding the size of `node`.
|
||||
*
|
||||
* Accepts an `options` object with the following properties:
|
||||
* - `initialSize`: The initial size of the element. Defaults to `{ width: 0, height: 0 }`.
|
||||
* - `box`: The box model to use. Can be either `"content-box"` or `"border-box"`. Defaults to `"border-box"`.
|
||||
*
|
||||
* @returns an object with `width` and `height` properties.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/element-size}
|
||||
*/
|
||||
export declare class ElementSize {
|
||||
#private;
|
||||
constructor(node: MaybeElementGetter, options?: ElementSizeOptions);
|
||||
calculateSize(): {
|
||||
width: number;
|
||||
height: number;
|
||||
} | undefined;
|
||||
getSize(): {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
get current(): {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
get width(): number;
|
||||
get height(): number;
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { defaultWindow } from "../../internal/configurable-globals.js";
|
||||
import { get } from "../../internal/utils/get.js";
|
||||
import { createSubscriber } from "svelte/reactivity";
|
||||
/**
|
||||
* Returns a reactive value holding the size of `node`.
|
||||
*
|
||||
* Accepts an `options` object with the following properties:
|
||||
* - `initialSize`: The initial size of the element. Defaults to `{ width: 0, height: 0 }`.
|
||||
* - `box`: The box model to use. Can be either `"content-box"` or `"border-box"`. Defaults to `"border-box"`.
|
||||
*
|
||||
* @returns an object with `width` and `height` properties.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/element-size}
|
||||
*/
|
||||
export class ElementSize {
|
||||
// no need to use `$state` here since we are using createSubscriber
|
||||
#size = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
#observed = false;
|
||||
#options;
|
||||
#node;
|
||||
#window;
|
||||
// we use a derived here to extract the width so that if the width doesn't change we don't get a state update
|
||||
// which we would get if we would just use a getter since the version of the subscriber will be changing
|
||||
#width = $derived.by(() => {
|
||||
this.#subscribe?.();
|
||||
return this.getSize().width;
|
||||
});
|
||||
// we use a derived here to extract the height so that if the height doesn't change we don't get a state update
|
||||
// which we would get if we would just use a getter since the version of the subscriber will be changing
|
||||
#height = $derived.by(() => {
|
||||
this.#subscribe?.();
|
||||
return this.getSize().height;
|
||||
});
|
||||
// we need to use a derived here because the class will be created before the node is bound to the ref
|
||||
#subscribe = $derived.by(() => {
|
||||
const node$ = get(this.#node);
|
||||
if (!node$)
|
||||
return;
|
||||
return createSubscriber((update) => {
|
||||
if (!this.#window)
|
||||
return;
|
||||
const observer = new this.#window.ResizeObserver((entries) => {
|
||||
this.#observed = true;
|
||||
for (const entry of entries) {
|
||||
const boxSize = this.#options.box === "content-box" ? entry.contentBoxSize : entry.borderBoxSize;
|
||||
const boxSizeArr = Array.isArray(boxSize) ? boxSize : [boxSize];
|
||||
this.#size.width = boxSizeArr.reduce((acc, size) => Math.max(acc, size.inlineSize), 0);
|
||||
this.#size.height = boxSizeArr.reduce((acc, size) => Math.max(acc, size.blockSize), 0);
|
||||
}
|
||||
update();
|
||||
});
|
||||
observer.observe(node$);
|
||||
return () => {
|
||||
this.#observed = false;
|
||||
observer.disconnect();
|
||||
};
|
||||
});
|
||||
});
|
||||
constructor(node, options = { box: "border-box" }) {
|
||||
this.#window = options.window ?? defaultWindow;
|
||||
this.#options = options;
|
||||
this.#node = node;
|
||||
this.#size = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
}
|
||||
calculateSize() {
|
||||
const element = get(this.#node);
|
||||
// no element or no window, return undefined, we will return 0x0 in the getSize method
|
||||
if (!element || !this.#window) {
|
||||
return;
|
||||
}
|
||||
const offsetWidth = element.offsetWidth;
|
||||
const offsetHeight = element.offsetHeight;
|
||||
// easy mode, just return offsets
|
||||
if (this.#options.box === "border-box") {
|
||||
return {
|
||||
width: offsetWidth,
|
||||
height: offsetHeight,
|
||||
};
|
||||
}
|
||||
// hard mode, we need to calculate the content size
|
||||
const style = this.#window.getComputedStyle(element);
|
||||
const paddingWidth = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
||||
const paddingHeight = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
|
||||
const borderWidth = parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth);
|
||||
const borderHeight = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
|
||||
const contentWidth = offsetWidth - paddingWidth - borderWidth;
|
||||
const contentHeight = offsetHeight - paddingHeight - borderHeight;
|
||||
return {
|
||||
width: contentWidth,
|
||||
height: contentHeight,
|
||||
};
|
||||
}
|
||||
getSize() {
|
||||
// if the resize observer already run we can just return the size
|
||||
// otherwise we calculate the size if possible or we return the initial size
|
||||
return this.#observed ? this.#size : (this.calculateSize() ?? this.#size);
|
||||
}
|
||||
get current() {
|
||||
this.#subscribe?.();
|
||||
return this.getSize();
|
||||
}
|
||||
get width() {
|
||||
return this.#width;
|
||||
}
|
||||
get height() {
|
||||
return this.#height;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./element-size.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./element-size.svelte.js";
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import type { MaybeGetter } from "../../internal/types.js";
|
||||
/**
|
||||
* Resolves a value that may be a getter function or a direct value.
|
||||
*
|
||||
* If the input is a function, it will be invoked to retrieve the actual value.
|
||||
* If the resolved value (or the input itself) is `undefined`, the optional
|
||||
* `defaultValue` is returned instead.
|
||||
*
|
||||
* @template T - The expected return type.
|
||||
* @param value - A value or a function that returns a value.
|
||||
* @param defaultValue - A fallback value returned if the resolved value is `undefined`.
|
||||
* @returns The resolved value or the default.
|
||||
*/
|
||||
export declare function extract<T>(value: MaybeGetter<T>): T;
|
||||
export declare function extract<T>(value: MaybeGetter<T | undefined>, defaultValue: T): T;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { isFunction } from "../../internal/utils/is.js";
|
||||
export function extract(value, defaultValue) {
|
||||
if (isFunction(value)) {
|
||||
const getter = value;
|
||||
const gotten = getter();
|
||||
if (gotten === undefined)
|
||||
return defaultValue;
|
||||
return gotten;
|
||||
}
|
||||
if (value === undefined)
|
||||
return defaultValue;
|
||||
return value;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { extract } from "./extract.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { extract } from "./extract.svelte.js";
|
||||
Generated
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
export type FSMLifecycleFn<StatesT extends string, EventsT extends string> = (meta: LifecycleFnMeta<StatesT, EventsT>) => void;
|
||||
export type LifecycleFnMeta<StatesT extends string, EventsT extends string> = {
|
||||
from: StatesT | null;
|
||||
to: StatesT;
|
||||
event: EventsT | null;
|
||||
args: unknown;
|
||||
};
|
||||
export declare function isLifecycleFnMeta<StatesT extends string, EventsT extends string>(meta: unknown): meta is LifecycleFnMeta<StatesT, EventsT>;
|
||||
export type FSMLifecycle = "_enter" | "_exit";
|
||||
export type ActionFn<StatesT> = (...args: unknown[]) => StatesT | void;
|
||||
export type Action<StatesT> = StatesT | ActionFn<StatesT>;
|
||||
export type StateHandler<StatesT extends string, EventsT extends string> = {
|
||||
[e in EventsT]?: Action<StatesT>;
|
||||
} & {
|
||||
[k in FSMLifecycle]?: FSMLifecycleFn<StatesT, EventsT>;
|
||||
};
|
||||
export type Transition<StatesT extends string, EventsT extends string> = {
|
||||
[s in StatesT]: StateHandler<StatesT, EventsT>;
|
||||
} & {
|
||||
"*"?: StateHandler<StatesT, EventsT>;
|
||||
};
|
||||
/**
|
||||
* Defines a strongly-typed finite state machine.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/finite-state-machine}
|
||||
*/
|
||||
export declare class FiniteStateMachine<StatesT extends string, EventsT extends string> {
|
||||
#private;
|
||||
readonly states: Transition<StatesT, EventsT>;
|
||||
constructor(initial: StatesT, states: Transition<StatesT, EventsT>);
|
||||
/** Triggers a new event and returns the new state. */
|
||||
send(event: EventsT, ...args: unknown[]): StatesT;
|
||||
/** Debounces the triggering of an event. */
|
||||
debounce(wait: number | undefined, event: EventsT, ...args: unknown[]): Promise<StatesT>;
|
||||
/** The current state. */
|
||||
get current(): StatesT;
|
||||
}
|
||||
Generated
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
export function isLifecycleFnMeta(meta) {
|
||||
return (!!meta &&
|
||||
typeof meta === "object" &&
|
||||
"to" in meta &&
|
||||
"from" in meta &&
|
||||
"event" in meta &&
|
||||
"args" in meta);
|
||||
}
|
||||
/**
|
||||
* Defines a strongly-typed finite state machine.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/finite-state-machine}
|
||||
*/
|
||||
export class FiniteStateMachine {
|
||||
#current = $state();
|
||||
states;
|
||||
#timeout = {};
|
||||
constructor(initial, states) {
|
||||
this.#current = initial;
|
||||
this.states = states;
|
||||
this.send = this.send.bind(this);
|
||||
this.debounce = this.debounce.bind(this);
|
||||
// synthetically trigger _enter for the initial state.
|
||||
this.#dispatch("_enter", { from: null, to: initial, event: null, args: [] });
|
||||
}
|
||||
#transition(newState, event, args) {
|
||||
const metadata = { from: this.#current, to: newState, event, args };
|
||||
this.#dispatch("_exit", metadata);
|
||||
this.#current = newState;
|
||||
this.#dispatch("_enter", metadata);
|
||||
}
|
||||
#dispatch(event, ...args) {
|
||||
const action = this.states[this.#current]?.[event] ?? this.states["*"]?.[event];
|
||||
if (action instanceof Function) {
|
||||
if (event === "_enter" || event === "_exit") {
|
||||
if (isLifecycleFnMeta(args[0])) {
|
||||
action(args[0]);
|
||||
}
|
||||
else {
|
||||
console.warn("Invalid metadata passed to lifecycle function of the FSM.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
return action(...args);
|
||||
}
|
||||
}
|
||||
else if (typeof action === "string") {
|
||||
return action;
|
||||
}
|
||||
else if (event !== "_enter" && event !== "_exit") {
|
||||
console.warn("No action defined for event", event, "in state", this.#current);
|
||||
}
|
||||
}
|
||||
/** Triggers a new event and returns the new state. */
|
||||
send(event, ...args) {
|
||||
const newState = this.#dispatch(event, ...args);
|
||||
if (newState && newState !== this.#current) {
|
||||
this.#transition(newState, event, args);
|
||||
}
|
||||
return this.#current;
|
||||
}
|
||||
/** Debounces the triggering of an event. */
|
||||
async debounce(wait = 500, event, ...args) {
|
||||
if (this.#timeout[event]) {
|
||||
clearTimeout(this.#timeout[event]);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.#timeout[event] = setTimeout(() => {
|
||||
delete this.#timeout[event];
|
||||
resolve(this.send(event, ...args));
|
||||
}, wait);
|
||||
});
|
||||
}
|
||||
/** The current state. */
|
||||
get current() {
|
||||
return this.#current;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./finite-state-machine.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./finite-state-machine.svelte.js";
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
export * from "./active-element/index.js";
|
||||
export * from "./animation-frames/index.js";
|
||||
export * from "./context/index.js";
|
||||
export * from "./debounced/index.js";
|
||||
export * from "./element-rect/index.js";
|
||||
export * from "./element-size/index.js";
|
||||
export * from "./extract/index.js";
|
||||
export * from "./finite-state-machine/index.js";
|
||||
export * from "./is-focus-within/index.js";
|
||||
export * from "./is-idle/index.js";
|
||||
export * from "./is-in-viewport/index.js";
|
||||
export * from "./is-mounted/index.js";
|
||||
export * from "./is-document-visible/index.js";
|
||||
export * from "./on-click-outside/index.js";
|
||||
export * from "./persisted-state/index.js";
|
||||
export * from "./pressed-keys/index.js";
|
||||
export * from "./previous/index.js";
|
||||
export * from "./resource/index.js";
|
||||
export * from "./scroll-state/index.js";
|
||||
export * from "./state-history/index.js";
|
||||
export * from "./textarea-autosize/index.js";
|
||||
export * from "./throttled/index.js";
|
||||
export * from "./use-debounce/index.js";
|
||||
export * from "./use-event-listener/index.js";
|
||||
export * from "./use-geolocation/index.js";
|
||||
export * from "./use-intersection-observer/index.js";
|
||||
export * from "./use-mutation-observer/index.js";
|
||||
export * from "./use-resize-observer/index.js";
|
||||
export * from "./use-interval/index.js";
|
||||
export * from "./use-throttle/index.js";
|
||||
export * from "./watch/index.js";
|
||||
export * from "./scroll-state/index.js";
|
||||
export * from "./bool-attr/index.js";
|
||||
export * from "./on-cleanup/index.js";
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
export * from "./active-element/index.js";
|
||||
export * from "./animation-frames/index.js";
|
||||
export * from "./context/index.js";
|
||||
export * from "./debounced/index.js";
|
||||
export * from "./element-rect/index.js";
|
||||
export * from "./element-size/index.js";
|
||||
export * from "./extract/index.js";
|
||||
export * from "./finite-state-machine/index.js";
|
||||
export * from "./is-focus-within/index.js";
|
||||
export * from "./is-idle/index.js";
|
||||
export * from "./is-in-viewport/index.js";
|
||||
export * from "./is-mounted/index.js";
|
||||
export * from "./is-document-visible/index.js";
|
||||
export * from "./on-click-outside/index.js";
|
||||
export * from "./persisted-state/index.js";
|
||||
export * from "./pressed-keys/index.js";
|
||||
export * from "./previous/index.js";
|
||||
export * from "./resource/index.js";
|
||||
export * from "./scroll-state/index.js";
|
||||
export * from "./state-history/index.js";
|
||||
export * from "./textarea-autosize/index.js";
|
||||
export * from "./throttled/index.js";
|
||||
export * from "./use-debounce/index.js";
|
||||
export * from "./use-event-listener/index.js";
|
||||
export * from "./use-geolocation/index.js";
|
||||
export * from "./use-intersection-observer/index.js";
|
||||
export * from "./use-mutation-observer/index.js";
|
||||
export * from "./use-resize-observer/index.js";
|
||||
export * from "./use-interval/index.js";
|
||||
export * from "./use-throttle/index.js";
|
||||
export * from "./watch/index.js";
|
||||
export * from "./scroll-state/index.js";
|
||||
export * from "./bool-attr/index.js";
|
||||
export * from "./on-cleanup/index.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./is-document-visible.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./is-document-visible.svelte.js";
|
||||
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import { type ConfigurableDocument, type ConfigurableWindow } from "../../internal/configurable-globals.js";
|
||||
export type IsDocumentVisibleOptions = ConfigurableWindow & ConfigurableDocument;
|
||||
/**
|
||||
* Tracks whether the current document is visible (i.e., not hidden).
|
||||
* It listens to the "visibilitychange" event and updates reactively.
|
||||
*
|
||||
* @see {@link https://developer.mozilla.org/docs/Web/API/Document/visibilitychange_event}
|
||||
* @see {@link https://runed.dev/docs/utilities/is-document-visible}
|
||||
*/
|
||||
export declare class IsDocumentVisible {
|
||||
#private;
|
||||
constructor(options?: IsDocumentVisibleOptions);
|
||||
get current(): boolean;
|
||||
}
|
||||
Generated
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
import { defaultWindow, } from "../../internal/configurable-globals.js";
|
||||
import { on } from "svelte/events";
|
||||
/**
|
||||
* Tracks whether the current document is visible (i.e., not hidden).
|
||||
* It listens to the "visibilitychange" event and updates reactively.
|
||||
*
|
||||
* @see {@link https://developer.mozilla.org/docs/Web/API/Document/visibilitychange_event}
|
||||
* @see {@link https://runed.dev/docs/utilities/is-document-visible}
|
||||
*/
|
||||
export class IsDocumentVisible {
|
||||
#visible = $state(false);
|
||||
constructor(options = {}) {
|
||||
const window = options.window ?? defaultWindow;
|
||||
const document = options.document ?? window?.document;
|
||||
this.#visible = document ? !document.hidden : false;
|
||||
$effect(() => {
|
||||
if (!document)
|
||||
return;
|
||||
return on(document, "visibilitychange", () => {
|
||||
this.#visible = !document.hidden;
|
||||
});
|
||||
});
|
||||
}
|
||||
get current() {
|
||||
return this.#visible;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./is-focus-within.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./is-focus-within.svelte.js";
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import type { MaybeElementGetter } from "../../internal/types.js";
|
||||
import { type ActiveElementOptions } from "../active-element/active-element.svelte.js";
|
||||
export interface IsFocusWithinOptions extends ActiveElementOptions {
|
||||
}
|
||||
/**
|
||||
* Tracks whether the focus is within a target element.
|
||||
* @see {@link https://runed.dev/docs/utilities/is-focus-within}
|
||||
*/
|
||||
export declare class IsFocusWithin {
|
||||
#private;
|
||||
constructor(node: MaybeElementGetter, options?: IsFocusWithinOptions);
|
||||
readonly current: boolean;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { ActiveElement, } from "../active-element/active-element.svelte.js";
|
||||
import { extract } from "../extract/extract.svelte.js";
|
||||
/**
|
||||
* Tracks whether the focus is within a target element.
|
||||
* @see {@link https://runed.dev/docs/utilities/is-focus-within}
|
||||
*/
|
||||
export class IsFocusWithin {
|
||||
#node;
|
||||
#activeElement;
|
||||
constructor(node, options = {}) {
|
||||
this.#node = node;
|
||||
this.#activeElement = new ActiveElement(options);
|
||||
}
|
||||
current = $derived.by(() => {
|
||||
const node = extract(this.#node);
|
||||
if (node == null)
|
||||
return false;
|
||||
return node.contains(this.#activeElement.current);
|
||||
});
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./is-idle.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./is-idle.svelte.js";
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import type { MaybeGetter } from "../../internal/types.js";
|
||||
import { type ConfigurableDocument, type ConfigurableWindow } from "../../internal/configurable-globals.js";
|
||||
export type IsIdleOptions = ConfigurableDocument & ConfigurableWindow & {
|
||||
/**
|
||||
* The events that should set the idle state to `true`
|
||||
*
|
||||
* @default ['mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel']
|
||||
*/
|
||||
events?: MaybeGetter<(keyof WindowEventMap)[]>;
|
||||
/**
|
||||
* The timeout in milliseconds before the idle state is set to `true`. Defaults to 60 seconds.
|
||||
*
|
||||
* @default 60000
|
||||
*/
|
||||
timeout?: MaybeGetter<number>;
|
||||
/**
|
||||
* Detect document visibility changes
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
detectVisibilityChanges?: MaybeGetter<boolean>;
|
||||
/**
|
||||
* The initial state of the idle property
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
initialState?: boolean;
|
||||
};
|
||||
/**
|
||||
* Tracks whether the user is being inactive.
|
||||
* @see {@link https://runed.dev/docs/utilities/is-idle}
|
||||
*/
|
||||
export declare class IsIdle {
|
||||
#private;
|
||||
constructor(_options?: IsIdleOptions);
|
||||
get lastActive(): number;
|
||||
get current(): boolean;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { extract } from "../extract/index.js";
|
||||
import { useDebounce } from "../use-debounce/index.js";
|
||||
import { useEventListener } from "../use-event-listener/use-event-listener.svelte.js";
|
||||
import { defaultWindow, } from "../../internal/configurable-globals.js";
|
||||
const DEFAULT_EVENTS = [
|
||||
"keypress",
|
||||
"mousemove",
|
||||
"touchmove",
|
||||
"click",
|
||||
"scroll",
|
||||
];
|
||||
const DEFAULT_OPTIONS = {
|
||||
events: DEFAULT_EVENTS,
|
||||
initialState: false,
|
||||
timeout: 60000,
|
||||
};
|
||||
/**
|
||||
* Tracks whether the user is being inactive.
|
||||
* @see {@link https://runed.dev/docs/utilities/is-idle}
|
||||
*/
|
||||
export class IsIdle {
|
||||
#current = $state(false);
|
||||
#lastActive = $state(Date.now());
|
||||
constructor(_options) {
|
||||
const opts = {
|
||||
...DEFAULT_OPTIONS,
|
||||
..._options,
|
||||
};
|
||||
const window = opts.window ?? defaultWindow;
|
||||
const document = opts.document ?? window?.document;
|
||||
const timeout = $derived(extract(opts.timeout));
|
||||
const events = $derived(extract(opts.events));
|
||||
const detectVisibilityChanges = $derived(extract(opts.detectVisibilityChanges));
|
||||
this.#current = opts.initialState;
|
||||
const debouncedReset = useDebounce(() => {
|
||||
this.#current = true;
|
||||
}, () => timeout);
|
||||
debouncedReset();
|
||||
const handleActivity = () => {
|
||||
this.#current = false;
|
||||
this.#lastActive = Date.now();
|
||||
debouncedReset();
|
||||
};
|
||||
useEventListener(() => window, () => events, () => {
|
||||
handleActivity();
|
||||
}, { passive: true });
|
||||
$effect(() => {
|
||||
if (!detectVisibilityChanges || !document)
|
||||
return;
|
||||
useEventListener(document, ["visibilitychange"], () => {
|
||||
if (document.hidden)
|
||||
return;
|
||||
handleActivity();
|
||||
});
|
||||
});
|
||||
}
|
||||
get lastActive() {
|
||||
return this.#lastActive;
|
||||
}
|
||||
get current() {
|
||||
return this.#current;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./is-in-viewport.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./is-in-viewport.svelte.js";
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import type { ConfigurableWindow } from "../../internal/configurable-globals.js";
|
||||
import type { MaybeElementGetter } from "../../internal/types.js";
|
||||
import { type UseIntersectionObserverOptions } from "../use-intersection-observer/use-intersection-observer.svelte.js";
|
||||
export type IsInViewportOptions = ConfigurableWindow & UseIntersectionObserverOptions;
|
||||
/**
|
||||
* Tracks if an element is visible within the current viewport.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/is-in-viewport}
|
||||
*/
|
||||
export declare class IsInViewport {
|
||||
#private;
|
||||
constructor(node: MaybeElementGetter, options?: IsInViewportOptions);
|
||||
get current(): boolean;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { useIntersectionObserver, } from "../use-intersection-observer/use-intersection-observer.svelte.js";
|
||||
/**
|
||||
* Tracks if an element is visible within the current viewport.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/is-in-viewport}
|
||||
*/
|
||||
export class IsInViewport {
|
||||
#isInViewport = $state(false);
|
||||
constructor(node, options) {
|
||||
useIntersectionObserver(node, (intersectionObserverEntries) => {
|
||||
let isIntersecting = this.#isInViewport;
|
||||
let latestTime = 0;
|
||||
for (const entry of intersectionObserverEntries) {
|
||||
if (entry.time >= latestTime) {
|
||||
latestTime = entry.time;
|
||||
isIntersecting = entry.isIntersecting;
|
||||
}
|
||||
}
|
||||
this.#isInViewport = isIntersecting;
|
||||
}, options);
|
||||
}
|
||||
get current() {
|
||||
return this.#isInViewport;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./is-mounted.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./is-mounted.svelte.js";
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Returns an object with the mounted state of the component
|
||||
* that invokes this function.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/is-mounted}
|
||||
*/
|
||||
export declare class IsMounted {
|
||||
#private;
|
||||
constructor();
|
||||
get current(): boolean;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { untrack } from "svelte";
|
||||
/**
|
||||
* Returns an object with the mounted state of the component
|
||||
* that invokes this function.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/is-mounted}
|
||||
*/
|
||||
export class IsMounted {
|
||||
#isMounted = $state(false);
|
||||
constructor() {
|
||||
$effect(() => {
|
||||
untrack(() => (this.#isMounted = true));
|
||||
return () => {
|
||||
this.#isMounted = false;
|
||||
};
|
||||
});
|
||||
}
|
||||
get current() {
|
||||
return this.#isMounted;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { onCleanup } from "./on-cleanup.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { onCleanup } from "./on-cleanup.svelte.js";
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Register a cleanup function that will be called when the current effect context is disposed,
|
||||
* which could be when a component is destroyed or when a root effect is disposed.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/on-cleanup}
|
||||
*
|
||||
* @param cb - The cleanup function to register.
|
||||
*/
|
||||
export declare function onCleanup(cb: () => void): void;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Register a cleanup function that will be called when the current effect context is disposed,
|
||||
* which could be when a component is destroyed or when a root effect is disposed.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/on-cleanup}
|
||||
*
|
||||
* @param cb - The cleanup function to register.
|
||||
*/
|
||||
export function onCleanup(cb) {
|
||||
$effect(() => {
|
||||
return () => {
|
||||
cb();
|
||||
};
|
||||
});
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./on-click-outside.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./on-click-outside.svelte.js";
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { type ConfigurableDocument, type ConfigurableWindow } from "../../internal/configurable-globals.js";
|
||||
import type { MaybeElementGetter } from "../../internal/types.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<T>} 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
|
||||
* <script>
|
||||
* import { onClickOutside } from 'runed'
|
||||
* let container = $state<HTMLElement>()!
|
||||
*
|
||||
* const clickOutside = onClickOutside(() => container, () => {
|
||||
* console.log('clicked outside the container!')
|
||||
* });
|
||||
* </script>
|
||||
*
|
||||
* <div bind:this={container}>
|
||||
* <span>Inside</span>
|
||||
* </div>
|
||||
* <button>Outside, click me to trigger callback</button>
|
||||
* <button onclick={clickOutside.start}>Start</button>
|
||||
* <button onclick={clickOutside.stop}>Stop</button>
|
||||
* <span>Enabled: {clickOutside.enabled}</span>
|
||||
*```
|
||||
* @see {@link https://runed.dev/docs/utilities/on-click-outside}
|
||||
*/
|
||||
export declare function onClickOutside<T extends Element = HTMLElement>(container: MaybeElementGetter<T>, 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;
|
||||
};
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import { defaultWindow, } from "../../internal/configurable-globals.js";
|
||||
import { getActiveElement, getOwnerDocument, isOrContainsTarget } from "../../internal/utils/dom.js";
|
||||
import { noop } from "../../internal/utils/function.js";
|
||||
import { isElement } from "../../internal/utils/is.js";
|
||||
import { sleep } from "../../internal/utils/sleep.js";
|
||||
import { on } from "svelte/events";
|
||||
import { extract } from "../extract/extract.svelte.js";
|
||||
import { useDebounce } from "../use-debounce/use-debounce.svelte.js";
|
||||
import { watch } from "../watch/watch.svelte.js";
|
||||
/**
|
||||
* 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<T>} 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
|
||||
* <script>
|
||||
* import { onClickOutside } from 'runed'
|
||||
* let container = $state<HTMLElement>()!
|
||||
*
|
||||
* const clickOutside = onClickOutside(() => container, () => {
|
||||
* console.log('clicked outside the container!')
|
||||
* });
|
||||
* </script>
|
||||
*
|
||||
* <div bind:this={container}>
|
||||
* <span>Inside</span>
|
||||
* </div>
|
||||
* <button>Outside, click me to trigger callback</button>
|
||||
* <button onclick={clickOutside.start}>Start</button>
|
||||
* <button onclick={clickOutside.stop}>Stop</button>
|
||||
* <span>Enabled: {clickOutside.enabled}</span>
|
||||
*```
|
||||
* @see {@link https://runed.dev/docs/utilities/on-click-outside}
|
||||
*/
|
||||
export function onClickOutside(container, callback, opts = {}) {
|
||||
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) => {
|
||||
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 = on(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.
|
||||
*/
|
||||
on(nodeOwnerDocument, "pointerdown", (e) => {
|
||||
if (isValidEvent(e, node, nodeOwnerDocument)) {
|
||||
pointerDownIntercepted = true;
|
||||
}
|
||||
}, { capture: true }),
|
||||
/**
|
||||
* BUBBLE INTERACTION START
|
||||
* Mark the pointerdown event as non-intercepted. Debounce `handleClickOutside` to
|
||||
* avoid prematurely checking if other events were intercepted.
|
||||
*/
|
||||
on(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.
|
||||
*/
|
||||
on(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, container, defaultDocument) {
|
||||
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);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./persisted-state.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./persisted-state.svelte.js";
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { type ConfigurableWindow } from "../../internal/configurable-globals.js";
|
||||
type Serializer<T> = {
|
||||
serialize: (value: T) => string;
|
||||
deserialize: (value: string) => T | undefined;
|
||||
};
|
||||
type StorageType = "local" | "session";
|
||||
type PersistedStateOptions<T> = {
|
||||
/** The storage type to use. Defaults to `local`. */
|
||||
storage?: StorageType;
|
||||
/** The serializer to use. Defaults to `JSON.stringify` and `JSON.parse`. */
|
||||
serializer?: Serializer<T>;
|
||||
/** Whether to sync with the state changes from other tabs. Defaults to `true`. */
|
||||
syncTabs?: boolean;
|
||||
} & ConfigurableWindow;
|
||||
/**
|
||||
* Creates reactive state that is persisted and synchronized across browser sessions and tabs using Web Storage.
|
||||
* @param key The unique key used to store the state in the storage.
|
||||
* @param initialValue The initial value of the state if not already present in the storage.
|
||||
* @param options Configuration options including storage type, serializer for complex data types, and whether to sync state changes across tabs.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/persisted-state}
|
||||
*/
|
||||
export declare class PersistedState<T> {
|
||||
#private;
|
||||
constructor(key: string, initialValue: T, options?: PersistedStateOptions<T>);
|
||||
get current(): T;
|
||||
set current(newValue: T);
|
||||
}
|
||||
export {};
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { defaultWindow } from "../../internal/configurable-globals.js";
|
||||
import { on } from "svelte/events";
|
||||
import { createSubscriber } from "svelte/reactivity";
|
||||
function getStorage(storageType, window) {
|
||||
switch (storageType) {
|
||||
case "local":
|
||||
return window.localStorage;
|
||||
case "session":
|
||||
return window.sessionStorage;
|
||||
}
|
||||
}
|
||||
function proxy(value, root, proxies, subscribe, update, serialize) {
|
||||
if (value === null || typeof value !== "object") {
|
||||
return value;
|
||||
}
|
||||
const proto = Object.getPrototypeOf(value);
|
||||
if (proto !== null && proto !== Object.prototype && !Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
let p = proxies.get(value);
|
||||
if (!p) {
|
||||
p = new Proxy(value, {
|
||||
get: (target, property) => {
|
||||
subscribe?.();
|
||||
return proxy(Reflect.get(target, property), root, proxies, subscribe, update, serialize);
|
||||
},
|
||||
set: (target, property, value) => {
|
||||
update?.();
|
||||
Reflect.set(target, property, value);
|
||||
serialize(root);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
proxies.set(value, p);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
/**
|
||||
* Creates reactive state that is persisted and synchronized across browser sessions and tabs using Web Storage.
|
||||
* @param key The unique key used to store the state in the storage.
|
||||
* @param initialValue The initial value of the state if not already present in the storage.
|
||||
* @param options Configuration options including storage type, serializer for complex data types, and whether to sync state changes across tabs.
|
||||
*
|
||||
* @see {@link https://runed.dev/docs/utilities/persisted-state}
|
||||
*/
|
||||
export class PersistedState {
|
||||
#current;
|
||||
#key;
|
||||
#serializer;
|
||||
#storage;
|
||||
#subscribe;
|
||||
#update;
|
||||
#proxies = new WeakMap();
|
||||
constructor(key, initialValue, options = {}) {
|
||||
const { storage: storageType = "local", serializer = { serialize: JSON.stringify, deserialize: JSON.parse }, syncTabs = true, } = options;
|
||||
const window = "window" in options ? options.window : defaultWindow; // window is not mockable to be undefined without this, because JavaScript will fill undefined with `= default`
|
||||
this.#current = initialValue;
|
||||
this.#key = key;
|
||||
this.#serializer = serializer;
|
||||
if (window === undefined)
|
||||
return;
|
||||
const storage = getStorage(storageType, window);
|
||||
this.#storage = storage;
|
||||
const existingValue = storage.getItem(key);
|
||||
if (existingValue !== null) {
|
||||
this.#current = this.#deserialize(existingValue);
|
||||
}
|
||||
else {
|
||||
this.#serialize(initialValue);
|
||||
}
|
||||
this.#subscribe = createSubscriber((update) => {
|
||||
this.#update = update;
|
||||
const cleanup = syncTabs && storageType === "local"
|
||||
? on(window, "storage", this.#handleStorageEvent)
|
||||
: null;
|
||||
return () => {
|
||||
cleanup?.();
|
||||
this.#update = undefined;
|
||||
};
|
||||
});
|
||||
}
|
||||
get current() {
|
||||
this.#subscribe?.();
|
||||
const storageItem = this.#storage?.getItem(this.#key);
|
||||
const root = storageItem ? this.#deserialize(storageItem) : this.#current;
|
||||
return proxy(root, root, this.#proxies, this.#subscribe?.bind(this), this.#update?.bind(this), this.#serialize.bind(this));
|
||||
}
|
||||
set current(newValue) {
|
||||
this.#serialize(newValue);
|
||||
this.#update?.();
|
||||
}
|
||||
#handleStorageEvent = (event) => {
|
||||
if (event.key !== this.#key || event.newValue === null)
|
||||
return;
|
||||
this.#current = this.#deserialize(event.newValue);
|
||||
this.#update?.();
|
||||
};
|
||||
#deserialize(value) {
|
||||
try {
|
||||
return this.#serializer.deserialize(value);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Error when parsing "${value}" from persisted store "${this.#key}"`, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#serialize(value) {
|
||||
try {
|
||||
if (value != undefined) {
|
||||
this.#storage?.setItem(this.#key, this.#serializer.serialize(value));
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Error when writing value from persisted store "${this.#key}" to ${this.#storage}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { PressedKeys } from "./pressed-keys.svelte.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { PressedKeys } from "./pressed-keys.svelte.js";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user