From ba8f8c95b130ca09f344bdd77e6cfa44310d8999 Mon Sep 17 00:00:00 2001 From: Hunter Johnston <64506580+huntabyte@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:36:29 -0600 Subject: [PATCH] feat: `useGelocation` (#165) --- .changeset/gentle-plants-tan.md | 5 + .changeset/young-comics-punch.md | 5 + eslint.config.js | 1 + .../IsSupported/IsSupported.svelte.ts | 4 +- packages/runed/src/lib/utilities/index.ts | 1 + .../src/lib/utilities/useGeolocation/index.ts | 1 + .../useGeolocation/useGeolocation.svelte.ts | 97 +++++++++++++++++++ scripts/add-utility.mjs | 2 +- .../src/content/utilities/is-supported.md | 8 ++ .../src/content/utilities/use-geolocation.md | 61 ++++++++++++ .../components/demos/use-geolocation.svelte | 17 ++++ 11 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 .changeset/gentle-plants-tan.md create mode 100644 .changeset/young-comics-punch.md create mode 100644 packages/runed/src/lib/utilities/useGeolocation/index.ts create mode 100644 packages/runed/src/lib/utilities/useGeolocation/useGeolocation.svelte.ts create mode 100644 sites/docs/src/content/utilities/use-geolocation.md create mode 100644 sites/docs/src/lib/components/demos/use-geolocation.svelte diff --git a/.changeset/gentle-plants-tan.md b/.changeset/gentle-plants-tan.md new file mode 100644 index 00000000..873bbf4c --- /dev/null +++ b/.changeset/gentle-plants-tan.md @@ -0,0 +1,5 @@ +--- +"runed": minor +--- + +change: handle boolean conversion within `IsSupported` to improve DX diff --git a/.changeset/young-comics-punch.md b/.changeset/young-comics-punch.md new file mode 100644 index 00000000..d2ebdf40 --- /dev/null +++ b/.changeset/young-comics-punch.md @@ -0,0 +1,5 @@ +--- +"runed": minor +--- + +feat: `useGeolocation` diff --git a/eslint.config.js b/eslint.config.js index 0016803c..d4a1b922 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -37,6 +37,7 @@ export default tseslint.config( ], "@typescript-eslint/no-unused-expressions": "off", "@typescript-eslint/no-empty-object-type": "off", + "prefer-const": "off", }, }, { diff --git a/packages/runed/src/lib/utilities/IsSupported/IsSupported.svelte.ts b/packages/runed/src/lib/utilities/IsSupported/IsSupported.svelte.ts index 71f928f0..c15fbc90 100644 --- a/packages/runed/src/lib/utilities/IsSupported/IsSupported.svelte.ts +++ b/packages/runed/src/lib/utilities/IsSupported/IsSupported.svelte.ts @@ -8,9 +8,9 @@ export class IsSupported { #current: boolean = $state(false); - constructor(predicate: () => boolean) { + constructor(predicate: () => unknown) { $effect(() => { - this.#current = predicate(); + this.#current = Boolean(predicate()); }); } diff --git a/packages/runed/src/lib/utilities/index.ts b/packages/runed/src/lib/utilities/index.ts index b4e77985..196f290e 100644 --- a/packages/runed/src/lib/utilities/index.ts +++ b/packages/runed/src/lib/utilities/index.ts @@ -21,3 +21,4 @@ export * from "./useIntersectionObserver/index.js"; export * from "./IsFocusWithin/index.js"; export * from "./FiniteStateMachine/index.js"; export * from "./PersistedState/index.js"; +export * from "./useGeolocation/index.js"; diff --git a/packages/runed/src/lib/utilities/useGeolocation/index.ts b/packages/runed/src/lib/utilities/useGeolocation/index.ts new file mode 100644 index 00000000..9b59393a --- /dev/null +++ b/packages/runed/src/lib/utilities/useGeolocation/index.ts @@ -0,0 +1 @@ +export * from "./useGeolocation.svelte.js"; diff --git a/packages/runed/src/lib/utilities/useGeolocation/useGeolocation.svelte.ts b/packages/runed/src/lib/utilities/useGeolocation/useGeolocation.svelte.ts new file mode 100644 index 00000000..c2b4982d --- /dev/null +++ b/packages/runed/src/lib/utilities/useGeolocation/useGeolocation.svelte.ts @@ -0,0 +1,97 @@ +import { IsSupported } from "../IsSupported/IsSupported.svelte.js"; + +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 WritableProperties = { + -readonly [P in keyof T]: T[P]; +}; + +/** + * Reactive access to the browser's [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API). + * + * @see https://runed.dev/docs/utilities/use-geolocation + */ +export function useGeolocation(options: UseGeolocationOptions = {}) { + const { + enableHighAccuracy = true, + maximumAge = 30000, + timeout = 27000, + immediate = true, + } = options; + + const isSupported = new IsSupported(() => navigator && "geolocation" in navigator); + + let locatedAt = $state(null); + let error = $state.raw(null); + let coords = $state>>({ + accuracy: 0, + latitude: Number.POSITIVE_INFINITY, + longitude: Number.POSITIVE_INFINITY, + altitude: null, + altitudeAccuracy: null, + heading: null, + speed: null, + }); + let isPaused = $state(false); + + function updatePosition(position: GeolocationPosition) { + locatedAt = position.timestamp; + coords.accuracy = position.coords.accuracy; + coords.altitude = position.coords.altitude; + coords.altitudeAccuracy = position.coords.altitudeAccuracy; + coords.heading = position.coords.heading; + coords.latitude = position.coords.latitude; + coords.longitude = position.coords.longitude; + coords.speed = position.coords.speed; + } + + let watcher: number; + + function resume() { + if (!isSupported.current) return; + watcher = navigator!.geolocation.watchPosition(updatePosition, (err) => (error = err), { + enableHighAccuracy, + maximumAge, + timeout, + }); + isPaused = false; + } + + function pause() { + if (watcher && navigator) navigator.geolocation.clearWatch(watcher); + isPaused = true; + } + + $effect(() => { + if (immediate) resume(); + return () => pause(); + }); + + return { + get isSupported() { + return isSupported.current; + }, + get coords() { + return coords; + }, + get locatedAt() { + return locatedAt; + }, + get error() { + return error; + }, + get isPaused() { + return isPaused; + }, + resume, + pause, + }; +} diff --git a/scripts/add-utility.mjs b/scripts/add-utility.mjs index 0f32157c..ae8e7b8a 100644 --- a/scripts/add-utility.mjs +++ b/scripts/add-utility.mjs @@ -51,7 +51,7 @@ fs.writeFileSync( ` diff --git a/sites/docs/src/content/utilities/is-supported.md b/sites/docs/src/content/utilities/is-supported.md index 1d51c3fe..bf37adb1 100644 --- a/sites/docs/src/content/utilities/is-supported.md +++ b/sites/docs/src/content/utilities/is-supported.md @@ -17,3 +17,11 @@ category: Utilities } ``` + +## Type Definition + +```ts +class IsSupported { + readonly current: boolean; +} +``` diff --git a/sites/docs/src/content/utilities/use-geolocation.md b/sites/docs/src/content/utilities/use-geolocation.md new file mode 100644 index 00000000..61370c1a --- /dev/null +++ b/sites/docs/src/content/utilities/use-geolocation.md @@ -0,0 +1,61 @@ +--- +title: useGeolocation +description: Reactive access to the browser's Geolocation API. +category: Browser +--- + + + +`useGeolocation` is a reactive wrapper around the browser's +[Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API). + +## Demo + + + +## Usage + +```svelte + + +
Coords: {JSON.stringify(location.coords, null, 2)}
+
Located at: {location.locatedAt}
+
Error: {JSON.stringify(location.error, null, 2)}
+
Is Supported: {location.isSupported}
+ + +``` + +## Type Definitions + +```ts +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; +}; +``` + +``` + +``` diff --git a/sites/docs/src/lib/components/demos/use-geolocation.svelte b/sites/docs/src/lib/components/demos/use-geolocation.svelte new file mode 100644 index 00000000..35708a87 --- /dev/null +++ b/sites/docs/src/lib/components/demos/use-geolocation.svelte @@ -0,0 +1,17 @@ + + + +
Coords: {JSON.stringify(location.coords, null, 2)}
+
Located at: {location.locatedAt}
+
Error: {JSON.stringify(location.error, null, 2)}
+
Is Supported: {location.isSupported}
+
+ + +
+