@mmstack/primitives 20.5.4 → 20.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +276 -924
- package/fesm2022/mmstack-primitives.mjs +441 -80
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +258 -40
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, computed, signal, isSignal,
|
|
2
|
+
import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, computed, signal, isSignal, PLATFORM_ID, ElementRef, Injectable, runInInjectionContext } from '@angular/core';
|
|
3
3
|
import { isPlatformServer } from '@angular/common';
|
|
4
4
|
import { SIGNAL } from '@angular/core/primitives/signals';
|
|
5
5
|
|
|
@@ -353,9 +353,9 @@ function debounce(source, opt) {
|
|
|
353
353
|
const { is } = Object;
|
|
354
354
|
function mutable(initial, opt) {
|
|
355
355
|
const baseEqual = opt?.equal ?? is;
|
|
356
|
-
let
|
|
356
|
+
let cnt = 0;
|
|
357
357
|
const equal = (a, b) => {
|
|
358
|
-
if (
|
|
358
|
+
if (cnt > 0)
|
|
359
359
|
return false;
|
|
360
360
|
return baseEqual(a, b);
|
|
361
361
|
};
|
|
@@ -365,9 +365,9 @@ function mutable(initial, opt) {
|
|
|
365
365
|
});
|
|
366
366
|
const internalUpdate = sig.update;
|
|
367
367
|
sig.mutate = (updater) => {
|
|
368
|
-
|
|
368
|
+
cnt++;
|
|
369
369
|
internalUpdate(updater);
|
|
370
|
-
|
|
370
|
+
cnt--;
|
|
371
371
|
};
|
|
372
372
|
sig.inline = (updater) => {
|
|
373
373
|
sig.mutate((prev) => {
|
|
@@ -434,10 +434,10 @@ function derived(source, optOrKey, opt) {
|
|
|
434
434
|
};
|
|
435
435
|
const rest = typeof optOrKey === 'object' ? { ...optOrKey, ...opt } : opt;
|
|
436
436
|
const baseEqual = rest?.equal ?? Object.is;
|
|
437
|
-
let
|
|
437
|
+
let cnt = 0;
|
|
438
438
|
const equal = isMutable(source)
|
|
439
439
|
? (a, b) => {
|
|
440
|
-
if (
|
|
440
|
+
if (cnt > 0)
|
|
441
441
|
return false;
|
|
442
442
|
return baseEqual(a, b);
|
|
443
443
|
}
|
|
@@ -446,9 +446,9 @@ function derived(source, optOrKey, opt) {
|
|
|
446
446
|
sig.from = from;
|
|
447
447
|
if (isMutable(source)) {
|
|
448
448
|
sig.mutate = (updater) => {
|
|
449
|
-
|
|
449
|
+
cnt++;
|
|
450
450
|
sig.update(updater);
|
|
451
|
-
|
|
451
|
+
cnt--;
|
|
452
452
|
};
|
|
453
453
|
sig.inline = (updater) => {
|
|
454
454
|
sig.mutate((prev) => {
|
|
@@ -553,9 +553,11 @@ function indexArray(source, map, opt = {}) {
|
|
|
553
553
|
});
|
|
554
554
|
if (isWritableSignal(data) && isMutable(data) && !opt.equal) {
|
|
555
555
|
opt.equal = (a, b) => {
|
|
556
|
-
if (a !== b)
|
|
557
|
-
return false;
|
|
558
|
-
|
|
556
|
+
if (typeof a !== typeof b)
|
|
557
|
+
return false;
|
|
558
|
+
if (typeof a === 'object' || typeof a === 'function')
|
|
559
|
+
return false;
|
|
560
|
+
return a === b;
|
|
559
561
|
};
|
|
560
562
|
}
|
|
561
563
|
return linkedSignal({
|
|
@@ -777,10 +779,35 @@ const filter = (predicate) => (src) => linkedSignal({
|
|
|
777
779
|
},
|
|
778
780
|
});
|
|
779
781
|
/** tap into the value */
|
|
780
|
-
const tap = (fn) => (src) => {
|
|
781
|
-
effect(() => fn(src())
|
|
782
|
+
const tap = (fn, injector) => (src) => {
|
|
783
|
+
effect(() => fn(src()), {
|
|
784
|
+
injector,
|
|
785
|
+
});
|
|
782
786
|
return src;
|
|
783
787
|
};
|
|
788
|
+
/**
|
|
789
|
+
* Like {@link filter}, but emits `initial` until a value passes the predicate
|
|
790
|
+
* for the first time. Avoids the `T | undefined` return type.
|
|
791
|
+
*/
|
|
792
|
+
const filterWith = (predicate, initial) => (src) => linkedSignal({
|
|
793
|
+
source: src,
|
|
794
|
+
computation: (next, prev) => predicate(next) ? next : (prev?.value ?? initial),
|
|
795
|
+
});
|
|
796
|
+
/** Emits `initial` first, then mirrors source. */
|
|
797
|
+
const startWith = (initial) => (src) => linkedSignal({
|
|
798
|
+
source: src,
|
|
799
|
+
computation: (next, prev) => (prev === undefined ? initial : next),
|
|
800
|
+
});
|
|
801
|
+
/** Emits `[prev, curr]` pairs. The first emission has prev = undefined. */
|
|
802
|
+
const pairwise = () => (src) => linkedSignal({
|
|
803
|
+
source: src,
|
|
804
|
+
computation: (next, prev) => [prev?.source, next],
|
|
805
|
+
});
|
|
806
|
+
/** Reduce-like accumulator across emissions. */
|
|
807
|
+
const scan = (reducer, seed) => (src) => linkedSignal({
|
|
808
|
+
source: src,
|
|
809
|
+
computation: (next, prev) => reducer(prev?.value ?? seed, next),
|
|
810
|
+
});
|
|
784
811
|
|
|
785
812
|
/**
|
|
786
813
|
* Decorate any `Signal<T>` with a chainable `.pipe(...)` method.
|
|
@@ -878,28 +905,22 @@ function piped(initial, opt) {
|
|
|
878
905
|
function pooled({ create, reset, computation, ...opt }) {
|
|
879
906
|
let other = opt.eager ? create() : undefined;
|
|
880
907
|
let current = opt.eager ? create() : undefined;
|
|
881
|
-
|
|
908
|
+
// only relevant for eager mode: the pre-allocated `current` is fresh until its first demote
|
|
882
909
|
let currentFresh = opt.eager;
|
|
883
910
|
return computed(() => {
|
|
884
|
-
|
|
885
|
-
let nextFresh;
|
|
886
|
-
if (other !== undefined) {
|
|
887
|
-
next = other;
|
|
888
|
-
nextFresh = !!otherFresh;
|
|
889
|
-
}
|
|
890
|
-
else {
|
|
891
|
-
next = untracked(() => create());
|
|
892
|
-
nextFresh = true;
|
|
893
|
-
}
|
|
911
|
+
const next = other ?? untracked(() => create());
|
|
894
912
|
if (current !== undefined) {
|
|
895
|
-
|
|
896
|
-
|
|
913
|
+
if (currentFresh) {
|
|
914
|
+
other = current;
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by the `current !== undefined` check above
|
|
918
|
+
other = untracked(() => reset(current)) ?? current;
|
|
919
|
+
}
|
|
897
920
|
}
|
|
898
921
|
current = next;
|
|
899
|
-
// the buffer is about to be mutated by `computation`, so it's no longer fresh
|
|
900
922
|
currentFresh = false;
|
|
901
|
-
|
|
902
|
-
return computation(clean);
|
|
923
|
+
return computation(next);
|
|
903
924
|
}, opt);
|
|
904
925
|
}
|
|
905
926
|
|
|
@@ -940,6 +961,87 @@ function pooledMap(optOrComputation, signalOpt) {
|
|
|
940
961
|
return pooled(toPooledOptions(optOrComputation, createEmptyMap, resetClearable, signalOpt));
|
|
941
962
|
}
|
|
942
963
|
|
|
964
|
+
const EVENTS = [
|
|
965
|
+
'chargingchange',
|
|
966
|
+
'levelchange',
|
|
967
|
+
'chargingtimechange',
|
|
968
|
+
'dischargingtimechange',
|
|
969
|
+
];
|
|
970
|
+
/**
|
|
971
|
+
* Creates a read-only signal that tracks the system battery status using the
|
|
972
|
+
* Battery Status API. Returns `null` until the underlying `getBattery()`
|
|
973
|
+
* promise resolves, or permanently when the API is unsupported (Firefox /
|
|
974
|
+
* Safari at the time of writing). SSR-safe.
|
|
975
|
+
*/
|
|
976
|
+
function batteryStatus(debugName = 'batteryStatus') {
|
|
977
|
+
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
978
|
+
typeof navigator === 'undefined' ||
|
|
979
|
+
typeof navigator.getBattery !== 'function') {
|
|
980
|
+
return computed(() => null, { debugName });
|
|
981
|
+
}
|
|
982
|
+
const state = signal(null, ...(ngDevMode ? [{ debugName: "state", debugName }] : [{ debugName }]));
|
|
983
|
+
const abortController = new AbortController();
|
|
984
|
+
inject(DestroyRef).onDestroy(() => abortController.abort());
|
|
985
|
+
navigator.getBattery().then((battery) => {
|
|
986
|
+
if (abortController.signal.aborted)
|
|
987
|
+
return;
|
|
988
|
+
const read = () => ({
|
|
989
|
+
level: battery.level,
|
|
990
|
+
charging: battery.charging,
|
|
991
|
+
chargingTime: battery.chargingTime,
|
|
992
|
+
dischargingTime: battery.dischargingTime,
|
|
993
|
+
});
|
|
994
|
+
const onChange = () => state.set(read());
|
|
995
|
+
state.set(read());
|
|
996
|
+
for (const ev of EVENTS) {
|
|
997
|
+
battery.addEventListener(ev, onChange, {
|
|
998
|
+
signal: abortController.signal,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
return state.asReadonly();
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Creates a read-only signal mirroring the system clipboard contents.
|
|
1007
|
+
*
|
|
1008
|
+
* The signal value starts empty and updates whenever a `copy` event fires on
|
|
1009
|
+
* the document (or {@link ClipboardSignal.copy} is invoked from this app).
|
|
1010
|
+
* SSR-safe — returns `''` and `isSupported: false` on the server.
|
|
1011
|
+
*
|
|
1012
|
+
* Note: read access requires the Clipboard API and an active permission grant
|
|
1013
|
+
* in browsers that gate it. Errors from `navigator.clipboard.readText` are
|
|
1014
|
+
* swallowed silently to keep the signal value stable.
|
|
1015
|
+
*/
|
|
1016
|
+
function clipboard(debugName = 'clipboard') {
|
|
1017
|
+
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
1018
|
+
typeof navigator === 'undefined' ||
|
|
1019
|
+
!navigator.clipboard) {
|
|
1020
|
+
const sig = computed(() => '', { debugName });
|
|
1021
|
+
sig.copy = () => Promise.resolve();
|
|
1022
|
+
sig.isSupported = computed(() => false, ...(ngDevMode ? [{ debugName: "isSupported" }] : []));
|
|
1023
|
+
return sig;
|
|
1024
|
+
}
|
|
1025
|
+
const state = signal('', ...(ngDevMode ? [{ debugName: "state", debugName }] : [{ debugName }]));
|
|
1026
|
+
const refresh = () => {
|
|
1027
|
+
navigator.clipboard.readText().then((value) => state.set(value), () => {
|
|
1028
|
+
// permission denied / focus required — ignore
|
|
1029
|
+
});
|
|
1030
|
+
};
|
|
1031
|
+
const abortController = new AbortController();
|
|
1032
|
+
const onCopy = () => refresh();
|
|
1033
|
+
document.addEventListener('copy', onCopy, { signal: abortController.signal });
|
|
1034
|
+
document.addEventListener('cut', onCopy, { signal: abortController.signal });
|
|
1035
|
+
inject(DestroyRef).onDestroy(() => abortController.abort());
|
|
1036
|
+
const sig = state.asReadonly();
|
|
1037
|
+
sig.copy = async (value) => {
|
|
1038
|
+
await navigator.clipboard.writeText(value);
|
|
1039
|
+
state.set(value);
|
|
1040
|
+
};
|
|
1041
|
+
sig.isSupported = computed(() => true, ...(ngDevMode ? [{ debugName: "isSupported" }] : []));
|
|
1042
|
+
return sig;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
943
1045
|
function observerSupported$1() {
|
|
944
1046
|
return typeof ResizeObserver !== 'undefined';
|
|
945
1047
|
}
|
|
@@ -1140,6 +1242,167 @@ function elementVisibility(target = inject(ElementRef), opt) {
|
|
|
1140
1242
|
return base;
|
|
1141
1243
|
}
|
|
1142
1244
|
|
|
1245
|
+
function unwrap$1(target) {
|
|
1246
|
+
if (!target)
|
|
1247
|
+
return null;
|
|
1248
|
+
return target instanceof ElementRef ? target.nativeElement : target;
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Creates a read-only signal that tracks whether the focused element is the
|
|
1252
|
+
* target or a descendant of it. Mirrors the CSS `:focus-within` pseudo-class.
|
|
1253
|
+
*
|
|
1254
|
+
* Defaults `target` to the current `ElementRef` so it can be used inline in a
|
|
1255
|
+
* component's `class` field. SSR-safe — returns a constant `false` signal on
|
|
1256
|
+
* the server.
|
|
1257
|
+
*/
|
|
1258
|
+
function focusWithin(target = inject(ElementRef)) {
|
|
1259
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
1260
|
+
return computed(() => false, { debugName: 'focusWithin' });
|
|
1261
|
+
}
|
|
1262
|
+
const state = signal(false, { debugName: 'focusWithin' });
|
|
1263
|
+
const attach = (el) => {
|
|
1264
|
+
state.set(el.contains(document.activeElement));
|
|
1265
|
+
const abortController = new AbortController();
|
|
1266
|
+
el.addEventListener('focusin', () => state.set(true), {
|
|
1267
|
+
signal: abortController.signal,
|
|
1268
|
+
});
|
|
1269
|
+
el.addEventListener('focusout', () => {
|
|
1270
|
+
// Defer so `document.activeElement` reflects the focus move.
|
|
1271
|
+
queueMicrotask(() => state.set(el.contains(document.activeElement)));
|
|
1272
|
+
}, { signal: abortController.signal });
|
|
1273
|
+
return () => abortController.abort();
|
|
1274
|
+
};
|
|
1275
|
+
if (isSignal(target)) {
|
|
1276
|
+
const targetSig = target;
|
|
1277
|
+
effect((cleanup) => {
|
|
1278
|
+
const el = unwrap$1(targetSig());
|
|
1279
|
+
if (!el) {
|
|
1280
|
+
state.set(false);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
cleanup(attach(el));
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
const el = unwrap$1(target);
|
|
1288
|
+
if (el) {
|
|
1289
|
+
const detach = attach(el);
|
|
1290
|
+
inject(DestroyRef).onDestroy(detach);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return state.asReadonly();
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Creates a read-only signal that exposes the current geolocation position.
|
|
1298
|
+
*
|
|
1299
|
+
* The returned signal carries `error` and `loading` sub-signals for permission
|
|
1300
|
+
* failures and the in-flight initial fetch respectively. SSR-safe — on the
|
|
1301
|
+
* server the position is `null`, loading is `false`, and no API calls are made.
|
|
1302
|
+
*
|
|
1303
|
+
* @example
|
|
1304
|
+
* ```ts
|
|
1305
|
+
* const where = geolocation({ watch: true, enableHighAccuracy: true });
|
|
1306
|
+
* effect(() => console.log(where()?.coords, where.error()));
|
|
1307
|
+
* ```
|
|
1308
|
+
*/
|
|
1309
|
+
function geolocation(opt) {
|
|
1310
|
+
if (isPlatformServer(inject(PLATFORM_ID)) || typeof navigator === 'undefined' || !navigator.geolocation) {
|
|
1311
|
+
const sig = computed(() => null, {
|
|
1312
|
+
debugName: opt?.debugName ?? 'geolocation',
|
|
1313
|
+
});
|
|
1314
|
+
sig.error = computed(() => null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
1315
|
+
sig.loading = computed(() => false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
1316
|
+
return sig;
|
|
1317
|
+
}
|
|
1318
|
+
const position = signal(null, {
|
|
1319
|
+
debugName: opt?.debugName ?? 'geolocation',
|
|
1320
|
+
});
|
|
1321
|
+
const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
1322
|
+
const loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
1323
|
+
const onSuccess = (p) => {
|
|
1324
|
+
position.set(p);
|
|
1325
|
+
error.set(null);
|
|
1326
|
+
loading.set(false);
|
|
1327
|
+
};
|
|
1328
|
+
const onError = (e) => {
|
|
1329
|
+
error.set(e);
|
|
1330
|
+
loading.set(false);
|
|
1331
|
+
};
|
|
1332
|
+
if (opt?.watch) {
|
|
1333
|
+
const watchId = navigator.geolocation.watchPosition(onSuccess, onError, opt);
|
|
1334
|
+
inject(DestroyRef).onDestroy(() => navigator.geolocation.clearWatch(watchId));
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
navigator.geolocation.getCurrentPosition(onSuccess, onError, opt);
|
|
1338
|
+
}
|
|
1339
|
+
const sig = position.asReadonly();
|
|
1340
|
+
sig.error = error.asReadonly();
|
|
1341
|
+
sig.loading = loading.asReadonly();
|
|
1342
|
+
return sig;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const DEFAULT_EVENTS = [
|
|
1346
|
+
'mousemove',
|
|
1347
|
+
'keydown',
|
|
1348
|
+
'touchstart',
|
|
1349
|
+
'scroll',
|
|
1350
|
+
'visibilitychange',
|
|
1351
|
+
];
|
|
1352
|
+
const serverDate$1 = new Date();
|
|
1353
|
+
/**
|
|
1354
|
+
* Creates a read-only signal that flips to `true` after a window of user
|
|
1355
|
+
* inactivity. Any of the configured `events` (default: pointer/keyboard/scroll
|
|
1356
|
+
* activity) resets the timer and flips the signal back to `false`.
|
|
1357
|
+
*
|
|
1358
|
+
* SSR-safe — always `false` with a frozen `since` date on the server.
|
|
1359
|
+
*/
|
|
1360
|
+
function idle(opt) {
|
|
1361
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
1362
|
+
const sig = computed(() => false, {
|
|
1363
|
+
debugName: opt?.debugName ?? 'idle',
|
|
1364
|
+
});
|
|
1365
|
+
sig.since = computed(() => serverDate$1, ...(ngDevMode ? [{ debugName: "since" }] : []));
|
|
1366
|
+
return sig;
|
|
1367
|
+
}
|
|
1368
|
+
const ms = opt?.ms ?? 60_000;
|
|
1369
|
+
const events = opt?.events ?? DEFAULT_EVENTS;
|
|
1370
|
+
const state = signal(false, { debugName: opt?.debugName ?? 'idle' });
|
|
1371
|
+
const since = signal(new Date(), ...(ngDevMode ? [{ debugName: "since" }] : []));
|
|
1372
|
+
let timer;
|
|
1373
|
+
const goIdle = () => {
|
|
1374
|
+
if (state())
|
|
1375
|
+
return;
|
|
1376
|
+
state.set(true);
|
|
1377
|
+
since.set(new Date());
|
|
1378
|
+
};
|
|
1379
|
+
const reset = () => {
|
|
1380
|
+
if (timer)
|
|
1381
|
+
clearTimeout(timer);
|
|
1382
|
+
if (state()) {
|
|
1383
|
+
state.set(false);
|
|
1384
|
+
since.set(new Date());
|
|
1385
|
+
}
|
|
1386
|
+
timer = setTimeout(goIdle, ms);
|
|
1387
|
+
};
|
|
1388
|
+
const abortController = new AbortController();
|
|
1389
|
+
for (const ev of events) {
|
|
1390
|
+
window.addEventListener(ev, reset, {
|
|
1391
|
+
passive: true,
|
|
1392
|
+
signal: abortController.signal,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
timer = setTimeout(goIdle, ms);
|
|
1396
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1397
|
+
if (timer)
|
|
1398
|
+
clearTimeout(timer);
|
|
1399
|
+
abortController.abort();
|
|
1400
|
+
});
|
|
1401
|
+
const sig = state.asReadonly();
|
|
1402
|
+
sig.since = since.asReadonly();
|
|
1403
|
+
return sig;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1143
1406
|
/**
|
|
1144
1407
|
* Creates a read-only signal that reactively tracks whether a CSS media query
|
|
1145
1408
|
* string currently matches.
|
|
@@ -1301,26 +1564,41 @@ function throttled(initial, opt) {
|
|
|
1301
1564
|
*/
|
|
1302
1565
|
function throttle(source, opt) {
|
|
1303
1566
|
const ms = opt?.ms ?? 0;
|
|
1567
|
+
const leading = opt?.leading ?? false;
|
|
1568
|
+
const trailing = opt?.trailing ?? true;
|
|
1304
1569
|
const trigger = signal(false, ...(ngDevMode ? [{ debugName: "trigger" }] : []));
|
|
1570
|
+
const fire = () => trigger.update((c) => !c);
|
|
1305
1571
|
let timeout;
|
|
1572
|
+
let pendingTrailing = false;
|
|
1306
1573
|
try {
|
|
1307
1574
|
const destroyRef = opt?.destroyRef ?? inject(DestroyRef, { optional: true });
|
|
1308
1575
|
destroyRef?.onDestroy(() => {
|
|
1309
1576
|
if (timeout)
|
|
1310
1577
|
clearTimeout(timeout);
|
|
1311
1578
|
timeout = undefined;
|
|
1579
|
+
pendingTrailing = false;
|
|
1312
1580
|
});
|
|
1313
1581
|
}
|
|
1314
1582
|
catch {
|
|
1315
1583
|
// not in injection context & no destroyRef provided opting out of cleanup
|
|
1316
1584
|
}
|
|
1317
1585
|
const tick = () => {
|
|
1318
|
-
if (timeout)
|
|
1586
|
+
if (!timeout) {
|
|
1587
|
+
if (leading)
|
|
1588
|
+
fire();
|
|
1589
|
+
else
|
|
1590
|
+
pendingTrailing = trailing;
|
|
1591
|
+
timeout = setTimeout(() => {
|
|
1592
|
+
timeout = undefined;
|
|
1593
|
+
if (trailing && pendingTrailing) {
|
|
1594
|
+
pendingTrailing = false;
|
|
1595
|
+
fire();
|
|
1596
|
+
}
|
|
1597
|
+
}, ms);
|
|
1319
1598
|
return;
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
}, ms);
|
|
1599
|
+
}
|
|
1600
|
+
if (trailing)
|
|
1601
|
+
pendingTrailing = true;
|
|
1324
1602
|
};
|
|
1325
1603
|
const set = (value) => {
|
|
1326
1604
|
source.set(value);
|
|
@@ -1473,6 +1751,38 @@ function networkStatus(debugName = 'networkStatus') {
|
|
|
1473
1751
|
return sig;
|
|
1474
1752
|
}
|
|
1475
1753
|
|
|
1754
|
+
const SSR_FALLBACK = {
|
|
1755
|
+
angle: 0,
|
|
1756
|
+
type: 'portrait-primary',
|
|
1757
|
+
};
|
|
1758
|
+
/**
|
|
1759
|
+
* Creates a read-only signal that tracks `screen.orientation`.
|
|
1760
|
+
*
|
|
1761
|
+
* SSR-safe — returns a constant `portrait-primary / 0°` signal on the server
|
|
1762
|
+
* and in environments without `screen.orientation` support.
|
|
1763
|
+
*/
|
|
1764
|
+
function orientation(debugName = 'orientation') {
|
|
1765
|
+
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
1766
|
+
typeof screen === 'undefined' ||
|
|
1767
|
+
!screen.orientation) {
|
|
1768
|
+
return computed(() => SSR_FALLBACK, { debugName });
|
|
1769
|
+
}
|
|
1770
|
+
const so = screen.orientation;
|
|
1771
|
+
const read = () => ({
|
|
1772
|
+
angle: so.angle,
|
|
1773
|
+
type: so.type,
|
|
1774
|
+
});
|
|
1775
|
+
const state = signal(read(), ...(ngDevMode ? [{ debugName: "state", debugName,
|
|
1776
|
+
equal: (a, b) => a.angle === b.angle && a.type === b.type }] : [{
|
|
1777
|
+
debugName,
|
|
1778
|
+
equal: (a, b) => a.angle === b.angle && a.type === b.type,
|
|
1779
|
+
}]));
|
|
1780
|
+
const onChange = () => state.set(read());
|
|
1781
|
+
so.addEventListener('change', onChange);
|
|
1782
|
+
inject(DestroyRef).onDestroy(() => so.removeEventListener('change', onChange));
|
|
1783
|
+
return state.asReadonly();
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1476
1786
|
/**
|
|
1477
1787
|
* Creates a read-only signal that tracks the page's visibility state.
|
|
1478
1788
|
*
|
|
@@ -1689,35 +1999,42 @@ function windowSize(opt) {
|
|
|
1689
1999
|
* @internal
|
|
1690
2000
|
*/
|
|
1691
2001
|
function sensor(type, options) {
|
|
2002
|
+
const opts = options;
|
|
1692
2003
|
switch (type) {
|
|
1693
2004
|
case 'mousePosition':
|
|
1694
|
-
return mousePosition(
|
|
2005
|
+
return mousePosition(opts);
|
|
1695
2006
|
case 'networkStatus':
|
|
1696
|
-
return networkStatus(
|
|
2007
|
+
return networkStatus(opts?.debugName);
|
|
1697
2008
|
case 'pageVisibility':
|
|
1698
|
-
return pageVisibility(
|
|
2009
|
+
return pageVisibility(opts?.debugName);
|
|
1699
2010
|
case 'darkMode':
|
|
1700
2011
|
case 'dark-mode':
|
|
1701
|
-
return prefersDarkMode(
|
|
2012
|
+
return prefersDarkMode(opts?.debugName);
|
|
1702
2013
|
case 'reducedMotion':
|
|
1703
2014
|
case 'reduced-motion':
|
|
1704
|
-
return prefersReducedMotion(
|
|
1705
|
-
case 'mediaQuery':
|
|
1706
|
-
|
|
1707
|
-
return mediaQuery(opt.query, opt.debugName);
|
|
1708
|
-
}
|
|
2015
|
+
return prefersReducedMotion(opts?.debugName);
|
|
2016
|
+
case 'mediaQuery':
|
|
2017
|
+
return mediaQuery(opts.query, opts.debugName);
|
|
1709
2018
|
case 'windowSize':
|
|
1710
|
-
return windowSize(
|
|
2019
|
+
return windowSize(opts);
|
|
1711
2020
|
case 'scrollPosition':
|
|
1712
|
-
return scrollPosition(
|
|
1713
|
-
case 'elementVisibility':
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
case '
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
2021
|
+
return scrollPosition(opts);
|
|
2022
|
+
case 'elementVisibility':
|
|
2023
|
+
return elementVisibility(opts?.target, opts);
|
|
2024
|
+
case 'elementSize':
|
|
2025
|
+
return elementSize(opts?.target, opts);
|
|
2026
|
+
case 'geolocation':
|
|
2027
|
+
return geolocation(opts);
|
|
2028
|
+
case 'clipboard':
|
|
2029
|
+
return clipboard(opts?.debugName);
|
|
2030
|
+
case 'orientation':
|
|
2031
|
+
return orientation(opts?.debugName);
|
|
2032
|
+
case 'batteryStatus':
|
|
2033
|
+
return batteryStatus(opts?.debugName);
|
|
2034
|
+
case 'idle':
|
|
2035
|
+
return idle(opts);
|
|
2036
|
+
case 'focusWithin':
|
|
2037
|
+
return focusWithin(opts?.target);
|
|
1721
2038
|
default:
|
|
1722
2039
|
throw new Error(`Unknown sensor type: ${type}`);
|
|
1723
2040
|
}
|
|
@@ -1729,6 +2046,49 @@ function sensors(track, opt) {
|
|
|
1729
2046
|
}, {});
|
|
1730
2047
|
}
|
|
1731
2048
|
|
|
2049
|
+
function unwrap(t) {
|
|
2050
|
+
if (!t)
|
|
2051
|
+
return null;
|
|
2052
|
+
return t instanceof ElementRef ? t.nativeElement : t;
|
|
2053
|
+
}
|
|
2054
|
+
function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
|
|
2055
|
+
const project = typeof projectOrOpt === 'function' ? projectOrOpt : undefined;
|
|
2056
|
+
const opt = typeof projectOrOpt === 'function' ? maybeOpt : projectOrOpt;
|
|
2057
|
+
const injector = opt?.injector ?? inject(Injector);
|
|
2058
|
+
if (isPlatformServer(injector.get(PLATFORM_ID))) {
|
|
2059
|
+
return computed(() => initial, { debugName: opt?.debugName });
|
|
2060
|
+
}
|
|
2061
|
+
const state = signal(initial, {
|
|
2062
|
+
debugName: opt?.debugName,
|
|
2063
|
+
});
|
|
2064
|
+
const handler = (event) => {
|
|
2065
|
+
if (project)
|
|
2066
|
+
state.set(project(event));
|
|
2067
|
+
else
|
|
2068
|
+
state.set(event);
|
|
2069
|
+
};
|
|
2070
|
+
const { destroyRef: providedDestroyRef, ...listenerOpts } = opt ?? {};
|
|
2071
|
+
if (isSignal(target)) {
|
|
2072
|
+
const targetSig = target;
|
|
2073
|
+
effect((cleanup) => {
|
|
2074
|
+
const resolved = unwrap(targetSig());
|
|
2075
|
+
if (!resolved)
|
|
2076
|
+
return;
|
|
2077
|
+
resolved.addEventListener(eventName, handler, listenerOpts);
|
|
2078
|
+
cleanup(() => resolved.removeEventListener(eventName, handler, listenerOpts));
|
|
2079
|
+
}, { injector });
|
|
2080
|
+
}
|
|
2081
|
+
else {
|
|
2082
|
+
const resolved = unwrap(target);
|
|
2083
|
+
if (resolved) {
|
|
2084
|
+
resolved.addEventListener(eventName, handler, listenerOpts);
|
|
2085
|
+
const destroyRef = providedDestroyRef ?? injector.get(DestroyRef);
|
|
2086
|
+
destroyRef.onDestroy(() => resolved.removeEventListener(eventName, handler, listenerOpts));
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
return untracked(() => state.asReadonly());
|
|
2090
|
+
}
|
|
2091
|
+
|
|
1732
2092
|
const IS_STORE = Symbol('MMSTACK::IS_STORE');
|
|
1733
2093
|
const PROXY_CACHE = new WeakMap();
|
|
1734
2094
|
const SIGNAL_FN_PROP = new Set([
|
|
@@ -2228,14 +2588,11 @@ function generateDeterministicID() {
|
|
|
2228
2588
|
*
|
|
2229
2589
|
* @template T - The type of the WritableSignal
|
|
2230
2590
|
* @param sig - The WritableSignal to synchronize across tabs
|
|
2231
|
-
* @param opt -
|
|
2232
|
-
* @param opt.id - Explicit channel ID for synchronization.
|
|
2233
|
-
* a deterministic ID is generated based on the call site.
|
|
2234
|
-
* Use explicit IDs in production for reliability.
|
|
2591
|
+
* @param opt - configuration object
|
|
2592
|
+
* @param opt.id - Explicit channel ID for synchronization.
|
|
2235
2593
|
*
|
|
2236
2594
|
* @returns The same WritableSignal instance, now synchronized across tabs
|
|
2237
2595
|
*
|
|
2238
|
-
* @throws {Error} When deterministic ID generation fails and no explicit ID is provided
|
|
2239
2596
|
*
|
|
2240
2597
|
* @example
|
|
2241
2598
|
* ```typescript
|
|
@@ -2259,7 +2616,7 @@ function generateDeterministicID() {
|
|
|
2259
2616
|
function tabSync(sig, opt) {
|
|
2260
2617
|
if (isPlatformServer(inject(PLATFORM_ID)))
|
|
2261
2618
|
return sig;
|
|
2262
|
-
const id = opt?.id
|
|
2619
|
+
const id = typeof opt === 'string' ? opt : (opt?.id ?? generateDeterministicID());
|
|
2263
2620
|
const bus = inject(MessageBus);
|
|
2264
2621
|
const { unsub, post } = bus.subscribe(id, (next) => sig.set(next));
|
|
2265
2622
|
let first = false;
|
|
@@ -2391,8 +2748,13 @@ function getSignalEquality(sig) {
|
|
|
2391
2748
|
* console.log('Can undo:', name.canUndo()); // false
|
|
2392
2749
|
* ```
|
|
2393
2750
|
*/
|
|
2394
|
-
function withHistory(
|
|
2395
|
-
const equal = opt?.equal ??
|
|
2751
|
+
function withHistory(sourceOrValue, opt) {
|
|
2752
|
+
const equal = (opt?.equal ?? isSignal(sourceOrValue))
|
|
2753
|
+
? getSignalEquality(sourceOrValue)
|
|
2754
|
+
: Object.is;
|
|
2755
|
+
const source = isSignal(sourceOrValue)
|
|
2756
|
+
? sourceOrValue
|
|
2757
|
+
: signal(sourceOrValue);
|
|
2396
2758
|
const maxSize = opt?.maxSize ?? Infinity;
|
|
2397
2759
|
const history = mutable([], {
|
|
2398
2760
|
...opt,
|
|
@@ -2400,20 +2762,22 @@ function withHistory(source, opt) {
|
|
|
2400
2762
|
});
|
|
2401
2763
|
const redoArray = mutable([]);
|
|
2402
2764
|
const originalSet = source.set;
|
|
2765
|
+
const trim = (arr) => {
|
|
2766
|
+
if (arr.length < maxSize)
|
|
2767
|
+
return arr;
|
|
2768
|
+
if (opt?.cleanupStrategy === 'shift') {
|
|
2769
|
+
arr.shift();
|
|
2770
|
+
return arr;
|
|
2771
|
+
}
|
|
2772
|
+
return arr.slice(Math.floor(maxSize / 2));
|
|
2773
|
+
};
|
|
2403
2774
|
const set = (value) => {
|
|
2404
2775
|
const current = untracked(source);
|
|
2405
2776
|
if (equal(value, current))
|
|
2406
2777
|
return;
|
|
2407
2778
|
source.set(value);
|
|
2408
2779
|
history.mutate((c) => {
|
|
2409
|
-
|
|
2410
|
-
if (opt?.cleanupStrategy === 'shift') {
|
|
2411
|
-
c.shift();
|
|
2412
|
-
}
|
|
2413
|
-
else {
|
|
2414
|
-
c = c.slice(Math.floor(maxSize / 2));
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2780
|
+
c = trim(c);
|
|
2417
2781
|
c.push(current);
|
|
2418
2782
|
return c;
|
|
2419
2783
|
});
|
|
@@ -2437,7 +2801,11 @@ function withHistory(source, opt) {
|
|
|
2437
2801
|
return;
|
|
2438
2802
|
originalSet.call(source, valueToRestore);
|
|
2439
2803
|
history.inline((h) => h.pop());
|
|
2440
|
-
redoArray.
|
|
2804
|
+
redoArray.mutate((r) => {
|
|
2805
|
+
r = trim(r);
|
|
2806
|
+
r.push(valueForRedo);
|
|
2807
|
+
return r;
|
|
2808
|
+
});
|
|
2441
2809
|
};
|
|
2442
2810
|
internal.redo = () => {
|
|
2443
2811
|
const redoStack = untracked(redoArray);
|
|
@@ -2450,14 +2818,7 @@ function withHistory(source, opt) {
|
|
|
2450
2818
|
originalSet.call(source, valueToRestore);
|
|
2451
2819
|
redoArray.inline((r) => r.pop());
|
|
2452
2820
|
history.mutate((h) => {
|
|
2453
|
-
|
|
2454
|
-
if (opt?.cleanupStrategy === 'shift') {
|
|
2455
|
-
h.shift();
|
|
2456
|
-
}
|
|
2457
|
-
else {
|
|
2458
|
-
h = h.slice(Math.floor(maxSize / 2));
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2821
|
+
h = trim(h);
|
|
2461
2822
|
h.push(valueForUndo);
|
|
2462
2823
|
return h;
|
|
2463
2824
|
});
|
|
@@ -2476,5 +2837,5 @@ function withHistory(source, opt) {
|
|
|
2476
2837
|
* Generated bundle index. Do not edit.
|
|
2477
2838
|
*/
|
|
2478
2839
|
|
|
2479
|
-
export { chunked, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, indexArray, isDerivation, isMutable, isStore, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, pageVisibility, pipeable, piped, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, sensors, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
|
|
2840
|
+
export { batteryStatus, chunked, clipboard, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, filterWith, focusWithin, geolocation, idle, indexArray, isDerivation, isMutable, isStore, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, orientation, pageVisibility, pairwise, pipeable, piped, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
|
|
2480
2841
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|