@mmstack/primitives 20.8.0 → 20.9.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/README.md +38 -1
- package/fesm2022/mmstack-primitives.mjs +264 -44
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +85 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ npm install @mmstack/primitives
|
|
|
19
19
|
- [Timing & propagation](#timing--propagation) — `debounced`, `throttled`, `until`
|
|
20
20
|
- [Reactive collections](#reactive-collections) — `indexArray`, `keyArray`, `mapObject`
|
|
21
21
|
- [Effects](#effects) — `nestedEffect`
|
|
22
|
-
- [Concurrency & transitions](#concurrency--transitions) — `keepPrevious`, keep-alive (`MmActivity`), `pausable*` / `providePausableOptions`, Suspense (`mm-suspense`), `startTransition` / `startTransaction`, `holdUntilReady`
|
|
22
|
+
- [Concurrency & transitions](#concurrency--transitions) — `keepPrevious`, keep-alive (`MmActivity`), `pausable*` / `providePausableOptions`, Suspense (`mm-suspense`), hold-and-swap (`*mmTransition`), `startTransition` / `startTransaction`, `holdUntilReady`
|
|
23
23
|
- [History & persistence](#history--persistence) — `withHistory`, `stored`, `tabSync`
|
|
24
24
|
- [Performance helpers](#performance-helpers) — `chunked`, `pooled` / `pooledArray` / `pooledMap` / `pooledSet`
|
|
25
25
|
- [Sensors](#sensors) — `sensor()` facade + browser-state signals
|
|
@@ -119,6 +119,10 @@ Top-level array support isn't exposed yet — use `indexArray` / `keyArray` for
|
|
|
119
119
|
|
|
120
120
|
**Union leaves (perf opt-in).** `noUnionLeaves: true` promises no node ever flips between a leaf and a sub-store, so each node's leaf-ness is resolved once on first access and cached instead of staying reactive. Off by default — leave it off if a value can switch between a primitive and an object/array.
|
|
121
121
|
|
|
122
|
+
**Unions are fully supported by default.** A node may flip between array ↔ record ↔ primitive ↔ `null` freely: routing (`keys`/iteration/prototype) follows the live kind, and a child signal you grabbed **before** a flip stays correct after it — reads resolve against the new shape (`undefined` through a `null` parent, no throw) and writes copy by the container's live shape, so writing through a pre-flip child never turns an array into a plain object.
|
|
123
|
+
|
|
124
|
+
> Reserved keys: `set`, `update`, `mutate`, `inline`, `asReadonly` (and `extend`, until its removal next minor) resolve to the signal's own methods, so record keys with those names aren't reachable as child stores — read them off the value (`s().set`) instead.
|
|
125
|
+
|
|
122
126
|
### `extendStore` (scoped overlay)
|
|
123
127
|
|
|
124
128
|
`extendStore(store, seed)` (on any store kind) creates a **scoped overlay** — a child store that **shares** the parent's signals for inherited keys (the same `WritableSignal`: writes go through to the parent and parent changes flow down) while keeping the seed and any new keys in a **local layer** that never propagates upward. No diffing, no syncing — local keys simply aren't wired to the parent.
|
|
@@ -437,6 +441,25 @@ This is also the pattern for coordinating resources registered _above_ a boundar
|
|
|
437
441
|
|
|
438
442
|
**Forwarding scope (advanced).** `provideForwardingTransitionScope()` provides a scope that can be **re-pointed at a different target at runtime** via `setTarget(scope | null)` — reads follow the current target, while `add`/`remove` pin to the target a resource was registered under (so re-pointing never strands a registration). It's the building block for a coordinator that hosts several independent sub-scopes and switches which one it observes — e.g. a router outlet that, per navigation, points at the incoming route's own scope (read it from any injector with `getTransitionScope(injector)`). Most apps reach for `provideTransitionScope()`; this is for that one extra level of control.
|
|
439
443
|
|
|
444
|
+
### Hold-and-swap — `*mmTransition`
|
|
445
|
+
|
|
446
|
+
The transition itself, for any branch change — tabs, wizard steps, master-detail. Suspense decides placeholder-vs-content _within_ a branch, but it can't stop an `@switch` from unmounting the old branch the instant the value flips. `*mmTransition` holds it: when the bound value changes, the **old view stays mounted and visible** (keeping its old value) while the **new view mounts hidden with its own transition scope**; resources created in the incoming subtree register there just by existing, and once they've gone in flight and settled the views swap in one frame.
|
|
447
|
+
|
|
448
|
+
```html
|
|
449
|
+
<div *mmTransition="selectedTab(); let tab">
|
|
450
|
+
@switch (tab) {
|
|
451
|
+
@case ('overview') {
|
|
452
|
+
<overview-pane />
|
|
453
|
+
}
|
|
454
|
+
@case ('activity') {
|
|
455
|
+
<activity-pane />
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
</div>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
The first render is immediate (nothing to hold). An interrupting change mid-hold destroys the half-ready hidden view and re-targets — the stable view stays visible until the newest branch settles. A branch that loads nothing swaps right after its first render, and per-view scopes mean the outgoing branch's background work can never delay the swap. `immediate: true` skips holding; `viewTransition: true` wraps the swap in `document.startViewTransition` (feature detected). This is `@mmstack/router-core`'s `<mm-transition-outlet>` without the router — same semantics, any signal as the trigger.
|
|
462
|
+
|
|
440
463
|
### `injectStartTransition`
|
|
441
464
|
|
|
442
465
|
The analog of React's `useTransition`. `startTransition(fn)` runs your state mutations (which commit immediately); any resource that reloads as a result **holds its value and reveals together once everything settles** — so a multi-resource update lands as one consistent frame instead of a torn mix of new and stale. The returned handle gives you a unified `pending` signal and a `done` promise for imperative coordination (disable a button, await completion).
|
|
@@ -461,6 +484,15 @@ const t = startTransaction(() => applyBulkEdit()); // live state updates; the di
|
|
|
461
484
|
await t.done; // committed, display revealed in one frame
|
|
462
485
|
```
|
|
463
486
|
|
|
487
|
+
Every exit settles: a throwing body rolls back, and if the calling context is **destroyed
|
|
488
|
+
mid-flight** the hold is released (writes kept) and `done` resolves — a transaction can never
|
|
489
|
+
leave a surviving ancestor scope frozen.
|
|
490
|
+
|
|
491
|
+
Attribution is **per transaction**: a load already in flight when it starts is not adopted —
|
|
492
|
+
it can neither commit the transaction early nor block its settle. (The same applies to
|
|
493
|
+
`startTransition`.) A pre-existing flight re-triggered by the transaction's own writes counts
|
|
494
|
+
once it restarts.
|
|
495
|
+
|
|
464
496
|
### `holdUntilReady`
|
|
465
497
|
|
|
466
498
|
The **structural** counterpart to `keepPrevious`: where that holds a _value_ through a reload, this holds a _structure_ through a swap. Given a `target` signal and a `ready` predicate, it keeps yielding the previous value until `ready()` is true, then swaps to the current target. Mount the incoming structure off to the side so its resources can settle and flip `ready`, keep showing the held one meanwhile, and let the old one go once `ready` releases the swap. (`@mmstack/router-core`'s `<mm-transition-outlet>` is this pattern applied to routes.)
|
|
@@ -677,6 +709,11 @@ outer one on the same tree (e.g. a nested sortable). Reads are throttled
|
|
|
677
709
|
(`throttle`, default 16ms); `drag.unthrottled()` exposes the un-throttled view
|
|
678
710
|
for logic that needs the exact release position.
|
|
679
711
|
|
|
712
|
+
The idle state carries the **end reason**: `cancelled` is `true` when the gesture
|
|
713
|
+
was aborted (Escape, `pointercancel`, `.cancel()`) rather than released, and stays
|
|
714
|
+
set until the next `pointerdown` — so a drag consumer can tell "drop here" from
|
|
715
|
+
"abort" (`@mmstack/dnd` uses this to cancel instead of committing).
|
|
716
|
+
|
|
680
717
|
```typescript
|
|
681
718
|
import { sensor } from '@mmstack/primitives';
|
|
682
719
|
|
|
@@ -640,6 +640,38 @@ function provideForwardingTransitionScope() {
|
|
|
640
640
|
function getTransitionScope(injector) {
|
|
641
641
|
return injector.get(TRANSITION_SCOPE, null);
|
|
642
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* @internal Transaction-attributed pending for `startTransition`/`startTransaction`: like
|
|
645
|
+
* `scope.pending`, but loads already in flight when the tracker is created are NOT attributed —
|
|
646
|
+
* a pre-existing background load can neither settle the transaction early nor block its settle
|
|
647
|
+
* forever. A pre-existing flight is excluded only until it first settles; a later re-trigger of
|
|
648
|
+
* the same resource (e.g. the transaction's write changed its request) counts as the
|
|
649
|
+
* transaction's own work.
|
|
650
|
+
*/
|
|
651
|
+
function createAttributedPending(scope) {
|
|
652
|
+
const isInFlight = (ref) => {
|
|
653
|
+
const s = untracked(ref.status);
|
|
654
|
+
return s === 'loading' || s === 'reloading';
|
|
655
|
+
};
|
|
656
|
+
const preexisting = new Set(untracked(scope.resources).filter(isInFlight));
|
|
657
|
+
return computed(() => {
|
|
658
|
+
let pending = false;
|
|
659
|
+
for (const ref of scope.resources()) {
|
|
660
|
+
const s = ref.status();
|
|
661
|
+
const loading = s === 'loading' || s === 'reloading';
|
|
662
|
+
if (preexisting.has(ref)) {
|
|
663
|
+
// deletes are monotonic, so this stays sound under re-computation
|
|
664
|
+
if (loading)
|
|
665
|
+
continue;
|
|
666
|
+
preexisting.delete(ref);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (loading)
|
|
670
|
+
pending = true;
|
|
671
|
+
}
|
|
672
|
+
return pending;
|
|
673
|
+
});
|
|
674
|
+
}
|
|
643
675
|
/**
|
|
644
676
|
* Returns a register function bound to the nearest transition scope: it adds a resource
|
|
645
677
|
* to the scope and removes it when the caller's injection context is destroyed. Pass any
|
|
@@ -677,38 +709,43 @@ function registerResource(res, opt) {
|
|
|
677
709
|
function injectStartTransition() {
|
|
678
710
|
const scope = injectTransitionScope();
|
|
679
711
|
const injector = inject(Injector);
|
|
712
|
+
const destroyRef = inject(DestroyRef);
|
|
680
713
|
const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
|
|
681
714
|
return (fn) => {
|
|
715
|
+
// attributed: loads already in flight when the transition starts are not ours —
|
|
716
|
+
// they can neither settle this transition early nor block it forever
|
|
717
|
+
const pending = createAttributedPending(scope);
|
|
682
718
|
untracked(fn);
|
|
683
719
|
let sawPending = false;
|
|
684
720
|
const done = new Promise((resolve) => {
|
|
721
|
+
const settle = () => {
|
|
722
|
+
releaseDestroy();
|
|
723
|
+
watcher.destroy();
|
|
724
|
+
resolve();
|
|
725
|
+
};
|
|
685
726
|
const watcher = effect(() => {
|
|
686
|
-
const p =
|
|
727
|
+
const p = pending();
|
|
687
728
|
if (p)
|
|
688
729
|
sawPending = true;
|
|
689
730
|
// settle: requests went in flight and then drained
|
|
690
|
-
if (sawPending && !p)
|
|
691
|
-
|
|
692
|
-
resolve();
|
|
693
|
-
}
|
|
731
|
+
if (sawPending && !p)
|
|
732
|
+
settle();
|
|
694
733
|
}, ...(ngDevMode ? [{ debugName: "watcher", injector }] : [{ injector }]));
|
|
734
|
+
// a destroy mid-flight kills the watcher — resolve so awaiters never hang
|
|
735
|
+
const releaseDestroy = destroyRef.onDestroy(settle);
|
|
695
736
|
if (onServer) {
|
|
696
|
-
if (!untracked(
|
|
697
|
-
|
|
698
|
-
resolve();
|
|
699
|
-
}
|
|
737
|
+
if (!untracked(pending))
|
|
738
|
+
settle();
|
|
700
739
|
return;
|
|
701
740
|
}
|
|
702
741
|
// no-async fallback: once the reactive system has processed the writes (afterNextRender),
|
|
703
742
|
// if nothing ever went in flight, the transition is already complete.
|
|
704
743
|
afterNextRender(() => {
|
|
705
|
-
if (!sawPending && !untracked(
|
|
706
|
-
|
|
707
|
-
resolve();
|
|
708
|
-
}
|
|
744
|
+
if (!sawPending && !untracked(pending))
|
|
745
|
+
settle();
|
|
709
746
|
}, { injector });
|
|
710
747
|
});
|
|
711
|
-
return { pending
|
|
748
|
+
return { pending, done };
|
|
712
749
|
};
|
|
713
750
|
}
|
|
714
751
|
|
|
@@ -779,8 +816,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
779
816
|
}] });
|
|
780
817
|
/**
|
|
781
818
|
* Unscoped suspense boundary — **reads the ambient scope** instead of providing one. For cases where
|
|
782
|
-
* the resources to coordinate are registered *above* the boundary
|
|
783
|
-
* manifests/connectors register at a higher injector), so the boundary observes that outer scope
|
|
819
|
+
* the resources to coordinate are registered *above* the boundary so the boundary observes that outer scope
|
|
784
820
|
* rather than opening a fresh one. Pair with a `provideTransitionScope()` (or another boundary) in an
|
|
785
821
|
* ancestor.
|
|
786
822
|
*/
|
|
@@ -845,9 +881,13 @@ function runInTransaction(txn, fn) {
|
|
|
845
881
|
function injectStartTransaction() {
|
|
846
882
|
const scope = injectTransitionScope();
|
|
847
883
|
const injector = inject(Injector);
|
|
884
|
+
const destroyRef = inject(DestroyRef);
|
|
848
885
|
const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
|
|
849
886
|
return (fn) => {
|
|
850
887
|
const txn = createTransaction();
|
|
888
|
+
// attributed: loads already in flight when the transaction starts are not ours —
|
|
889
|
+
// they can neither commit this transaction early nor block its settle forever
|
|
890
|
+
const pending = createAttributedPending(scope);
|
|
851
891
|
// Hold BEFORE the writes, so the display freezes at pre-transaction values.
|
|
852
892
|
scope.beginHold();
|
|
853
893
|
let finished = false;
|
|
@@ -864,6 +904,7 @@ function injectStartTransaction() {
|
|
|
864
904
|
if (finished)
|
|
865
905
|
return;
|
|
866
906
|
finished = true;
|
|
907
|
+
releaseDestroy();
|
|
867
908
|
watcher?.destroy();
|
|
868
909
|
if (restore)
|
|
869
910
|
txn.restore();
|
|
@@ -872,6 +913,10 @@ function injectStartTransaction() {
|
|
|
872
913
|
scope.endHold();
|
|
873
914
|
resolveDone();
|
|
874
915
|
};
|
|
916
|
+
// The scope may outlive the calling context (a component transacting on an ancestor
|
|
917
|
+
// boundary): a destroy mid-flight kills the settle watcher, so without this the hold
|
|
918
|
+
// would leak and freeze the surviving scope forever. Keep the writes — they landed live.
|
|
919
|
+
const releaseDestroy = destroyRef.onDestroy(() => finish(false));
|
|
875
920
|
try {
|
|
876
921
|
runInTransaction(txn, fn);
|
|
877
922
|
}
|
|
@@ -881,31 +926,195 @@ function injectStartTransaction() {
|
|
|
881
926
|
}
|
|
882
927
|
let sawPending = false;
|
|
883
928
|
watcher = effect(() => {
|
|
884
|
-
const p =
|
|
929
|
+
const p = pending();
|
|
885
930
|
if (p)
|
|
886
931
|
sawPending = true;
|
|
887
932
|
if (sawPending && !p)
|
|
888
933
|
finish(false);
|
|
889
934
|
}, { injector });
|
|
890
935
|
if (onServer) {
|
|
891
|
-
if (!untracked(
|
|
936
|
+
if (!untracked(pending))
|
|
892
937
|
finish(false);
|
|
893
938
|
}
|
|
894
939
|
else {
|
|
895
940
|
// no-async fallback: if nothing ever went in flight, settle once the writes are processed.
|
|
896
941
|
afterNextRender(() => {
|
|
897
|
-
if (!sawPending && !untracked(
|
|
942
|
+
if (!sawPending && !untracked(pending))
|
|
898
943
|
finish(false);
|
|
899
944
|
}, { injector });
|
|
900
945
|
}
|
|
901
946
|
return {
|
|
902
|
-
pending
|
|
947
|
+
pending,
|
|
903
948
|
done,
|
|
904
949
|
abort: () => finish(true),
|
|
905
950
|
};
|
|
906
951
|
};
|
|
907
952
|
}
|
|
908
953
|
|
|
954
|
+
/**
|
|
955
|
+
* Generic hold-and-swap: the non-router `TransitionRouterOutlet`. When the bound value changes,
|
|
956
|
+
* the OLD view stays mounted and visible (it keeps its old context value — that's the hold) while
|
|
957
|
+
* the NEW view mounts hidden with its **own transition scope**; resources created in the incoming
|
|
958
|
+
* subtree register into that scope just by existing, and once they've gone in flight and settled
|
|
959
|
+
* the views swap in one frame. Tabs, wizard steps, master-detail — any branch change that would
|
|
960
|
+
* otherwise flash a loading state.
|
|
961
|
+
*
|
|
962
|
+
* ```html
|
|
963
|
+
* <div *mmTransition="selectedTab(); let tab">
|
|
964
|
+
* @switch (tab) { ... }
|
|
965
|
+
* </div>
|
|
966
|
+
* ```
|
|
967
|
+
*
|
|
968
|
+
* Distinct from `<mm-suspense>` (the readiness gate): suspense decides placeholder-vs-content
|
|
969
|
+
* *within* one branch, but can't stop an `@switch` from unmounting the old branch the instant the
|
|
970
|
+
* value flips. This directive is the swap itself — the old branch survives until the new one is
|
|
971
|
+
* ready. Compose them freely: suspense inside a transitioned branch handles its first load.
|
|
972
|
+
*
|
|
973
|
+
* Semantics mirror the outlet: the first render is immediate (nothing to hold); an interrupting
|
|
974
|
+
* value change mid-hold destroys the half-ready hidden view and re-targets; a branch that loads
|
|
975
|
+
* nothing swaps right after its first render. Per-view scopes mean the outgoing branch's
|
|
976
|
+
* background work can never delay the swap. Set `mmTransitionImmediate` to skip holding, and
|
|
977
|
+
* `mmTransitionViewTransition` to wrap the swap in `document.startViewTransition` (feature
|
|
978
|
+
* detected). On the server every change swaps immediately.
|
|
979
|
+
*/
|
|
980
|
+
class MmTransition {
|
|
981
|
+
tpl = inject(TemplateRef);
|
|
982
|
+
vcr = inject(ViewContainerRef);
|
|
983
|
+
parent = inject(Injector);
|
|
984
|
+
onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
|
|
985
|
+
/** The value whose changes are transitioned. Each view keeps the value it was created with. */
|
|
986
|
+
value = input.required(...(ngDevMode ? [{ debugName: "value", alias: 'mmTransition' }] : [{ alias: 'mmTransition' }]));
|
|
987
|
+
/** Skip holding entirely — every change swaps at once (the plain re-render behavior). */
|
|
988
|
+
immediate = input(false, ...(ngDevMode ? [{ debugName: "immediate", alias: 'mmTransitionImmediate' }] : [{ alias: 'mmTransitionImmediate' }]));
|
|
989
|
+
/** Wrap the swap in the View Transitions API for an animated cross-fade (feature detected). */
|
|
990
|
+
viewTransition = input(false, ...(ngDevMode ? [{ debugName: "viewTransition", alias: 'mmTransitionViewTransition' }] : [{
|
|
991
|
+
alias: 'mmTransitionViewTransition',
|
|
992
|
+
}]));
|
|
993
|
+
current = null;
|
|
994
|
+
incoming = null;
|
|
995
|
+
/** Bumped on every re-target/teardown so a superseded (possibly deferred) swap can't commit. */
|
|
996
|
+
swapEpoch = 0;
|
|
997
|
+
holding = signal(false, ...(ngDevMode ? [{ debugName: "holding" }] : []));
|
|
998
|
+
/** True while an incoming view is mounted hidden, waiting to settle. */
|
|
999
|
+
pending = this.holding.asReadonly();
|
|
1000
|
+
static ngTemplateContextGuard(dir,
|
|
1001
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1002
|
+
ctx) {
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
constructor() {
|
|
1006
|
+
effect(() => {
|
|
1007
|
+
const v = this.value();
|
|
1008
|
+
untracked(() => this.onValue(v));
|
|
1009
|
+
});
|
|
1010
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1011
|
+
this.swapEpoch++; // a deferred view-transition callback must not touch destroyed state
|
|
1012
|
+
this.dropIncoming();
|
|
1013
|
+
// `current` is destroyed with the container
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
onValue(v) {
|
|
1017
|
+
if (!this.current) {
|
|
1018
|
+
// first render: nothing to hold yet — show immediately (also what SSR serializes)
|
|
1019
|
+
this.current = this.createView(v).view;
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
this.dropIncoming(); // an interrupting change supersedes the previous hold
|
|
1023
|
+
this.swapEpoch++;
|
|
1024
|
+
const epoch = this.swapEpoch;
|
|
1025
|
+
if (this.onServer || this.immediate()) {
|
|
1026
|
+
this.finishSwap(epoch, this.createView(v).view);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const { view, scope } = this.createView(v);
|
|
1030
|
+
this.setHidden(view, true);
|
|
1031
|
+
this.holding.set(true);
|
|
1032
|
+
// Registration happens synchronously during view creation, so a resource already in
|
|
1033
|
+
// flight counts from the start; later kickoffs are caught by the watcher.
|
|
1034
|
+
let sawPending = untracked(scope.pending);
|
|
1035
|
+
const watcher = effect(() => {
|
|
1036
|
+
const pending = scope.pending();
|
|
1037
|
+
untracked(() => {
|
|
1038
|
+
if (epoch !== this.swapEpoch)
|
|
1039
|
+
return;
|
|
1040
|
+
if (pending)
|
|
1041
|
+
sawPending = true;
|
|
1042
|
+
if (sawPending && !pending)
|
|
1043
|
+
this.commitSwap(epoch, view);
|
|
1044
|
+
});
|
|
1045
|
+
}, ...(ngDevMode ? [{ debugName: "watcher", injector: this.parent }] : [{ injector: this.parent }]));
|
|
1046
|
+
this.incoming = { view, watcher };
|
|
1047
|
+
// Fallback for a branch that loads nothing.
|
|
1048
|
+
afterNextRender(() => {
|
|
1049
|
+
if (epoch === this.swapEpoch &&
|
|
1050
|
+
!sawPending &&
|
|
1051
|
+
!untracked(scope.pending)) {
|
|
1052
|
+
this.commitSwap(epoch, view);
|
|
1053
|
+
}
|
|
1054
|
+
}, { injector: this.parent });
|
|
1055
|
+
}
|
|
1056
|
+
commitSwap(epoch, view) {
|
|
1057
|
+
if (epoch !== this.swapEpoch)
|
|
1058
|
+
return;
|
|
1059
|
+
if (this.viewTransition() &&
|
|
1060
|
+
typeof document !== 'undefined' &&
|
|
1061
|
+
document.startViewTransition) {
|
|
1062
|
+
// the browser snapshots the old frame first; the epoch guard covers the deferral
|
|
1063
|
+
document.startViewTransition(() => this.finishSwap(epoch, view));
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
this.finishSwap(epoch, view);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
/** The actual swap: destroy the old view, reveal the new one. Always instant. */
|
|
1070
|
+
finishSwap(epoch, view) {
|
|
1071
|
+
if (epoch !== this.swapEpoch)
|
|
1072
|
+
return; // superseded while deferred — not ours to commit
|
|
1073
|
+
this.swapEpoch++; // consume: the watcher and the render fallback can both fire, one commits
|
|
1074
|
+
this.current?.destroy();
|
|
1075
|
+
this.setHidden(view, false);
|
|
1076
|
+
this.current = view;
|
|
1077
|
+
this.incoming?.watcher.destroy();
|
|
1078
|
+
this.incoming = null;
|
|
1079
|
+
this.holding.set(false);
|
|
1080
|
+
}
|
|
1081
|
+
dropIncoming() {
|
|
1082
|
+
if (!this.incoming)
|
|
1083
|
+
return;
|
|
1084
|
+
this.incoming.watcher.destroy();
|
|
1085
|
+
this.incoming.view.destroy();
|
|
1086
|
+
this.incoming = null;
|
|
1087
|
+
this.holding.set(false);
|
|
1088
|
+
}
|
|
1089
|
+
createView(v) {
|
|
1090
|
+
// Each view gets its own scope, so its subtree's resources register here by existing —
|
|
1091
|
+
// and the outgoing view's background work can't block the swap (per-view isolation).
|
|
1092
|
+
const injector = Injector.create({
|
|
1093
|
+
parent: this.parent,
|
|
1094
|
+
providers: [provideTransitionScope()],
|
|
1095
|
+
});
|
|
1096
|
+
const scope = getTransitionScope(injector);
|
|
1097
|
+
const view = this.vcr.createEmbeddedView(this.tpl, { $implicit: v, mmTransition: v }, { injector });
|
|
1098
|
+
return { view, scope };
|
|
1099
|
+
}
|
|
1100
|
+
setHidden(view, hidden) {
|
|
1101
|
+
for (const node of view.rootNodes) {
|
|
1102
|
+
// covers HTML and SVG roots; text/comment roots can't be styled — prefer an element root
|
|
1103
|
+
if (node instanceof HTMLElement || node instanceof SVGElement)
|
|
1104
|
+
node.style.display = hidden ? 'none' : '';
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: MmTransition, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1108
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.17", type: MmTransition, isStandalone: true, selector: "[mmTransition]", inputs: { value: { classPropertyName: "value", publicName: "mmTransition", isSignal: true, isRequired: true, transformFunction: null }, immediate: { classPropertyName: "immediate", publicName: "mmTransitionImmediate", isSignal: true, isRequired: false, transformFunction: null }, viewTransition: { classPropertyName: "viewTransition", publicName: "mmTransitionViewTransition", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["mmTransition"], ngImport: i0 });
|
|
1109
|
+
}
|
|
1110
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: MmTransition, decorators: [{
|
|
1111
|
+
type: Directive,
|
|
1112
|
+
args: [{
|
|
1113
|
+
selector: '[mmTransition]',
|
|
1114
|
+
exportAs: 'mmTransition',
|
|
1115
|
+
}]
|
|
1116
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "mmTransition", required: true }] }], immediate: [{ type: i0.Input, args: [{ isSignal: true, alias: "mmTransitionImmediate", required: false }] }], viewTransition: [{ type: i0.Input, args: [{ isSignal: true, alias: "mmTransitionViewTransition", required: false }] }] } });
|
|
1117
|
+
|
|
909
1118
|
/**
|
|
910
1119
|
* @internal
|
|
911
1120
|
*/
|
|
@@ -2917,9 +3126,13 @@ const IDLE = {
|
|
|
2917
3126
|
button: -1,
|
|
2918
3127
|
pointerType: '',
|
|
2919
3128
|
origin: null,
|
|
3129
|
+
cancelled: false,
|
|
2920
3130
|
};
|
|
3131
|
+
/** Terminal state of an aborted gesture — same idle shape, `cancelled: true`. */
|
|
3132
|
+
const CANCELLED = { ...IDLE, cancelled: true };
|
|
2921
3133
|
function stateEqual(a, b) {
|
|
2922
3134
|
return (a.active === b.active &&
|
|
3135
|
+
a.cancelled === b.cancelled &&
|
|
2923
3136
|
a.pointerId === b.pointerId &&
|
|
2924
3137
|
a.current.x === b.current.x &&
|
|
2925
3138
|
a.current.y === b.current.y &&
|
|
@@ -2997,7 +3210,7 @@ function createPointerDrag(opt) {
|
|
|
2997
3210
|
ctrl: e.ctrlKey,
|
|
2998
3211
|
meta: e.metaKey,
|
|
2999
3212
|
});
|
|
3000
|
-
const end = () => {
|
|
3213
|
+
const end = (cancelled = false) => {
|
|
3001
3214
|
gesture?.abort();
|
|
3002
3215
|
gesture = null;
|
|
3003
3216
|
activePointerId = null;
|
|
@@ -3005,8 +3218,8 @@ function createPointerDrag(opt) {
|
|
|
3005
3218
|
activePointerType = '';
|
|
3006
3219
|
activeOrigin = null;
|
|
3007
3220
|
activated = false;
|
|
3008
|
-
state.set(IDLE);
|
|
3009
|
-
state.flush(); // terminal transition: reflect
|
|
3221
|
+
state.set(cancelled ? CANCELLED : IDLE);
|
|
3222
|
+
state.flush(); // terminal transition: reflect idle now, not on the trailing edge
|
|
3010
3223
|
};
|
|
3011
3224
|
const onMove = (e) => {
|
|
3012
3225
|
if (e.pointerId !== activePointerId)
|
|
@@ -3026,6 +3239,7 @@ function createPointerDrag(opt) {
|
|
|
3026
3239
|
button: activeButton, // pointermove button is -1; keep the down-button
|
|
3027
3240
|
pointerType: activePointerType,
|
|
3028
3241
|
origin: activeOrigin,
|
|
3242
|
+
cancelled: false,
|
|
3029
3243
|
});
|
|
3030
3244
|
};
|
|
3031
3245
|
const onUp = (e) => {
|
|
@@ -3034,11 +3248,11 @@ function createPointerDrag(opt) {
|
|
|
3034
3248
|
};
|
|
3035
3249
|
const onCancel = (e) => {
|
|
3036
3250
|
if (e.pointerId === activePointerId)
|
|
3037
|
-
end();
|
|
3251
|
+
end(true);
|
|
3038
3252
|
};
|
|
3039
3253
|
const onKey = (e) => {
|
|
3040
3254
|
if (e.key === 'Escape' && activePointerId !== null)
|
|
3041
|
-
end();
|
|
3255
|
+
end(true);
|
|
3042
3256
|
};
|
|
3043
3257
|
const onDown = (el) => (e) => {
|
|
3044
3258
|
if (activePointerId !== null)
|
|
@@ -3083,6 +3297,7 @@ function createPointerDrag(opt) {
|
|
|
3083
3297
|
button: e.button,
|
|
3084
3298
|
pointerType: activePointerType,
|
|
3085
3299
|
origin: activeOrigin,
|
|
3300
|
+
cancelled: false,
|
|
3086
3301
|
});
|
|
3087
3302
|
};
|
|
3088
3303
|
const attach = (el) => {
|
|
@@ -3092,7 +3307,7 @@ function createPointerDrag(opt) {
|
|
|
3092
3307
|
});
|
|
3093
3308
|
return () => {
|
|
3094
3309
|
controller.abort();
|
|
3095
|
-
end();
|
|
3310
|
+
end(true); // teardown mid-gesture is an abort, not a drop
|
|
3096
3311
|
};
|
|
3097
3312
|
};
|
|
3098
3313
|
if (isSignal(target)) {
|
|
@@ -3110,7 +3325,7 @@ function createPointerDrag(opt) {
|
|
|
3110
3325
|
}
|
|
3111
3326
|
const base = state.asReadonly();
|
|
3112
3327
|
base.unthrottled = state.original;
|
|
3113
|
-
base.cancel = end;
|
|
3328
|
+
base.cancel = () => end(true);
|
|
3114
3329
|
return base;
|
|
3115
3330
|
}
|
|
3116
3331
|
|
|
@@ -3703,30 +3918,35 @@ function getCachedChild(target, prop, build, cache, cleanupRegistry) {
|
|
|
3703
3918
|
cleanupRegistry.register(proxy, { target, prop }, ref);
|
|
3704
3919
|
return proxy;
|
|
3705
3920
|
}
|
|
3921
|
+
/**
|
|
3922
|
+
* @internal Whether a mutable parent's child value must always re-notify: in-place mutation
|
|
3923
|
+
* keeps an object child's reference stable, so `Object.is` would swallow the change. Decided
|
|
3924
|
+
* per-VALUE (not snapshotted at build) so a union child that becomes an object later still
|
|
3925
|
+
* propagates parent-level mutations.
|
|
3926
|
+
*/
|
|
3927
|
+
function mutableChildEqual(a, b) {
|
|
3928
|
+
if (typeof a === 'object' && a !== null)
|
|
3929
|
+
return false;
|
|
3930
|
+
return Object.is(a, b);
|
|
3931
|
+
}
|
|
3706
3932
|
/**
|
|
3707
3933
|
* @internal Builds the derived child signal for `prop` and wraps it as an array/object substore.
|
|
3708
|
-
*
|
|
3709
|
-
*
|
|
3710
|
-
* is constructed.
|
|
3934
|
+
* Both the read (`v?.[prop]`) and the write (`createFallbackOnChange` copies by the container's
|
|
3935
|
+
* LIVE shape) are shape-adaptive, so a child cached before an array↔record↔null union flip stays
|
|
3936
|
+
* correct after it. The only place a child node is constructed — shared by every container kind.
|
|
3711
3937
|
*/
|
|
3712
3938
|
function buildChildNode(target, prop, isMutableSource, options) {
|
|
3713
3939
|
const value = untracked(target);
|
|
3714
|
-
const valueIsRecord = isRecord(value);
|
|
3715
|
-
const valueIsArray = Array.isArray(value);
|
|
3716
3940
|
const nodeVivify = resolveVivify(value, options.vivify);
|
|
3717
3941
|
const vivifyFn = createVivify(nodeVivify);
|
|
3718
|
-
const equalFn = (
|
|
3719
|
-
|
|
3720
|
-
typeof value[prop] === 'object'
|
|
3721
|
-
? () => false
|
|
3942
|
+
const equalFn = isMutableSource && (isRecord(value) || Array.isArray(value))
|
|
3943
|
+
? mutableChildEqual
|
|
3722
3944
|
: undefined;
|
|
3723
|
-
const computation =
|
|
3724
|
-
|
|
3725
|
-
:
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
equal: equalFn,
|
|
3729
|
-
});
|
|
3945
|
+
const computation = derived(target, {
|
|
3946
|
+
from: (v) => v?.[prop],
|
|
3947
|
+
onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
|
|
3948
|
+
equal: equalFn,
|
|
3949
|
+
});
|
|
3730
3950
|
const childSample = untracked(computation);
|
|
3731
3951
|
const childVivify = resolveVivify(childSample, options.vivify);
|
|
3732
3952
|
const proxy = toStore(computation, options);
|
|
@@ -4623,5 +4843,5 @@ function withHistory(sourceOrValue, opt) {
|
|
|
4623
4843
|
* Generated bundle index. Do not edit.
|
|
4624
4844
|
*/
|
|
4625
4845
|
|
|
4626
|
-
export { MmActivity, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pointerDrag, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePausableOptions, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
|
|
4846
|
+
export { MmActivity, MmTransition, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createAttributedPending, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pointerDrag, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePausableOptions, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
|
|
4627
4847
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|