@mmstack/primitives 21.0.21 → 21.0.23
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 +438 -80
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-primitives.d.ts +258 -40
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, computed, signal, isWritableSignal as isWritableSignal$1, isSignal,
|
|
2
|
+
import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, computed, signal, isWritableSignal as isWritableSignal$1, 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
|
|
|
@@ -349,9 +349,9 @@ function debounce(source, opt) {
|
|
|
349
349
|
const { is } = Object;
|
|
350
350
|
function mutable(initial, opt) {
|
|
351
351
|
const baseEqual = opt?.equal ?? is;
|
|
352
|
-
let
|
|
352
|
+
let cnt = 0;
|
|
353
353
|
const equal = (a, b) => {
|
|
354
|
-
if (
|
|
354
|
+
if (cnt > 0)
|
|
355
355
|
return false;
|
|
356
356
|
return baseEqual(a, b);
|
|
357
357
|
};
|
|
@@ -361,9 +361,9 @@ function mutable(initial, opt) {
|
|
|
361
361
|
});
|
|
362
362
|
const internalUpdate = sig.update;
|
|
363
363
|
sig.mutate = (updater) => {
|
|
364
|
-
|
|
364
|
+
cnt++;
|
|
365
365
|
internalUpdate(updater);
|
|
366
|
-
|
|
366
|
+
cnt--;
|
|
367
367
|
};
|
|
368
368
|
sig.inline = (updater) => {
|
|
369
369
|
sig.mutate((prev) => {
|
|
@@ -430,10 +430,10 @@ function derived(source, optOrKey, opt) {
|
|
|
430
430
|
};
|
|
431
431
|
const rest = typeof optOrKey === 'object' ? { ...optOrKey, ...opt } : opt;
|
|
432
432
|
const baseEqual = rest?.equal ?? Object.is;
|
|
433
|
-
let
|
|
433
|
+
let cnt = 0;
|
|
434
434
|
const equal = isMutable(source)
|
|
435
435
|
? (a, b) => {
|
|
436
|
-
if (
|
|
436
|
+
if (cnt > 0)
|
|
437
437
|
return false;
|
|
438
438
|
return baseEqual(a, b);
|
|
439
439
|
}
|
|
@@ -442,9 +442,9 @@ function derived(source, optOrKey, opt) {
|
|
|
442
442
|
sig.from = from;
|
|
443
443
|
if (isMutable(source)) {
|
|
444
444
|
sig.mutate = (updater) => {
|
|
445
|
-
|
|
445
|
+
cnt++;
|
|
446
446
|
sig.update(updater);
|
|
447
|
-
|
|
447
|
+
cnt--;
|
|
448
448
|
};
|
|
449
449
|
sig.inline = (updater) => {
|
|
450
450
|
sig.mutate((prev) => {
|
|
@@ -549,9 +549,11 @@ function indexArray(source, map, opt = {}) {
|
|
|
549
549
|
});
|
|
550
550
|
if (isWritableSignal(data) && isMutable(data) && !opt.equal) {
|
|
551
551
|
opt.equal = (a, b) => {
|
|
552
|
-
if (a !== b)
|
|
553
|
-
return false;
|
|
554
|
-
|
|
552
|
+
if (typeof a !== typeof b)
|
|
553
|
+
return false;
|
|
554
|
+
if (typeof a === 'object' || typeof a === 'function')
|
|
555
|
+
return false;
|
|
556
|
+
return a === b;
|
|
555
557
|
};
|
|
556
558
|
}
|
|
557
559
|
return linkedSignal({
|
|
@@ -773,10 +775,35 @@ const filter = (predicate) => (src) => linkedSignal({
|
|
|
773
775
|
},
|
|
774
776
|
});
|
|
775
777
|
/** tap into the value */
|
|
776
|
-
const tap = (fn) => (src) => {
|
|
777
|
-
effect(() => fn(src())
|
|
778
|
+
const tap = (fn, injector) => (src) => {
|
|
779
|
+
effect(() => fn(src()), {
|
|
780
|
+
injector,
|
|
781
|
+
});
|
|
778
782
|
return src;
|
|
779
783
|
};
|
|
784
|
+
/**
|
|
785
|
+
* Like {@link filter}, but emits `initial` until a value passes the predicate
|
|
786
|
+
* for the first time. Avoids the `T | undefined` return type.
|
|
787
|
+
*/
|
|
788
|
+
const filterWith = (predicate, initial) => (src) => linkedSignal({
|
|
789
|
+
source: src,
|
|
790
|
+
computation: (next, prev) => predicate(next) ? next : (prev?.value ?? initial),
|
|
791
|
+
});
|
|
792
|
+
/** Emits `initial` first, then mirrors source. */
|
|
793
|
+
const startWith = (initial) => (src) => linkedSignal({
|
|
794
|
+
source: src,
|
|
795
|
+
computation: (next, prev) => (prev === undefined ? initial : next),
|
|
796
|
+
});
|
|
797
|
+
/** Emits `[prev, curr]` pairs. The first emission has prev = undefined. */
|
|
798
|
+
const pairwise = () => (src) => linkedSignal({
|
|
799
|
+
source: src,
|
|
800
|
+
computation: (next, prev) => [prev?.source, next],
|
|
801
|
+
});
|
|
802
|
+
/** Reduce-like accumulator across emissions. */
|
|
803
|
+
const scan = (reducer, seed) => (src) => linkedSignal({
|
|
804
|
+
source: src,
|
|
805
|
+
computation: (next, prev) => reducer(prev?.value ?? seed, next),
|
|
806
|
+
});
|
|
780
807
|
|
|
781
808
|
/**
|
|
782
809
|
* Decorate any `Signal<T>` with a chainable `.pipe(...)` method.
|
|
@@ -874,28 +901,22 @@ function piped(initial, opt) {
|
|
|
874
901
|
function pooled({ create, reset, computation, ...opt }) {
|
|
875
902
|
let other = opt.eager ? create() : undefined;
|
|
876
903
|
let current = opt.eager ? create() : undefined;
|
|
877
|
-
|
|
904
|
+
// only relevant for eager mode: the pre-allocated `current` is fresh until its first demote
|
|
878
905
|
let currentFresh = opt.eager;
|
|
879
906
|
return computed(() => {
|
|
880
|
-
|
|
881
|
-
let nextFresh;
|
|
882
|
-
if (other !== undefined) {
|
|
883
|
-
next = other;
|
|
884
|
-
nextFresh = !!otherFresh;
|
|
885
|
-
}
|
|
886
|
-
else {
|
|
887
|
-
next = untracked(() => create());
|
|
888
|
-
nextFresh = true;
|
|
889
|
-
}
|
|
907
|
+
const next = other ?? untracked(() => create());
|
|
890
908
|
if (current !== undefined) {
|
|
891
|
-
|
|
892
|
-
|
|
909
|
+
if (currentFresh) {
|
|
910
|
+
other = current;
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by the `current !== undefined` check above
|
|
914
|
+
other = untracked(() => reset(current)) ?? current;
|
|
915
|
+
}
|
|
893
916
|
}
|
|
894
917
|
current = next;
|
|
895
|
-
// the buffer is about to be mutated by `computation`, so it's no longer fresh
|
|
896
918
|
currentFresh = false;
|
|
897
|
-
|
|
898
|
-
return computation(clean);
|
|
919
|
+
return computation(next);
|
|
899
920
|
}, opt);
|
|
900
921
|
}
|
|
901
922
|
|
|
@@ -936,6 +957,87 @@ function pooledMap(optOrComputation, signalOpt) {
|
|
|
936
957
|
return pooled(toPooledOptions(optOrComputation, createEmptyMap, resetClearable, signalOpt));
|
|
937
958
|
}
|
|
938
959
|
|
|
960
|
+
const EVENTS = [
|
|
961
|
+
'chargingchange',
|
|
962
|
+
'levelchange',
|
|
963
|
+
'chargingtimechange',
|
|
964
|
+
'dischargingtimechange',
|
|
965
|
+
];
|
|
966
|
+
/**
|
|
967
|
+
* Creates a read-only signal that tracks the system battery status using the
|
|
968
|
+
* Battery Status API. Returns `null` until the underlying `getBattery()`
|
|
969
|
+
* promise resolves, or permanently when the API is unsupported (Firefox /
|
|
970
|
+
* Safari at the time of writing). SSR-safe.
|
|
971
|
+
*/
|
|
972
|
+
function batteryStatus(debugName = 'batteryStatus') {
|
|
973
|
+
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
974
|
+
typeof navigator === 'undefined' ||
|
|
975
|
+
typeof navigator.getBattery !== 'function') {
|
|
976
|
+
return computed(() => null, { debugName });
|
|
977
|
+
}
|
|
978
|
+
const state = signal(null, { ...(ngDevMode ? { debugName: "state" } : /* istanbul ignore next */ {}), debugName });
|
|
979
|
+
const abortController = new AbortController();
|
|
980
|
+
inject(DestroyRef).onDestroy(() => abortController.abort());
|
|
981
|
+
navigator.getBattery().then((battery) => {
|
|
982
|
+
if (abortController.signal.aborted)
|
|
983
|
+
return;
|
|
984
|
+
const read = () => ({
|
|
985
|
+
level: battery.level,
|
|
986
|
+
charging: battery.charging,
|
|
987
|
+
chargingTime: battery.chargingTime,
|
|
988
|
+
dischargingTime: battery.dischargingTime,
|
|
989
|
+
});
|
|
990
|
+
const onChange = () => state.set(read());
|
|
991
|
+
state.set(read());
|
|
992
|
+
for (const ev of EVENTS) {
|
|
993
|
+
battery.addEventListener(ev, onChange, {
|
|
994
|
+
signal: abortController.signal,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
return state.asReadonly();
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Creates a read-only signal mirroring the system clipboard contents.
|
|
1003
|
+
*
|
|
1004
|
+
* The signal value starts empty and updates whenever a `copy` event fires on
|
|
1005
|
+
* the document (or {@link ClipboardSignal.copy} is invoked from this app).
|
|
1006
|
+
* SSR-safe — returns `''` and `isSupported: false` on the server.
|
|
1007
|
+
*
|
|
1008
|
+
* Note: read access requires the Clipboard API and an active permission grant
|
|
1009
|
+
* in browsers that gate it. Errors from `navigator.clipboard.readText` are
|
|
1010
|
+
* swallowed silently to keep the signal value stable.
|
|
1011
|
+
*/
|
|
1012
|
+
function clipboard(debugName = 'clipboard') {
|
|
1013
|
+
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
1014
|
+
typeof navigator === 'undefined' ||
|
|
1015
|
+
!navigator.clipboard) {
|
|
1016
|
+
const sig = computed(() => '', { debugName });
|
|
1017
|
+
sig.copy = () => Promise.resolve();
|
|
1018
|
+
sig.isSupported = computed(() => false, ...(ngDevMode ? [{ debugName: "isSupported" }] : /* istanbul ignore next */ []));
|
|
1019
|
+
return sig;
|
|
1020
|
+
}
|
|
1021
|
+
const state = signal('', { ...(ngDevMode ? { debugName: "state" } : /* istanbul ignore next */ {}), debugName });
|
|
1022
|
+
const refresh = () => {
|
|
1023
|
+
navigator.clipboard.readText().then((value) => state.set(value), () => {
|
|
1024
|
+
// permission denied / focus required — ignore
|
|
1025
|
+
});
|
|
1026
|
+
};
|
|
1027
|
+
const abortController = new AbortController();
|
|
1028
|
+
const onCopy = () => refresh();
|
|
1029
|
+
document.addEventListener('copy', onCopy, { signal: abortController.signal });
|
|
1030
|
+
document.addEventListener('cut', onCopy, { signal: abortController.signal });
|
|
1031
|
+
inject(DestroyRef).onDestroy(() => abortController.abort());
|
|
1032
|
+
const sig = state.asReadonly();
|
|
1033
|
+
sig.copy = async (value) => {
|
|
1034
|
+
await navigator.clipboard.writeText(value);
|
|
1035
|
+
state.set(value);
|
|
1036
|
+
};
|
|
1037
|
+
sig.isSupported = computed(() => true, ...(ngDevMode ? [{ debugName: "isSupported" }] : /* istanbul ignore next */ []));
|
|
1038
|
+
return sig;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
939
1041
|
function observerSupported$1() {
|
|
940
1042
|
return typeof ResizeObserver !== 'undefined';
|
|
941
1043
|
}
|
|
@@ -1136,6 +1238,167 @@ function elementVisibility(target = inject(ElementRef), opt) {
|
|
|
1136
1238
|
return base;
|
|
1137
1239
|
}
|
|
1138
1240
|
|
|
1241
|
+
function unwrap$1(target) {
|
|
1242
|
+
if (!target)
|
|
1243
|
+
return null;
|
|
1244
|
+
return target instanceof ElementRef ? target.nativeElement : target;
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Creates a read-only signal that tracks whether the focused element is the
|
|
1248
|
+
* target or a descendant of it. Mirrors the CSS `:focus-within` pseudo-class.
|
|
1249
|
+
*
|
|
1250
|
+
* Defaults `target` to the current `ElementRef` so it can be used inline in a
|
|
1251
|
+
* component's `class` field. SSR-safe — returns a constant `false` signal on
|
|
1252
|
+
* the server.
|
|
1253
|
+
*/
|
|
1254
|
+
function focusWithin(target = inject(ElementRef)) {
|
|
1255
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
1256
|
+
return computed(() => false, { debugName: 'focusWithin' });
|
|
1257
|
+
}
|
|
1258
|
+
const state = signal(false, { debugName: 'focusWithin' });
|
|
1259
|
+
const attach = (el) => {
|
|
1260
|
+
state.set(el.contains(document.activeElement));
|
|
1261
|
+
const abortController = new AbortController();
|
|
1262
|
+
el.addEventListener('focusin', () => state.set(true), {
|
|
1263
|
+
signal: abortController.signal,
|
|
1264
|
+
});
|
|
1265
|
+
el.addEventListener('focusout', () => {
|
|
1266
|
+
// Defer so `document.activeElement` reflects the focus move.
|
|
1267
|
+
queueMicrotask(() => state.set(el.contains(document.activeElement)));
|
|
1268
|
+
}, { signal: abortController.signal });
|
|
1269
|
+
return () => abortController.abort();
|
|
1270
|
+
};
|
|
1271
|
+
if (isSignal(target)) {
|
|
1272
|
+
const targetSig = target;
|
|
1273
|
+
effect((cleanup) => {
|
|
1274
|
+
const el = unwrap$1(targetSig());
|
|
1275
|
+
if (!el) {
|
|
1276
|
+
state.set(false);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
cleanup(attach(el));
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
else {
|
|
1283
|
+
const el = unwrap$1(target);
|
|
1284
|
+
if (el) {
|
|
1285
|
+
const detach = attach(el);
|
|
1286
|
+
inject(DestroyRef).onDestroy(detach);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return state.asReadonly();
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Creates a read-only signal that exposes the current geolocation position.
|
|
1294
|
+
*
|
|
1295
|
+
* The returned signal carries `error` and `loading` sub-signals for permission
|
|
1296
|
+
* failures and the in-flight initial fetch respectively. SSR-safe — on the
|
|
1297
|
+
* server the position is `null`, loading is `false`, and no API calls are made.
|
|
1298
|
+
*
|
|
1299
|
+
* @example
|
|
1300
|
+
* ```ts
|
|
1301
|
+
* const where = geolocation({ watch: true, enableHighAccuracy: true });
|
|
1302
|
+
* effect(() => console.log(where()?.coords, where.error()));
|
|
1303
|
+
* ```
|
|
1304
|
+
*/
|
|
1305
|
+
function geolocation(opt) {
|
|
1306
|
+
if (isPlatformServer(inject(PLATFORM_ID)) || typeof navigator === 'undefined' || !navigator.geolocation) {
|
|
1307
|
+
const sig = computed(() => null, {
|
|
1308
|
+
debugName: opt?.debugName ?? 'geolocation',
|
|
1309
|
+
});
|
|
1310
|
+
sig.error = computed(() => null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
1311
|
+
sig.loading = computed(() => false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1312
|
+
return sig;
|
|
1313
|
+
}
|
|
1314
|
+
const position = signal(null, {
|
|
1315
|
+
debugName: opt?.debugName ?? 'geolocation',
|
|
1316
|
+
});
|
|
1317
|
+
const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
1318
|
+
const loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1319
|
+
const onSuccess = (p) => {
|
|
1320
|
+
position.set(p);
|
|
1321
|
+
error.set(null);
|
|
1322
|
+
loading.set(false);
|
|
1323
|
+
};
|
|
1324
|
+
const onError = (e) => {
|
|
1325
|
+
error.set(e);
|
|
1326
|
+
loading.set(false);
|
|
1327
|
+
};
|
|
1328
|
+
if (opt?.watch) {
|
|
1329
|
+
const watchId = navigator.geolocation.watchPosition(onSuccess, onError, opt);
|
|
1330
|
+
inject(DestroyRef).onDestroy(() => navigator.geolocation.clearWatch(watchId));
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
navigator.geolocation.getCurrentPosition(onSuccess, onError, opt);
|
|
1334
|
+
}
|
|
1335
|
+
const sig = position.asReadonly();
|
|
1336
|
+
sig.error = error.asReadonly();
|
|
1337
|
+
sig.loading = loading.asReadonly();
|
|
1338
|
+
return sig;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const DEFAULT_EVENTS = [
|
|
1342
|
+
'mousemove',
|
|
1343
|
+
'keydown',
|
|
1344
|
+
'touchstart',
|
|
1345
|
+
'scroll',
|
|
1346
|
+
'visibilitychange',
|
|
1347
|
+
];
|
|
1348
|
+
const serverDate$1 = new Date();
|
|
1349
|
+
/**
|
|
1350
|
+
* Creates a read-only signal that flips to `true` after a window of user
|
|
1351
|
+
* inactivity. Any of the configured `events` (default: pointer/keyboard/scroll
|
|
1352
|
+
* activity) resets the timer and flips the signal back to `false`.
|
|
1353
|
+
*
|
|
1354
|
+
* SSR-safe — always `false` with a frozen `since` date on the server.
|
|
1355
|
+
*/
|
|
1356
|
+
function idle(opt) {
|
|
1357
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
1358
|
+
const sig = computed(() => false, {
|
|
1359
|
+
debugName: opt?.debugName ?? 'idle',
|
|
1360
|
+
});
|
|
1361
|
+
sig.since = computed(() => serverDate$1, ...(ngDevMode ? [{ debugName: "since" }] : /* istanbul ignore next */ []));
|
|
1362
|
+
return sig;
|
|
1363
|
+
}
|
|
1364
|
+
const ms = opt?.ms ?? 60_000;
|
|
1365
|
+
const events = opt?.events ?? DEFAULT_EVENTS;
|
|
1366
|
+
const state = signal(false, { debugName: opt?.debugName ?? 'idle' });
|
|
1367
|
+
const since = signal(new Date(), ...(ngDevMode ? [{ debugName: "since" }] : /* istanbul ignore next */ []));
|
|
1368
|
+
let timer;
|
|
1369
|
+
const goIdle = () => {
|
|
1370
|
+
if (state())
|
|
1371
|
+
return;
|
|
1372
|
+
state.set(true);
|
|
1373
|
+
since.set(new Date());
|
|
1374
|
+
};
|
|
1375
|
+
const reset = () => {
|
|
1376
|
+
if (timer)
|
|
1377
|
+
clearTimeout(timer);
|
|
1378
|
+
if (state()) {
|
|
1379
|
+
state.set(false);
|
|
1380
|
+
since.set(new Date());
|
|
1381
|
+
}
|
|
1382
|
+
timer = setTimeout(goIdle, ms);
|
|
1383
|
+
};
|
|
1384
|
+
const abortController = new AbortController();
|
|
1385
|
+
for (const ev of events) {
|
|
1386
|
+
window.addEventListener(ev, reset, {
|
|
1387
|
+
passive: true,
|
|
1388
|
+
signal: abortController.signal,
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
timer = setTimeout(goIdle, ms);
|
|
1392
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1393
|
+
if (timer)
|
|
1394
|
+
clearTimeout(timer);
|
|
1395
|
+
abortController.abort();
|
|
1396
|
+
});
|
|
1397
|
+
const sig = state.asReadonly();
|
|
1398
|
+
sig.since = since.asReadonly();
|
|
1399
|
+
return sig;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1139
1402
|
/**
|
|
1140
1403
|
* Creates a read-only signal that reactively tracks whether a CSS media query
|
|
1141
1404
|
* string currently matches.
|
|
@@ -1297,26 +1560,41 @@ function throttled(initial, opt) {
|
|
|
1297
1560
|
*/
|
|
1298
1561
|
function throttle(source, opt) {
|
|
1299
1562
|
const ms = opt?.ms ?? 0;
|
|
1563
|
+
const leading = opt?.leading ?? false;
|
|
1564
|
+
const trailing = opt?.trailing ?? true;
|
|
1300
1565
|
const trigger = signal(false, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
|
|
1566
|
+
const fire = () => trigger.update((c) => !c);
|
|
1301
1567
|
let timeout;
|
|
1568
|
+
let pendingTrailing = false;
|
|
1302
1569
|
try {
|
|
1303
1570
|
const destroyRef = opt?.destroyRef ?? inject(DestroyRef, { optional: true });
|
|
1304
1571
|
destroyRef?.onDestroy(() => {
|
|
1305
1572
|
if (timeout)
|
|
1306
1573
|
clearTimeout(timeout);
|
|
1307
1574
|
timeout = undefined;
|
|
1575
|
+
pendingTrailing = false;
|
|
1308
1576
|
});
|
|
1309
1577
|
}
|
|
1310
1578
|
catch {
|
|
1311
1579
|
// not in injection context & no destroyRef provided opting out of cleanup
|
|
1312
1580
|
}
|
|
1313
1581
|
const tick = () => {
|
|
1314
|
-
if (timeout)
|
|
1582
|
+
if (!timeout) {
|
|
1583
|
+
if (leading)
|
|
1584
|
+
fire();
|
|
1585
|
+
else
|
|
1586
|
+
pendingTrailing = trailing;
|
|
1587
|
+
timeout = setTimeout(() => {
|
|
1588
|
+
timeout = undefined;
|
|
1589
|
+
if (trailing && pendingTrailing) {
|
|
1590
|
+
pendingTrailing = false;
|
|
1591
|
+
fire();
|
|
1592
|
+
}
|
|
1593
|
+
}, ms);
|
|
1315
1594
|
return;
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
}, ms);
|
|
1595
|
+
}
|
|
1596
|
+
if (trailing)
|
|
1597
|
+
pendingTrailing = true;
|
|
1320
1598
|
};
|
|
1321
1599
|
const set = (value) => {
|
|
1322
1600
|
source.set(value);
|
|
@@ -1467,6 +1745,35 @@ function networkStatus(debugName = 'networkStatus') {
|
|
|
1467
1745
|
return sig;
|
|
1468
1746
|
}
|
|
1469
1747
|
|
|
1748
|
+
const SSR_FALLBACK = {
|
|
1749
|
+
angle: 0,
|
|
1750
|
+
type: 'portrait-primary',
|
|
1751
|
+
};
|
|
1752
|
+
/**
|
|
1753
|
+
* Creates a read-only signal that tracks `screen.orientation`.
|
|
1754
|
+
*
|
|
1755
|
+
* SSR-safe — returns a constant `portrait-primary / 0°` signal on the server
|
|
1756
|
+
* and in environments without `screen.orientation` support.
|
|
1757
|
+
*/
|
|
1758
|
+
function orientation(debugName = 'orientation') {
|
|
1759
|
+
if (isPlatformServer(inject(PLATFORM_ID)) ||
|
|
1760
|
+
typeof screen === 'undefined' ||
|
|
1761
|
+
!screen.orientation) {
|
|
1762
|
+
return computed(() => SSR_FALLBACK, { debugName });
|
|
1763
|
+
}
|
|
1764
|
+
const so = screen.orientation;
|
|
1765
|
+
const read = () => ({
|
|
1766
|
+
angle: so.angle,
|
|
1767
|
+
type: so.type,
|
|
1768
|
+
});
|
|
1769
|
+
const state = signal(read(), { ...(ngDevMode ? { debugName: "state" } : /* istanbul ignore next */ {}), debugName,
|
|
1770
|
+
equal: (a, b) => a.angle === b.angle && a.type === b.type });
|
|
1771
|
+
const onChange = () => state.set(read());
|
|
1772
|
+
so.addEventListener('change', onChange);
|
|
1773
|
+
inject(DestroyRef).onDestroy(() => so.removeEventListener('change', onChange));
|
|
1774
|
+
return state.asReadonly();
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1470
1777
|
/**
|
|
1471
1778
|
* Creates a read-only signal that tracks the page's visibility state.
|
|
1472
1779
|
*
|
|
@@ -1686,35 +1993,42 @@ function windowSize(opt) {
|
|
|
1686
1993
|
* @internal
|
|
1687
1994
|
*/
|
|
1688
1995
|
function sensor(type, options) {
|
|
1996
|
+
const opts = options;
|
|
1689
1997
|
switch (type) {
|
|
1690
1998
|
case 'mousePosition':
|
|
1691
|
-
return mousePosition(
|
|
1999
|
+
return mousePosition(opts);
|
|
1692
2000
|
case 'networkStatus':
|
|
1693
|
-
return networkStatus(
|
|
2001
|
+
return networkStatus(opts?.debugName);
|
|
1694
2002
|
case 'pageVisibility':
|
|
1695
|
-
return pageVisibility(
|
|
2003
|
+
return pageVisibility(opts?.debugName);
|
|
1696
2004
|
case 'darkMode':
|
|
1697
2005
|
case 'dark-mode':
|
|
1698
|
-
return prefersDarkMode(
|
|
2006
|
+
return prefersDarkMode(opts?.debugName);
|
|
1699
2007
|
case 'reducedMotion':
|
|
1700
2008
|
case 'reduced-motion':
|
|
1701
|
-
return prefersReducedMotion(
|
|
1702
|
-
case 'mediaQuery':
|
|
1703
|
-
|
|
1704
|
-
return mediaQuery(opt.query, opt.debugName);
|
|
1705
|
-
}
|
|
2009
|
+
return prefersReducedMotion(opts?.debugName);
|
|
2010
|
+
case 'mediaQuery':
|
|
2011
|
+
return mediaQuery(opts.query, opts.debugName);
|
|
1706
2012
|
case 'windowSize':
|
|
1707
|
-
return windowSize(
|
|
2013
|
+
return windowSize(opts);
|
|
1708
2014
|
case 'scrollPosition':
|
|
1709
|
-
return scrollPosition(
|
|
1710
|
-
case 'elementVisibility':
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
case '
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
2015
|
+
return scrollPosition(opts);
|
|
2016
|
+
case 'elementVisibility':
|
|
2017
|
+
return elementVisibility(opts?.target, opts);
|
|
2018
|
+
case 'elementSize':
|
|
2019
|
+
return elementSize(opts?.target, opts);
|
|
2020
|
+
case 'geolocation':
|
|
2021
|
+
return geolocation(opts);
|
|
2022
|
+
case 'clipboard':
|
|
2023
|
+
return clipboard(opts?.debugName);
|
|
2024
|
+
case 'orientation':
|
|
2025
|
+
return orientation(opts?.debugName);
|
|
2026
|
+
case 'batteryStatus':
|
|
2027
|
+
return batteryStatus(opts?.debugName);
|
|
2028
|
+
case 'idle':
|
|
2029
|
+
return idle(opts);
|
|
2030
|
+
case 'focusWithin':
|
|
2031
|
+
return focusWithin(opts?.target);
|
|
1718
2032
|
default:
|
|
1719
2033
|
throw new Error(`Unknown sensor type: ${type}`);
|
|
1720
2034
|
}
|
|
@@ -1726,6 +2040,49 @@ function sensors(track, opt) {
|
|
|
1726
2040
|
}, {});
|
|
1727
2041
|
}
|
|
1728
2042
|
|
|
2043
|
+
function unwrap(t) {
|
|
2044
|
+
if (!t)
|
|
2045
|
+
return null;
|
|
2046
|
+
return t instanceof ElementRef ? t.nativeElement : t;
|
|
2047
|
+
}
|
|
2048
|
+
function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
|
|
2049
|
+
const project = typeof projectOrOpt === 'function' ? projectOrOpt : undefined;
|
|
2050
|
+
const opt = typeof projectOrOpt === 'function' ? maybeOpt : projectOrOpt;
|
|
2051
|
+
const injector = opt?.injector ?? inject(Injector);
|
|
2052
|
+
if (isPlatformServer(injector.get(PLATFORM_ID))) {
|
|
2053
|
+
return computed(() => initial, { debugName: opt?.debugName });
|
|
2054
|
+
}
|
|
2055
|
+
const state = signal(initial, {
|
|
2056
|
+
debugName: opt?.debugName,
|
|
2057
|
+
});
|
|
2058
|
+
const handler = (event) => {
|
|
2059
|
+
if (project)
|
|
2060
|
+
state.set(project(event));
|
|
2061
|
+
else
|
|
2062
|
+
state.set(event);
|
|
2063
|
+
};
|
|
2064
|
+
const { destroyRef: providedDestroyRef, ...listenerOpts } = opt ?? {};
|
|
2065
|
+
if (isSignal(target)) {
|
|
2066
|
+
const targetSig = target;
|
|
2067
|
+
effect((cleanup) => {
|
|
2068
|
+
const resolved = unwrap(targetSig());
|
|
2069
|
+
if (!resolved)
|
|
2070
|
+
return;
|
|
2071
|
+
resolved.addEventListener(eventName, handler, listenerOpts);
|
|
2072
|
+
cleanup(() => resolved.removeEventListener(eventName, handler, listenerOpts));
|
|
2073
|
+
}, { injector });
|
|
2074
|
+
}
|
|
2075
|
+
else {
|
|
2076
|
+
const resolved = unwrap(target);
|
|
2077
|
+
if (resolved) {
|
|
2078
|
+
resolved.addEventListener(eventName, handler, listenerOpts);
|
|
2079
|
+
const destroyRef = providedDestroyRef ?? injector.get(DestroyRef);
|
|
2080
|
+
destroyRef.onDestroy(() => resolved.removeEventListener(eventName, handler, listenerOpts));
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return untracked(() => state.asReadonly());
|
|
2084
|
+
}
|
|
2085
|
+
|
|
1729
2086
|
const IS_STORE = Symbol('MMSTACK::IS_STORE');
|
|
1730
2087
|
const PROXY_CACHE = new WeakMap();
|
|
1731
2088
|
const SIGNAL_FN_PROP = new Set([
|
|
@@ -2216,14 +2573,11 @@ function generateDeterministicID() {
|
|
|
2216
2573
|
*
|
|
2217
2574
|
* @template T - The type of the WritableSignal
|
|
2218
2575
|
* @param sig - The WritableSignal to synchronize across tabs
|
|
2219
|
-
* @param opt -
|
|
2220
|
-
* @param opt.id - Explicit channel ID for synchronization.
|
|
2221
|
-
* a deterministic ID is generated based on the call site.
|
|
2222
|
-
* Use explicit IDs in production for reliability.
|
|
2576
|
+
* @param opt - configuration object
|
|
2577
|
+
* @param opt.id - Explicit channel ID for synchronization.
|
|
2223
2578
|
*
|
|
2224
2579
|
* @returns The same WritableSignal instance, now synchronized across tabs
|
|
2225
2580
|
*
|
|
2226
|
-
* @throws {Error} When deterministic ID generation fails and no explicit ID is provided
|
|
2227
2581
|
*
|
|
2228
2582
|
* @example
|
|
2229
2583
|
* ```typescript
|
|
@@ -2247,7 +2601,7 @@ function generateDeterministicID() {
|
|
|
2247
2601
|
function tabSync(sig, opt) {
|
|
2248
2602
|
if (isPlatformServer(inject(PLATFORM_ID)))
|
|
2249
2603
|
return sig;
|
|
2250
|
-
const id = opt?.id
|
|
2604
|
+
const id = typeof opt === 'string' ? opt : (opt?.id ?? generateDeterministicID());
|
|
2251
2605
|
const bus = inject(MessageBus);
|
|
2252
2606
|
const { unsub, post } = bus.subscribe(id, (next) => sig.set(next));
|
|
2253
2607
|
let first = false;
|
|
@@ -2379,8 +2733,13 @@ function getSignalEquality(sig) {
|
|
|
2379
2733
|
* console.log('Can undo:', name.canUndo()); // false
|
|
2380
2734
|
* ```
|
|
2381
2735
|
*/
|
|
2382
|
-
function withHistory(
|
|
2383
|
-
const equal = opt?.equal ??
|
|
2736
|
+
function withHistory(sourceOrValue, opt) {
|
|
2737
|
+
const equal = (opt?.equal ?? isSignal(sourceOrValue))
|
|
2738
|
+
? getSignalEquality(sourceOrValue)
|
|
2739
|
+
: Object.is;
|
|
2740
|
+
const source = isSignal(sourceOrValue)
|
|
2741
|
+
? sourceOrValue
|
|
2742
|
+
: signal(sourceOrValue);
|
|
2384
2743
|
const maxSize = opt?.maxSize ?? Infinity;
|
|
2385
2744
|
const history = mutable([], {
|
|
2386
2745
|
...opt,
|
|
@@ -2388,20 +2747,22 @@ function withHistory(source, opt) {
|
|
|
2388
2747
|
});
|
|
2389
2748
|
const redoArray = mutable([]);
|
|
2390
2749
|
const originalSet = source.set;
|
|
2750
|
+
const trim = (arr) => {
|
|
2751
|
+
if (arr.length < maxSize)
|
|
2752
|
+
return arr;
|
|
2753
|
+
if (opt?.cleanupStrategy === 'shift') {
|
|
2754
|
+
arr.shift();
|
|
2755
|
+
return arr;
|
|
2756
|
+
}
|
|
2757
|
+
return arr.slice(Math.floor(maxSize / 2));
|
|
2758
|
+
};
|
|
2391
2759
|
const set = (value) => {
|
|
2392
2760
|
const current = untracked(source);
|
|
2393
2761
|
if (equal(value, current))
|
|
2394
2762
|
return;
|
|
2395
2763
|
source.set(value);
|
|
2396
2764
|
history.mutate((c) => {
|
|
2397
|
-
|
|
2398
|
-
if (opt?.cleanupStrategy === 'shift') {
|
|
2399
|
-
c.shift();
|
|
2400
|
-
}
|
|
2401
|
-
else {
|
|
2402
|
-
c = c.slice(Math.floor(maxSize / 2));
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2765
|
+
c = trim(c);
|
|
2405
2766
|
c.push(current);
|
|
2406
2767
|
return c;
|
|
2407
2768
|
});
|
|
@@ -2425,7 +2786,11 @@ function withHistory(source, opt) {
|
|
|
2425
2786
|
return;
|
|
2426
2787
|
originalSet.call(source, valueToRestore);
|
|
2427
2788
|
history.inline((h) => h.pop());
|
|
2428
|
-
redoArray.
|
|
2789
|
+
redoArray.mutate((r) => {
|
|
2790
|
+
r = trim(r);
|
|
2791
|
+
r.push(valueForRedo);
|
|
2792
|
+
return r;
|
|
2793
|
+
});
|
|
2429
2794
|
};
|
|
2430
2795
|
internal.redo = () => {
|
|
2431
2796
|
const redoStack = untracked(redoArray);
|
|
@@ -2438,14 +2803,7 @@ function withHistory(source, opt) {
|
|
|
2438
2803
|
originalSet.call(source, valueToRestore);
|
|
2439
2804
|
redoArray.inline((r) => r.pop());
|
|
2440
2805
|
history.mutate((h) => {
|
|
2441
|
-
|
|
2442
|
-
if (opt?.cleanupStrategy === 'shift') {
|
|
2443
|
-
h.shift();
|
|
2444
|
-
}
|
|
2445
|
-
else {
|
|
2446
|
-
h = h.slice(Math.floor(maxSize / 2));
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2806
|
+
h = trim(h);
|
|
2449
2807
|
h.push(valueForUndo);
|
|
2450
2808
|
return h;
|
|
2451
2809
|
});
|
|
@@ -2464,5 +2822,5 @@ function withHistory(source, opt) {
|
|
|
2464
2822
|
* Generated bundle index. Do not edit.
|
|
2465
2823
|
*/
|
|
2466
2824
|
|
|
2467
|
-
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 };
|
|
2825
|
+
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 };
|
|
2468
2826
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|