@mmstack/primitives 21.0.0 → 21.0.1
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 +337 -171
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-primitives.d.ts +241 -91
|
@@ -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,38 @@ 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
|
+
}
|
|
1265
1440
|
|
|
1266
1441
|
// Internal dummy store for server-side rendering
|
|
1267
1442
|
const noopStore = {
|
|
@@ -1363,23 +1538,14 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
|
|
|
1363
1538
|
equal,
|
|
1364
1539
|
};
|
|
1365
1540
|
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
|
-
}]));
|
|
1541
|
+
const internal = signal(getValue(initialKey), { ...(ngDevMode ? { debugName: "internal" } : {}), ...opt,
|
|
1542
|
+
equal: (a, b) => {
|
|
1543
|
+
if (a === null && b === null)
|
|
1544
|
+
return true;
|
|
1545
|
+
if (a === null || b === null)
|
|
1546
|
+
return false;
|
|
1547
|
+
return equal(a, b);
|
|
1548
|
+
} });
|
|
1383
1549
|
let prevKey = initialKey;
|
|
1384
1550
|
if (onKeyChange === 'store') {
|
|
1385
1551
|
effect(() => {
|
|
@@ -1457,10 +1623,10 @@ class MessageBus {
|
|
|
1457
1623
|
this.channel.removeEventListener('message', listener);
|
|
1458
1624
|
this.listeners.delete(id);
|
|
1459
1625
|
}
|
|
1460
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.
|
|
1461
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.
|
|
1626
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1627
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MessageBus, providedIn: 'root' });
|
|
1462
1628
|
}
|
|
1463
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.
|
|
1629
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: MessageBus, decorators: [{
|
|
1464
1630
|
type: Injectable,
|
|
1465
1631
|
args: [{
|
|
1466
1632
|
providedIn: 'root',
|
|
@@ -1743,5 +1909,5 @@ function withHistory(source, opt) {
|
|
|
1743
1909
|
* Generated bundle index. Do not edit.
|
|
1744
1910
|
*/
|
|
1745
1911
|
|
|
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 };
|
|
1912
|
+
export { combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, indexArray, isDerivation, isMutable, keyArray, map, mapArray, mediaQuery, mousePosition, mutable, nestedEffect, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, sensors, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toWritable, until, windowSize, withHistory };
|
|
1747
1913
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|