@mmstack/primitives 21.0.0 → 21.0.2
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/LICENSE +21 -21
- package/README.md +777 -777
- package/fesm2022/mmstack-primitives.mjs +407 -173
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-primitives.d.ts +270 -92
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { untracked, signal, inject, DestroyRef,
|
|
2
|
+
import { computed, untracked, signal, inject, DestroyRef, isWritableSignal as isWritableSignal$1, isSignal, linkedSignal, isDevMode, Injector, effect, ElementRef, PLATFORM_ID, Injectable, runInInjectionContext } from '@angular/core';
|
|
3
3
|
import { isPlatformServer } from '@angular/common';
|
|
4
4
|
import { SIGNAL } from '@angular/core/primitives/signals';
|
|
5
5
|
|
|
@@ -31,9 +31,9 @@ import { SIGNAL } from '@angular/core/primitives/signals';
|
|
|
31
31
|
*
|
|
32
32
|
* writableSignal.set(5); // sets value of originalValue.a to 5 & triggers all signals
|
|
33
33
|
*/
|
|
34
|
-
function toWritable(
|
|
35
|
-
const internal =
|
|
36
|
-
internal.asReadonly = () =>
|
|
34
|
+
function toWritable(source, set, update, opt) {
|
|
35
|
+
const internal = (opt?.pure !== false ? computed(source) : source);
|
|
36
|
+
internal.asReadonly = () => source;
|
|
37
37
|
internal.set = set;
|
|
38
38
|
internal.update = update ?? ((updater) => set(updater(untracked(internal))));
|
|
39
39
|
return internal;
|
|
@@ -289,117 +289,10 @@ function isDerivation(sig) {
|
|
|
289
289
|
return 'from' in sig;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
function
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Creates a read-only signal that tracks the intersection status of a target DOM element
|
|
297
|
-
* with the viewport or a specified root element, using the `IntersectionObserver` API.
|
|
298
|
-
*
|
|
299
|
-
* It can observe a static `ElementRef`/`Element` or a `Signal` that resolves to one,
|
|
300
|
-
* allowing for dynamic targets.
|
|
301
|
-
*
|
|
302
|
-
* @param target The DOM element (or `ElementRef`, or a `Signal` resolving to one) to observe.
|
|
303
|
-
* If the signal resolves to `null`, observation stops.
|
|
304
|
-
* @param options Optional `IntersectionObserverInit` options (e.g., `root`, `rootMargin`, `threshold`)
|
|
305
|
-
* and an optional `debugName`.
|
|
306
|
-
* @returns A `Signal<IntersectionObserverEntry | undefined>`. It emits `undefined` initially,
|
|
307
|
-
* on the server, or if the target is `null`. Otherwise, it emits the latest
|
|
308
|
-
* `IntersectionObserverEntry`. Consumers can derive a boolean `isVisible` from
|
|
309
|
-
* this entry's `isIntersecting` property.
|
|
310
|
-
*
|
|
311
|
-
* @example
|
|
312
|
-
* ```ts
|
|
313
|
-
* import { Component, effect, ElementRef, viewChild } from '@angular/core';
|
|
314
|
-
* import { elementVisibility } from '@mmstack/primitives';
|
|
315
|
-
* import { computed } from '@angular/core'; // For derived boolean
|
|
316
|
-
*
|
|
317
|
-
* @Component({
|
|
318
|
-
* selector: 'app-lazy-image',
|
|
319
|
-
* template: `
|
|
320
|
-
* <div #imageContainer style="height: 200px; border: 1px dashed grey;">
|
|
321
|
-
* @if (isVisible()) {
|
|
322
|
-
* <img src="your-image-url.jpg" alt="Lazy loaded image" />
|
|
323
|
-
* <p>Image is VISIBLE!</p>
|
|
324
|
-
* } @else {
|
|
325
|
-
* <p>Scroll down to see the image...</p>
|
|
326
|
-
* }
|
|
327
|
-
* </div>
|
|
328
|
-
* `
|
|
329
|
-
* })
|
|
330
|
-
* export class LazyImageComponent {
|
|
331
|
-
* readonly imageContainer = viewChild.required<ElementRef<HTMLDivElement>>('imageContainer');
|
|
332
|
-
*
|
|
333
|
-
* // Observe the element, get the full IntersectionObserverEntry
|
|
334
|
-
* readonly intersectionEntry = elementVisibility(this.imageContainer);
|
|
335
|
-
*
|
|
336
|
-
* // Derive a simple boolean for visibility
|
|
337
|
-
* readonly isVisible = computed(() => this.intersectionEntry()?.isIntersecting ?? false);
|
|
338
|
-
*
|
|
339
|
-
* constructor() {
|
|
340
|
-
* effect(() => {
|
|
341
|
-
* console.log('Intersection Entry:', this.intersectionEntry());
|
|
342
|
-
* console.log('Is Visible:', this.isVisible());
|
|
343
|
-
* });
|
|
344
|
-
* }
|
|
345
|
-
* }
|
|
346
|
-
* ```
|
|
347
|
-
*/
|
|
348
|
-
function elementVisibility(target = inject(ElementRef), opt) {
|
|
349
|
-
if (isPlatformServer(inject(PLATFORM_ID)) || !observerSupported()) {
|
|
350
|
-
const base = computed(() => undefined, {
|
|
351
|
-
debugName: opt?.debugName,
|
|
352
|
-
});
|
|
353
|
-
base.visible = computed(() => false, ...(ngDevMode ? [{ debugName: "visible" }] : []));
|
|
354
|
-
return base;
|
|
355
|
-
}
|
|
356
|
-
const state = signal(undefined, {
|
|
357
|
-
debugName: opt?.debugName,
|
|
358
|
-
equal: (a, b) => {
|
|
359
|
-
if (!a && !b)
|
|
360
|
-
return true;
|
|
361
|
-
if (!a || !b)
|
|
362
|
-
return false;
|
|
363
|
-
return (a.target === b.target &&
|
|
364
|
-
a.isIntersecting === b.isIntersecting &&
|
|
365
|
-
a.intersectionRatio === b.intersectionRatio &&
|
|
366
|
-
a.boundingClientRect.top === b.boundingClientRect.top &&
|
|
367
|
-
a.boundingClientRect.left === b.boundingClientRect.left &&
|
|
368
|
-
a.boundingClientRect.width === b.boundingClientRect.width &&
|
|
369
|
-
a.boundingClientRect.height === b.boundingClientRect.height);
|
|
370
|
-
},
|
|
371
|
-
});
|
|
372
|
-
const targetSignal = isSignal(target) ? target : computed(() => target);
|
|
373
|
-
effect((cleanup) => {
|
|
374
|
-
const el = targetSignal();
|
|
375
|
-
if (!el)
|
|
376
|
-
return state.set(undefined);
|
|
377
|
-
let observer = null;
|
|
378
|
-
observer = new IntersectionObserver(([entry]) => state.set(entry), opt);
|
|
379
|
-
observer.observe(el instanceof ElementRef ? el.nativeElement : el);
|
|
380
|
-
cleanup(() => {
|
|
381
|
-
observer?.disconnect();
|
|
382
|
-
});
|
|
383
|
-
});
|
|
384
|
-
const base = state.asReadonly();
|
|
385
|
-
base.visible = computed(() => {
|
|
386
|
-
const s = state();
|
|
387
|
-
if (!s)
|
|
388
|
-
return false;
|
|
389
|
-
return s.isIntersecting;
|
|
390
|
-
}, ...(ngDevMode ? [{ debugName: "visible" }] : []));
|
|
391
|
-
return base;
|
|
292
|
+
function isWritableSignal(value) {
|
|
293
|
+
return isWritableSignal$1(value);
|
|
392
294
|
}
|
|
393
295
|
|
|
394
|
-
/**
|
|
395
|
-
* @internal
|
|
396
|
-
* Checks if a signal is a WritableSignal.
|
|
397
|
-
* @param sig The signal to check.
|
|
398
|
-
*/
|
|
399
|
-
function isWritable(sig) {
|
|
400
|
-
// We just need to check for the presence of a 'set' method.
|
|
401
|
-
return 'set' in sig;
|
|
402
|
-
}
|
|
403
296
|
/**
|
|
404
297
|
* @internal
|
|
405
298
|
* Creates a setter function for a source signal of type `Signal<T[]>` or a function returning `T[]`.
|
|
@@ -407,31 +300,45 @@ function isWritable(sig) {
|
|
|
407
300
|
* @returns
|
|
408
301
|
*/
|
|
409
302
|
function createSetter(source) {
|
|
410
|
-
if (!
|
|
303
|
+
if (!isWritableSignal(source))
|
|
411
304
|
return () => {
|
|
412
305
|
// noop;
|
|
413
306
|
};
|
|
414
307
|
if (isMutable(source))
|
|
415
308
|
return (value, index) => {
|
|
416
|
-
source.
|
|
309
|
+
source.mutate((arr) => {
|
|
417
310
|
arr[index] = value;
|
|
311
|
+
return arr;
|
|
418
312
|
});
|
|
419
313
|
};
|
|
420
314
|
return (value, index) => {
|
|
421
315
|
source.update((arr) => arr.map((v, i) => (i === index ? value : v)));
|
|
422
316
|
};
|
|
423
317
|
}
|
|
424
|
-
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Helper to create the derived signal for a specific index.
|
|
321
|
+
* Extracts the cast logic to keep the main loop clean.
|
|
322
|
+
*/
|
|
323
|
+
function createItemSignal(source, index, setter, opt) {
|
|
324
|
+
return derived(
|
|
325
|
+
// We cast to any/Mutable to satisfy the overload signature,
|
|
326
|
+
// but 'derived' internally checks isMutable() for safety.
|
|
327
|
+
source, {
|
|
328
|
+
from: (src) => src[index],
|
|
329
|
+
onChange: (value) => setter(value, index),
|
|
330
|
+
}, opt);
|
|
331
|
+
}
|
|
332
|
+
function indexArray(source, map, opt = {}) {
|
|
425
333
|
const data = isSignal(source) ? source : computed(source);
|
|
426
334
|
const len = computed(() => data().length, ...(ngDevMode ? [{ debugName: "len" }] : []));
|
|
427
335
|
const setter = createSetter(data);
|
|
428
|
-
const
|
|
429
|
-
const writableData = isWritable(data)
|
|
336
|
+
const writableData = isWritableSignal(data)
|
|
430
337
|
? data
|
|
431
338
|
: toWritable(data, () => {
|
|
432
339
|
// noop
|
|
433
340
|
});
|
|
434
|
-
if (
|
|
341
|
+
if (isWritableSignal(data) && isMutable(data) && !opt.equal) {
|
|
435
342
|
opt.equal = (a, b) => {
|
|
436
343
|
if (a !== b)
|
|
437
344
|
return false; // actually check primitives and references
|
|
@@ -442,41 +349,92 @@ function mapArray(source, map, options) {
|
|
|
442
349
|
source: () => len(),
|
|
443
350
|
computation: (len, prev) => {
|
|
444
351
|
if (!prev)
|
|
445
|
-
return Array.from({ length: len }, (_, i) =>
|
|
446
|
-
const derivation = derived(writableData, // typcase to largest type
|
|
447
|
-
{
|
|
448
|
-
from: (src) => src[i],
|
|
449
|
-
onChange: (value) => setter(value, i),
|
|
450
|
-
}, opt);
|
|
451
|
-
return map(derivation, i);
|
|
452
|
-
});
|
|
352
|
+
return Array.from({ length: len }, (_, i) => map(createItemSignal(writableData, i, setter, opt), i));
|
|
453
353
|
if (len === prev.value.length)
|
|
454
354
|
return prev.value;
|
|
455
355
|
if (len < prev.value.length) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
opt.onDestroy?.(prev.value[i]);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return slice;
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
const next = [...prev.value];
|
|
466
|
-
for (let i = prev.value.length; i < len; i++) {
|
|
467
|
-
const derivation = derived(writableData, // typcase to largest type
|
|
468
|
-
{
|
|
469
|
-
from: (src) => src[i],
|
|
470
|
-
onChange: (value) => setter(value, i),
|
|
471
|
-
}, opt);
|
|
472
|
-
next[i] = map(derivation, i);
|
|
473
|
-
}
|
|
474
|
-
return next;
|
|
356
|
+
if (opt.onDestroy)
|
|
357
|
+
prev.value.forEach((v) => opt.onDestroy?.(v));
|
|
358
|
+
return prev.value.slice(0, len);
|
|
475
359
|
}
|
|
360
|
+
const next = prev.value.slice();
|
|
361
|
+
for (let i = prev.value.length; i < len; i++)
|
|
362
|
+
next[i] = map(createItemSignal(writableData, i, setter, opt), i);
|
|
363
|
+
return next;
|
|
476
364
|
},
|
|
477
365
|
equal: (a, b) => a.length === b.length,
|
|
478
366
|
});
|
|
479
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* @deprecated use indexArray instead
|
|
370
|
+
*/
|
|
371
|
+
const mapArray = indexArray;
|
|
372
|
+
|
|
373
|
+
function keyArray(source, keyFn, map, opt = {}) {
|
|
374
|
+
const data = isSignal(source) ? source : computed(source);
|
|
375
|
+
const setter = createSetter(data);
|
|
376
|
+
const writableData = isWritableSignal(data)
|
|
377
|
+
? data
|
|
378
|
+
: toWritable(data, () => {
|
|
379
|
+
// noop
|
|
380
|
+
});
|
|
381
|
+
if (isWritableSignal(data) && isMutable(data) && !opt.equal) {
|
|
382
|
+
opt.equal = (a, b) => {
|
|
383
|
+
if (a !== b)
|
|
384
|
+
return false;
|
|
385
|
+
return false; // opt out for same refs
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
let freeMap = new Map();
|
|
389
|
+
const createRecord = (i) => {
|
|
390
|
+
const idx = signal(i, ...(ngDevMode ? [{ debugName: "idx" }] : []));
|
|
391
|
+
const value = derived(writableData, {
|
|
392
|
+
from: (v) => v[idx()],
|
|
393
|
+
onChange: (next) => setter(next, untracked(idx)),
|
|
394
|
+
}, opt);
|
|
395
|
+
return {
|
|
396
|
+
source: {
|
|
397
|
+
idx,
|
|
398
|
+
value,
|
|
399
|
+
},
|
|
400
|
+
computation: map(value, idx),
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
const internal = linkedSignal({ ...(ngDevMode ? { debugName: "internal" } : {}), source: () => writableData(),
|
|
404
|
+
computation: (src, prev) => {
|
|
405
|
+
const prevCache = prev?.value.cache ?? new Map();
|
|
406
|
+
const nextCache = freeMap;
|
|
407
|
+
const nextValues = [];
|
|
408
|
+
let changed = false;
|
|
409
|
+
for (let i = 0; i < src.length; i++) {
|
|
410
|
+
const k = untracked(() => keyFn(src[i]));
|
|
411
|
+
let record = prevCache.get(k);
|
|
412
|
+
if (!record) {
|
|
413
|
+
changed = true;
|
|
414
|
+
record = createRecord(i);
|
|
415
|
+
}
|
|
416
|
+
prevCache.delete(k);
|
|
417
|
+
nextCache.set(k, record);
|
|
418
|
+
nextValues.push(record.computation);
|
|
419
|
+
if (untracked(record.source.idx) !== i) {
|
|
420
|
+
record.source.idx.set(i);
|
|
421
|
+
changed = true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (prevCache.size > 0)
|
|
425
|
+
changed = true;
|
|
426
|
+
if (opt.onDestroy)
|
|
427
|
+
prevCache.values().forEach((v) => opt.onDestroy?.(v.computation));
|
|
428
|
+
// clear for next run
|
|
429
|
+
prevCache.clear();
|
|
430
|
+
freeMap = prevCache;
|
|
431
|
+
return {
|
|
432
|
+
cache: nextCache,
|
|
433
|
+
values: changed ? nextValues : (prev?.value.values ?? []),
|
|
434
|
+
};
|
|
435
|
+
} });
|
|
436
|
+
return computed(() => internal().values);
|
|
437
|
+
}
|
|
480
438
|
|
|
481
439
|
const frameStack = [];
|
|
482
440
|
function current() {
|
|
@@ -698,6 +656,205 @@ function piped(initial, opt) {
|
|
|
698
656
|
return pipeable(signal(initial, opt));
|
|
699
657
|
}
|
|
700
658
|
|
|
659
|
+
function observerSupported$1() {
|
|
660
|
+
return typeof ResizeObserver !== 'undefined';
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Creates a read-only signal that tracks the size of a target DOM element.
|
|
664
|
+
*
|
|
665
|
+
* By default, it observes the `border-box` size to align with `getBoundingClientRect()`,
|
|
666
|
+
* which is used to provide a synchronous initial value if possible.
|
|
667
|
+
*
|
|
668
|
+
* @param target The DOM element (or `ElementRef`, or a `Signal` resolving to one) to observe.
|
|
669
|
+
* @param options Optional configuration including `box` (defaults to 'border-box') and `debugName`.
|
|
670
|
+
* @returns A `Signal<ElementSize | undefined>`.
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* ```ts
|
|
674
|
+
* const size = elementSize(elementRef);
|
|
675
|
+
* effect(() => {
|
|
676
|
+
* console.log('Size:', size()?.width, size()?.height);
|
|
677
|
+
* });
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
function elementSize(target = inject(ElementRef), opt) {
|
|
681
|
+
const getElement = () => {
|
|
682
|
+
if (isSignal(target)) {
|
|
683
|
+
try {
|
|
684
|
+
const val = target();
|
|
685
|
+
return val instanceof ElementRef ? val.nativeElement : val;
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return target instanceof ElementRef ? target.nativeElement : target;
|
|
692
|
+
};
|
|
693
|
+
const resolveInitialValue = () => {
|
|
694
|
+
if (!observerSupported$1())
|
|
695
|
+
return undefined;
|
|
696
|
+
const el = getElement();
|
|
697
|
+
if (el && el.getBoundingClientRect) {
|
|
698
|
+
const rect = el.getBoundingClientRect();
|
|
699
|
+
return { width: rect.width, height: rect.height };
|
|
700
|
+
}
|
|
701
|
+
return undefined;
|
|
702
|
+
};
|
|
703
|
+
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
704
|
+
return computed(() => untracked(resolveInitialValue), {
|
|
705
|
+
debugName: opt?.debugName,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
const state = signal(untracked(resolveInitialValue), {
|
|
709
|
+
debugName: opt?.debugName,
|
|
710
|
+
equal: (a, b) => a?.width === b?.width && a?.height === b?.height,
|
|
711
|
+
});
|
|
712
|
+
const targetSignal = isSignal(target) ? target : computed(() => target);
|
|
713
|
+
effect((cleanup) => {
|
|
714
|
+
const el = targetSignal();
|
|
715
|
+
if (el) {
|
|
716
|
+
const nativeEl = el instanceof ElementRef ? el.nativeElement : el;
|
|
717
|
+
const rect = nativeEl.getBoundingClientRect();
|
|
718
|
+
untracked(() => state.set({ width: rect.width, height: rect.height }));
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
untracked(() => state.set(undefined));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (!observerSupported$1())
|
|
725
|
+
return;
|
|
726
|
+
let observer = null;
|
|
727
|
+
observer = new ResizeObserver(([entry]) => {
|
|
728
|
+
let width = 0;
|
|
729
|
+
let height = 0;
|
|
730
|
+
const boxOption = opt?.box ?? 'border-box';
|
|
731
|
+
if (boxOption === 'border-box' && entry.borderBoxSize?.length > 0) {
|
|
732
|
+
const size = entry.borderBoxSize[0];
|
|
733
|
+
width = size.inlineSize;
|
|
734
|
+
height = size.blockSize;
|
|
735
|
+
}
|
|
736
|
+
else if (boxOption === 'content-box' && entry.contentBoxSize?.length > 0) {
|
|
737
|
+
width = entry.contentBoxSize[0].inlineSize;
|
|
738
|
+
height = entry.contentBoxSize[0].blockSize;
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
width = entry.contentRect.width;
|
|
742
|
+
height = entry.contentRect.height;
|
|
743
|
+
}
|
|
744
|
+
state.set({ width, height });
|
|
745
|
+
});
|
|
746
|
+
observer.observe(el instanceof ElementRef ? el.nativeElement : el, {
|
|
747
|
+
box: opt?.box ?? 'border-box',
|
|
748
|
+
});
|
|
749
|
+
cleanup(() => {
|
|
750
|
+
observer?.disconnect();
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
return state.asReadonly();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function observerSupported() {
|
|
757
|
+
return typeof IntersectionObserver !== 'undefined';
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Creates a read-only signal that tracks the intersection status of a target DOM element
|
|
761
|
+
* with the viewport or a specified root element, using the `IntersectionObserver` API.
|
|
762
|
+
*
|
|
763
|
+
* It can observe a static `ElementRef`/`Element` or a `Signal` that resolves to one,
|
|
764
|
+
* allowing for dynamic targets.
|
|
765
|
+
*
|
|
766
|
+
* @param target The DOM element (or `ElementRef`, or a `Signal` resolving to one) to observe.
|
|
767
|
+
* If the signal resolves to `null`, observation stops.
|
|
768
|
+
* @param options Optional `IntersectionObserverInit` options (e.g., `root`, `rootMargin`, `threshold`)
|
|
769
|
+
* and an optional `debugName`.
|
|
770
|
+
* @returns A `Signal<IntersectionObserverEntry | undefined>`. It emits `undefined` initially,
|
|
771
|
+
* on the server, or if the target is `null`. Otherwise, it emits the latest
|
|
772
|
+
* `IntersectionObserverEntry`. Consumers can derive a boolean `isVisible` from
|
|
773
|
+
* this entry's `isIntersecting` property.
|
|
774
|
+
*
|
|
775
|
+
* @example
|
|
776
|
+
* ```ts
|
|
777
|
+
* import { Component, effect, ElementRef, viewChild } from '@angular/core';
|
|
778
|
+
* import { elementVisibility } from '@mmstack/primitives';
|
|
779
|
+
* import { computed } from '@angular/core'; // For derived boolean
|
|
780
|
+
*
|
|
781
|
+
* @Component({
|
|
782
|
+
* selector: 'app-lazy-image',
|
|
783
|
+
* template: `
|
|
784
|
+
* <div #imageContainer style="height: 200px; border: 1px dashed grey;">
|
|
785
|
+
* @if (isVisible()) {
|
|
786
|
+
* <img src="your-image-url.jpg" alt="Lazy loaded image" />
|
|
787
|
+
* <p>Image is VISIBLE!</p>
|
|
788
|
+
* } @else {
|
|
789
|
+
* <p>Scroll down to see the image...</p>
|
|
790
|
+
* }
|
|
791
|
+
* </div>
|
|
792
|
+
* `
|
|
793
|
+
* })
|
|
794
|
+
* export class LazyImageComponent {
|
|
795
|
+
* readonly imageContainer = viewChild.required<ElementRef<HTMLDivElement>>('imageContainer');
|
|
796
|
+
*
|
|
797
|
+
* // Observe the element, get the full IntersectionObserverEntry
|
|
798
|
+
* readonly intersectionEntry = elementVisibility(this.imageContainer);
|
|
799
|
+
*
|
|
800
|
+
* // Derive a simple boolean for visibility
|
|
801
|
+
* readonly isVisible = computed(() => this.intersectionEntry()?.isIntersecting ?? false);
|
|
802
|
+
*
|
|
803
|
+
* constructor() {
|
|
804
|
+
* effect(() => {
|
|
805
|
+
* console.log('Intersection Entry:', this.intersectionEntry());
|
|
806
|
+
* console.log('Is Visible:', this.isVisible());
|
|
807
|
+
* });
|
|
808
|
+
* }
|
|
809
|
+
* }
|
|
810
|
+
* ```
|
|
811
|
+
*/
|
|
812
|
+
function elementVisibility(target = inject(ElementRef), opt) {
|
|
813
|
+
if (isPlatformServer(inject(PLATFORM_ID)) || !observerSupported()) {
|
|
814
|
+
const base = computed(() => undefined, {
|
|
815
|
+
debugName: opt?.debugName,
|
|
816
|
+
});
|
|
817
|
+
base.visible = computed(() => false, ...(ngDevMode ? [{ debugName: "visible" }] : []));
|
|
818
|
+
return base;
|
|
819
|
+
}
|
|
820
|
+
const state = signal(undefined, {
|
|
821
|
+
debugName: opt?.debugName,
|
|
822
|
+
equal: (a, b) => {
|
|
823
|
+
if (!a && !b)
|
|
824
|
+
return true;
|
|
825
|
+
if (!a || !b)
|
|
826
|
+
return false;
|
|
827
|
+
return (a.target === b.target &&
|
|
828
|
+
a.isIntersecting === b.isIntersecting &&
|
|
829
|
+
a.intersectionRatio === b.intersectionRatio &&
|
|
830
|
+
a.boundingClientRect.top === b.boundingClientRect.top &&
|
|
831
|
+
a.boundingClientRect.left === b.boundingClientRect.left &&
|
|
832
|
+
a.boundingClientRect.width === b.boundingClientRect.width &&
|
|
833
|
+
a.boundingClientRect.height === b.boundingClientRect.height);
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
const targetSignal = isSignal(target) ? target : computed(() => target);
|
|
837
|
+
effect((cleanup) => {
|
|
838
|
+
const el = targetSignal();
|
|
839
|
+
if (!el)
|
|
840
|
+
return state.set(undefined);
|
|
841
|
+
let observer = null;
|
|
842
|
+
observer = new IntersectionObserver(([entry]) => state.set(entry), opt);
|
|
843
|
+
observer.observe(el instanceof ElementRef ? el.nativeElement : el);
|
|
844
|
+
cleanup(() => {
|
|
845
|
+
observer?.disconnect();
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
const base = state.asReadonly();
|
|
849
|
+
base.visible = computed(() => {
|
|
850
|
+
const s = state();
|
|
851
|
+
if (!s)
|
|
852
|
+
return false;
|
|
853
|
+
return s.isIntersecting;
|
|
854
|
+
}, ...(ngDevMode ? [{ debugName: "visible" }] : []));
|
|
855
|
+
return base;
|
|
856
|
+
}
|
|
857
|
+
|
|
701
858
|
/**
|
|
702
859
|
* Creates a read-only signal that reactively tracks whether a CSS media query
|
|
703
860
|
* string currently matches.
|
|
@@ -1004,9 +1161,7 @@ function networkStatus(debugName = 'networkStatus') {
|
|
|
1004
1161
|
sig.since = computed(() => serverDate, ...(ngDevMode ? [{ debugName: "since" }] : []));
|
|
1005
1162
|
return sig;
|
|
1006
1163
|
}
|
|
1007
|
-
const state = signal(navigator.onLine, ...(ngDevMode ?
|
|
1008
|
-
debugName,
|
|
1009
|
-
}]));
|
|
1164
|
+
const state = signal(navigator.onLine, { ...(ngDevMode ? { debugName: "state" } : {}), debugName });
|
|
1010
1165
|
const since = signal(new Date(), ...(ngDevMode ? [{ debugName: "since" }] : []));
|
|
1011
1166
|
const goOnline = () => {
|
|
1012
1167
|
state.set(true);
|
|
@@ -1067,7 +1222,7 @@ function pageVisibility(debugName = 'pageVisibility') {
|
|
|
1067
1222
|
if (isPlatformServer(inject(PLATFORM_ID))) {
|
|
1068
1223
|
return computed(() => 'visible', { debugName });
|
|
1069
1224
|
}
|
|
1070
|
-
const visibility = signal(document.visibilityState, ...(ngDevMode ?
|
|
1225
|
+
const visibility = signal(document.visibilityState, { ...(ngDevMode ? { debugName: "visibility" } : {}), debugName });
|
|
1071
1226
|
const onVisibilityChange = () => visibility.set(document.visibilityState);
|
|
1072
1227
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
1073
1228
|
inject(DestroyRef).onDestroy(() => document.removeEventListener('visibilitychange', onVisibilityChange));
|
|
@@ -1250,18 +1405,103 @@ function sensor(type, options) {
|
|
|
1250
1405
|
return networkStatus(options?.debugName);
|
|
1251
1406
|
case 'pageVisibility':
|
|
1252
1407
|
return pageVisibility(options?.debugName);
|
|
1408
|
+
case 'darkMode':
|
|
1253
1409
|
case 'dark-mode':
|
|
1254
1410
|
return prefersDarkMode(options?.debugName);
|
|
1411
|
+
case 'reducedMotion':
|
|
1255
1412
|
case 'reduced-motion':
|
|
1256
1413
|
return prefersReducedMotion(options?.debugName);
|
|
1414
|
+
case 'mediaQuery': {
|
|
1415
|
+
const opt = options;
|
|
1416
|
+
return mediaQuery(opt.query, opt.debugName);
|
|
1417
|
+
}
|
|
1257
1418
|
case 'windowSize':
|
|
1258
1419
|
return windowSize(options);
|
|
1259
1420
|
case 'scrollPosition':
|
|
1260
1421
|
return scrollPosition(options);
|
|
1422
|
+
case 'elementVisibility': {
|
|
1423
|
+
const opt = options;
|
|
1424
|
+
return elementVisibility(opt.target, opt);
|
|
1425
|
+
}
|
|
1426
|
+
case 'elementSize': {
|
|
1427
|
+
const opt = options;
|
|
1428
|
+
return elementSize(opt.target, opt);
|
|
1429
|
+
}
|
|
1261
1430
|
default:
|
|
1262
1431
|
throw new Error(`Unknown sensor type: ${type}`);
|
|
1263
1432
|
}
|
|
1264
1433
|
}
|
|
1434
|
+
function sensors(track, opt) {
|
|
1435
|
+
return track.reduce((result, key) => {
|
|
1436
|
+
result[key] = sensor(key, opt?.[key]);
|
|
1437
|
+
return result;
|
|
1438
|
+
}, {});
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Credit to NGRX signal store, adaptation for purposes of supporting Writable/Mutable signals
|
|
1442
|
+
// Link to source: https://github.com/ngrx/platform/blob/main/modules/signals/src/deep-signal.ts
|
|
1443
|
+
const TREAT_AS_VALUE = new Set([
|
|
1444
|
+
Date,
|
|
1445
|
+
Error,
|
|
1446
|
+
RegExp,
|
|
1447
|
+
ArrayBuffer,
|
|
1448
|
+
DataView,
|
|
1449
|
+
Function,
|
|
1450
|
+
WeakSet,
|
|
1451
|
+
WeakMap,
|
|
1452
|
+
WeakRef,
|
|
1453
|
+
Promise,
|
|
1454
|
+
Iterator,
|
|
1455
|
+
]);
|
|
1456
|
+
function isIterable(value) {
|
|
1457
|
+
return typeof value?.[Symbol.iterator] === 'function';
|
|
1458
|
+
}
|
|
1459
|
+
function isRecord(value) {
|
|
1460
|
+
if (value === null || typeof value !== 'object' || isIterable(value)) {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
let proto = Object.getPrototypeOf(value);
|
|
1464
|
+
if (proto === Object.prototype) {
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
while (proto && proto !== Object.prototype) {
|
|
1468
|
+
if (TREAT_AS_VALUE.has(proto.constructor))
|
|
1469
|
+
return false;
|
|
1470
|
+
proto = Object.getPrototypeOf(proto);
|
|
1471
|
+
}
|
|
1472
|
+
return proto === Object.prototype;
|
|
1473
|
+
}
|
|
1474
|
+
const STORE = Symbol(isDevMode() ? 'SIGNAL_STORE' : '');
|
|
1475
|
+
function toStore(source) {
|
|
1476
|
+
return new Proxy(source, {
|
|
1477
|
+
has(target, prop) {
|
|
1478
|
+
return !!this.get(target, prop, undefined);
|
|
1479
|
+
},
|
|
1480
|
+
get(target, prop) {
|
|
1481
|
+
const value = untracked(target);
|
|
1482
|
+
if (!isRecord(value) || !(prop in value)) {
|
|
1483
|
+
if (isSignal(target[prop]) && target[prop][STORE]) {
|
|
1484
|
+
delete target[prop];
|
|
1485
|
+
}
|
|
1486
|
+
return target[prop];
|
|
1487
|
+
}
|
|
1488
|
+
if (!isSignal(target[prop])) {
|
|
1489
|
+
Object.defineProperty(target, prop, {
|
|
1490
|
+
value: derived(target, prop),
|
|
1491
|
+
configurable: true,
|
|
1492
|
+
});
|
|
1493
|
+
target[prop][STORE] = true;
|
|
1494
|
+
}
|
|
1495
|
+
return toStore(target[prop]);
|
|
1496
|
+
},
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
function store(value, opt) {
|
|
1500
|
+
return toStore(signal(value, opt));
|
|
1501
|
+
}
|
|
1502
|
+
function mutableStore(value, opt) {
|
|
1503
|
+
return toStore(mutable(value, opt));
|
|
1504
|
+
}
|
|
1265
1505
|
|
|
1266
1506
|
// Internal dummy store for server-side rendering
|
|
1267
1507
|
const noopStore = {
|
|
@@ -1324,7 +1564,7 @@ const noopStore = {
|
|
|
1324
1564
|
* }
|
|
1325
1565
|
* ```
|
|
1326
1566
|
*/
|
|
1327
|
-
function stored(fallback, { key, store: providedStore, serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = false, equal = Object.is, onKeyChange = 'load', cleanupOldKey = false, ...rest }) {
|
|
1567
|
+
function stored(fallback, { key, store: providedStore, serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = false, equal = Object.is, onKeyChange = 'load', cleanupOldKey = false, validate = () => true, ...rest }) {
|
|
1328
1568
|
const isServer = isPlatformServer(inject(PLATFORM_ID));
|
|
1329
1569
|
const fallbackStore = isServer ? noopStore : localStorage;
|
|
1330
1570
|
const store = providedStore ?? fallbackStore;
|
|
@@ -1338,7 +1578,10 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
1338
1578
|
if (found === null)
|
|
1339
1579
|
return null;
|
|
1340
1580
|
try {
|
|
1341
|
-
|
|
1581
|
+
const deserialized = deserialize(found);
|
|
1582
|
+
if (!validate(deserialized))
|
|
1583
|
+
return null;
|
|
1584
|
+
return deserialized;
|
|
1342
1585
|
}
|
|
1343
1586
|
catch (err) {
|
|
1344
1587
|
if (isDevMode())
|
|
@@ -1363,23 +1606,14 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
1363
1606
|
equal,
|
|
1364
1607
|
};
|
|
1365
1608
|
const initialKey = untracked(keySig);
|
|
1366
|
-
const internal = signal(getValue(initialKey), ...(ngDevMode ?
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
...opt,
|
|
1375
|
-
equal: (a, b) => {
|
|
1376
|
-
if (a === null && b === null)
|
|
1377
|
-
return true;
|
|
1378
|
-
if (a === null || b === null)
|
|
1379
|
-
return false;
|
|
1380
|
-
return equal(a, b);
|
|
1381
|
-
},
|
|
1382
|
-
}]));
|
|
1609
|
+
const internal = signal(getValue(initialKey), { ...(ngDevMode ? { debugName: "internal" } : {}), ...opt,
|
|
1610
|
+
equal: (a, b) => {
|
|
1611
|
+
if (a === null && b === null)
|
|
1612
|
+
return true;
|
|
1613
|
+
if (a === null || b === null)
|
|
1614
|
+
return false;
|
|
1615
|
+
return equal(a, b);
|
|
1616
|
+
} });
|
|
1383
1617
|
let prevKey = initialKey;
|
|
1384
1618
|
if (onKeyChange === 'store') {
|
|
1385
1619
|
effect(() => {
|
|
@@ -1457,10 +1691,10 @@ class MessageBus {
|
|
|
1457
1691
|
this.channel.removeEventListener('message', listener);
|
|
1458
1692
|
this.listeners.delete(id);
|
|
1459
1693
|
}
|
|
1460
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.
|
|
1461
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.
|
|
1694
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1695
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MessageBus, providedIn: 'root' });
|
|
1462
1696
|
}
|
|
1463
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.
|
|
1697
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MessageBus, decorators: [{
|
|
1464
1698
|
type: Injectable,
|
|
1465
1699
|
args: [{
|
|
1466
1700
|
providedIn: 'root',
|
|
@@ -1743,5 +1977,5 @@ function withHistory(source, opt) {
|
|
|
1743
1977
|
* Generated bundle index. Do not edit.
|
|
1744
1978
|
*/
|
|
1745
1979
|
|
|
1746
|
-
export { combineWith, debounce, debounced, derived, distinct, elementVisibility, filter, isDerivation, isMutable, map, mapArray, mediaQuery, mousePosition, mutable, nestedEffect, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toWritable, until, windowSize, withHistory };
|
|
1980
|
+
export { combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, indexArray, isDerivation, isMutable, keyArray, map, mapArray, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, sensors, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
|
|
1747
1981
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|