feat: add `svelte/reactivity/window` module

pull/14660/head
Rich Harris 2 years ago
parent ab1f7f437e
commit ea6f89d604

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: add `svelte/reactivity/window` module

@ -0,0 +1,15 @@
---
title: svelte/reactivity/window
---
This module exports reactive versions of various `window` properties, which you can use in components and [deriveds]($derived) and [effects]($effect) without using [`<svelte:window>`](svelte-window) bindings or manually creating your own event listeners.
```svelte
<script>
import { innerWidth, innerHeight } from 'svelte/reactivity/window';
</script>
<p>{innerWidth.current}x{innerHeight.current}</p>
```
> MODULE: svelte/reactivity/window

@ -73,6 +73,10 @@
"browser": "./src/reactivity/index-client.js",
"default": "./src/reactivity/index-server.js"
},
"./reactivity/window": {
"types": "./types/index.d.ts",
"default": "./src/reactivity/window/index.js"
},
"./server": {
"types": "./types/index.d.ts",
"default": "./src/server/index.js"

@ -35,6 +35,7 @@ await createBundle({
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
[`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`,
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index-client.js`,
[`${pkg.name}/reactivity/window`]: `${dir}/src/reactivity/window/index.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.d.ts`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,

@ -1,7 +1,7 @@
import { run_all } from '../../shared/utils.js';
// Fallback for when requestIdleCallback is not available
const request_idle_callback =
export const request_idle_callback =
typeof requestIdleCallback === 'undefined'
? (/** @type {() => void} */ cb) => setTimeout(cb, 1)
: requestIdleCallback;

@ -1,5 +1,5 @@
import { createSubscriber } from './create-subscriber.js';
import { on } from '../events/index.js';
import { ReactiveValue } from './reactive-value.js';
/**
* Creates a media query and provides a `current` property that reflects whether or not it matches.
@ -16,26 +16,19 @@ import { on } from '../events/index.js';
*
* <h1>{large.current ? 'large screen' : 'small screen'}</h1>
* ```
* @extends {ReactiveValue<boolean>}
* @since 5.7.0
*/
export class MediaQuery {
#query;
#subscribe = createSubscriber((update) => {
return on(this.#query, 'change', update);
});
get current() {
this.#subscribe();
return this.#query.matches;
}
export class MediaQuery extends ReactiveValue {
/**
* @param {string} query A media query string
* @param {boolean} [matches] Fallback value for the server
* @param {boolean} [fallback] Fallback value for the server
*/
constructor(query, matches) {
// For convenience (and because people likely forget them) we add the parentheses; double parentheses are not a problem
this.#query = window.matchMedia(`(${query})`);
constructor(query, fallback) {
const q = window.matchMedia(`(${query})`);
super(
() => q.matches,
(update) => on(q, 'change', update)
);
}
}

@ -0,0 +1,24 @@
import { createSubscriber } from './create-subscriber.js';
/**
* @template T
*/
export class ReactiveValue {
#fn;
#subscribe;
/**
*
* @param {() => T} fn
* @param {(update: () => void) => void} onsubscribe
*/
constructor(fn, onsubscribe) {
this.#fn = fn;
this.#subscribe = createSubscriber(onsubscribe);
}
get current() {
this.#subscribe();
return this.#fn();
}
}

@ -0,0 +1,128 @@
import { BROWSER } from 'esm-env';
import { on } from '../../events/index.js';
import { ReactiveValue } from '../reactive-value.js';
import { get } from '../../internal/client';
import { set, source } from '../../internal/client/reactivity/sources.js';
/**
* `scrollX.current` is a reactive view of `window.scrollX`. On the server it is `undefined`
*/
export const scrollX = new ReactiveValue(
BROWSER ? () => window.scrollX : () => undefined,
(update) => on(window, 'scroll', update)
);
/**
* `scrollY.current` is a reactive view of `window.scrollY`. On the server it is `undefined`
*/
export const scrollY = new ReactiveValue(
BROWSER ? () => window.scrollY : () => undefined,
(update) => on(window, 'scroll', update)
);
/**
* `innerWidth.current` is a reactive view of `window.innerWidth`. On the server it is `undefined`
*/
export const innerWidth = new ReactiveValue(
BROWSER ? () => window.innerWidth : () => undefined,
(update) => on(window, 'resize', update)
);
/**
* `innerHeight.current` is a reactive view of `window.innerHeight`. On the server it is `undefined`
*/
export const innerHeight = new ReactiveValue(
BROWSER ? () => window.innerHeight : () => undefined,
(update) => on(window, 'resize', update)
);
/**
* `outerWidth.current` is a reactive view of `window.outerWidth`. On the server it is `undefined`
*/
export const outerWidth = new ReactiveValue(
BROWSER ? () => window.outerWidth : () => undefined,
(update) => on(window, 'resize', update)
);
/**
* `outerHeight.current` is a reactive view of `window.outerHeight`. On the server it is `undefined`
*/
export const outerHeight = new ReactiveValue(
BROWSER ? () => window.outerHeight : () => undefined,
(update) => on(window, 'resize', update)
);
/**
* `screenLeft.current` is a reactive view of `window.screenLeft`. On the server it is `undefined`
*/
export const screenLeft = new ReactiveValue(
BROWSER ? () => window.screenLeft : () => undefined,
(update) => {
let screenLeft = window.screenLeft;
let frame = requestAnimationFrame(function check() {
frame = requestAnimationFrame(check);
if (screenLeft !== (screenLeft = window.screenLeft)) {
update();
}
});
return () => {
cancelAnimationFrame(frame);
};
}
);
/**
* `screenTop.current` is a reactive view of `window.screenTop`. On the server it is `undefined`
*/
export const screenTop = new ReactiveValue(
BROWSER ? () => window.screenTop : () => undefined,
(update) => {
let screenTop = window.screenTop;
let frame = requestAnimationFrame(function check() {
frame = requestAnimationFrame(check);
if (screenTop !== (screenTop = window.screenTop)) {
update();
}
});
return () => {
cancelAnimationFrame(frame);
};
}
);
/**
* `devicePixelRatio.current` is a reactive view of `window.devicePixelRatio`. On the server it is `undefined`.
* Note that behaviour differs between browsers on Chrome it will respond to the current zoom level,
* on Firefox and Safari it won't
*/
export const devicePixelRatio = /* @__PURE__ */ new (class DevicePixelRatio {
#dpr = source(BROWSER ? window.devicePixelRatio : undefined);
#update() {
const off = on(
window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`),
'change',
() => {
set(this.#dpr, window.devicePixelRatio);
off();
this.#update();
}
);
}
constructor() {
this.#update();
}
get current() {
get(this.#dpr);
return window.devicePixelRatio;
}
})();

@ -1900,16 +1900,15 @@ declare module 'svelte/reactivity' {
*
* <h1>{large.current ? 'large screen' : 'small screen'}</h1>
* ```
* @extends {ReactiveValue<boolean>}
* @since 5.7.0
*/
export class MediaQuery {
export class MediaQuery extends ReactiveValue<boolean> {
/**
* @param query A media query string
* @param matches Fallback value for the server
* @param fallback Fallback value for the server
*/
constructor(query: string, matches?: boolean | undefined);
get current(): boolean;
#private;
constructor(query: string, fallback?: boolean | undefined);
}
/**
* Returns a `subscribe` function that, if called in an effect (including expressions in the template),
@ -1953,6 +1952,199 @@ declare module 'svelte/reactivity' {
* @since 5.7.0
*/
export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void;
class ReactiveValue<T> {
constructor(fn: () => T, onsubscribe: (update: () => void) => void);
get current(): T;
#private;
}
export {};
}
declare module 'svelte/reactivity/window' {
/**
* `scrollX.current` is a reactive view of `window.scrollX`. On the server it is `undefined`
*/
export const scrollX: ReactiveValue<number | undefined>;
/**
* `scrollY.current` is a reactive view of `window.scrollY`. On the server it is `undefined`
*/
export const scrollY: ReactiveValue<number | undefined>;
/**
* `innerWidth.current` is a reactive view of `window.innerWidth`. On the server it is `undefined`
*/
export const innerWidth: ReactiveValue<number | undefined>;
/**
* `innerHeight.current` is a reactive view of `window.innerHeight`. On the server it is `undefined`
*/
export const innerHeight: ReactiveValue<number | undefined>;
/**
* `outerWidth.current` is a reactive view of `window.outerWidth`. On the server it is `undefined`
*/
export const outerWidth: ReactiveValue<number | undefined>;
/**
* `outerHeight.current` is a reactive view of `window.outerHeight`. On the server it is `undefined`
*/
export const outerHeight: ReactiveValue<number | undefined>;
/**
* `screenLeft.current` is a reactive view of `window.screenLeft`. On the server it is `undefined`
*/
export const screenLeft: ReactiveValue<number | undefined>;
/**
* `screenTop.current` is a reactive view of `window.screenTop`. On the server it is `undefined`
*/
export const screenTop: ReactiveValue<number | undefined>;
/**
* `devicePixelRatio.current` is a reactive view of `window.devicePixelRatio`. On the server it is `undefined`.
* Note that behaviour differs between browsers on Chrome it will respond to the current zoom level,
* on Firefox and Safari it won't
*/
export const devicePixelRatio: {
"__#19@#dpr": Source<number | undefined>;
"__#19@#update"(): void;
readonly current: number;
};
// For all the core internal objects, we use single-character property strings.
// This not only reduces code-size and parsing, but it also improves the performance
// when the JS VM JITs the code.
type ComponentContext = {
/** parent */
p: null | ComponentContext;
/** context */
c: null | Map<unknown, unknown>;
/** deferred effects */
e: null | Array<{
fn: () => void | (() => void);
effect: null | Effect;
reaction: null | Reaction;
}>;
/** mounted */
m: boolean;
/**
* props needed for legacy mode lifecycle functions, and for `createEventDispatcher`
* @deprecated remove in 6.0
*/
s: Record<string, unknown>;
/**
* exports (and props, if `accessors: true`) needed for `createEventDispatcher`
* @deprecated remove in 6.0
*/
x: Record<string, any> | null;
/**
* legacy stuff
* @deprecated remove in 6.0
*/
l: null | {
/** local signals (needed for beforeUpdate/afterUpdate) */
s: null | Source[];
/** update_callbacks */
u: null | {
/** afterUpdate callbacks */
a: Array<() => void>;
/** beforeUpdate callbacks */
b: Array<() => void>;
/** onMount callbacks */
m: Array<() => any>;
};
/** `$:` statements */
r1: any[];
/** This tracks whether `$:` statements have run in the current cycle, to ensure they only run once */
r2: Source<boolean>;
};
/**
* dev mode only: the component function
*/
function?: any;
};
type Equals = (this: Value, value: unknown) => boolean;
type TemplateNode = Text | Element | Comment;
interface TransitionManager {
/** Whether the `global` modifier was used (i.e. `transition:fade|global`) */
is_global: boolean;
/** Called inside `resume_effect` */
in: () => void;
/** Called inside `pause_effect` */
out: (callback?: () => void) => void;
/** Called inside `destroy_effect` */
stop: () => void;
}
class ReactiveValue<T> {
constructor(fn: () => T, onsubscribe: (update: () => void) => void);
get current(): T;
#private;
}
interface Signal {
/** Flags bitmask */
f: number;
/** Write version */
version: number;
}
interface Value<V = unknown> extends Signal {
/** Signals that read from this signal */
reactions: null | Reaction[];
/** Equality function */
equals: Equals;
/** The latest value for this signal */
v: V;
}
interface Reaction extends Signal {
/** The associated component context */
ctx: null | ComponentContext;
/** The reaction function */
fn: null | Function;
/** Signals that this signal reads from */
deps: null | Value[];
}
interface Derived<V = unknown> extends Value<V>, Reaction {
/** The derived function */
fn: () => V;
/** Reactions created inside this signal */
children: null | Reaction[];
/** Parent effect or derived */
parent: Effect | Derived | null;
}
interface Effect extends Reaction {
/**
* Branch effects store their start/end nodes so that they can be
* removed when the effect is destroyed, or moved when an `each`
* block is reconciled. In the case of a single text/element node,
* `start` and `end` will be the same.
*/
nodes_start: null | TemplateNode;
nodes_end: null | TemplateNode;
/** Reactions created inside this signal */
deriveds: null | Derived[];
/** The effect function */
fn: null | (() => void | (() => void));
/** The teardown function returned from the effect function */
teardown: null | (() => void);
/** Transition managers created with `$.transition` */
transitions: null | TransitionManager[];
/** Next sibling child effect created inside the parent signal */
prev: null | Effect;
/** Next sibling child effect created inside the parent signal */
next: null | Effect;
/** First child effect created inside this signal */
first: null | Effect;
/** Last child effect created inside this signal */
last: null | Effect;
/** Parent effect */
parent: Effect | null;
/** Dev only */
component_function?: any;
}
type Source<V = unknown> = Value<V>;
export {};
}

Loading…
Cancel
Save